mirror of
https://github.com/weewx/weewx.git
synced 2026-05-19 15:25:32 -04:00
merge util changes from trunk. merge catchup-on-startup changes from trunk. merge fousb, ws23xx, te923, and wmr200 driver changes from trunk.
This commit is contained in:
@@ -4,6 +4,12 @@
|
||||
|
||||
API_VERSION: 2
|
||||
|
||||
Compatibility:
|
||||
|
||||
US National Weather Service (NWS) point forecasts as of July 2013
|
||||
Weather Underground (WU) forecast10day and hourly10day as of July 2013
|
||||
XTide 2.10 (possibly earlier versions as well)
|
||||
|
||||
Design
|
||||
|
||||
The forecasting module supports various forecast methods for weather and
|
||||
@@ -123,8 +129,6 @@ Configuration
|
||||
#interval = 10800
|
||||
|
||||
[Databases]
|
||||
...
|
||||
# a typical installation will use either sqlite or mysql
|
||||
|
||||
[[forecast_sqlite]]
|
||||
root = %(WEEWX_ROOT)s
|
||||
@@ -313,7 +317,13 @@ Skin Configuration
|
||||
|
||||
Variables for Templates
|
||||
|
||||
Here are the variables that can be used in template files.
|
||||
This section shows some of the variables that can be used in template files.
|
||||
|
||||
Labels
|
||||
|
||||
Labels are grouped by module and identified by key. For example, specifying
|
||||
$forecast.label('Zambretti', 'C') returns 'Becoming fine', and specifying
|
||||
$forecast.label('Weather', 'temp') returns 'Temperature'.
|
||||
|
||||
$forecast.label(module, key)
|
||||
|
||||
@@ -390,9 +400,32 @@ $summary.obvis array
|
||||
# http://www.surf-forecast.com/
|
||||
# http://ocean.peterbrueggeman.com/tidepredict.html
|
||||
|
||||
# FIXME: 'method' should be called 'source'
|
||||
# FIXME: WU defines the following:
|
||||
# maxhumidity
|
||||
# minhumidity
|
||||
# feelslike
|
||||
# mslp - mean sea level pressure
|
||||
|
||||
# FIXME: ensure compatibility with uk met office
|
||||
# http://www.metoffice.gov.uk/datapoint/product/uk-3hourly-site-specific-forecast
|
||||
# forecast in 3-hour increments for up to 5 days in the future
|
||||
# UVIndex (1-11)
|
||||
# feels like temperature
|
||||
# weather type (0-30)
|
||||
# visibility (UN, VP, PO, MO, GO, VG, EX)
|
||||
# textual description
|
||||
# wind direction is 16-point compass
|
||||
# air quality index
|
||||
# also see icons used for uk metoffice
|
||||
|
||||
# FIXME: add support for openweathermap if/when the api stabilizes
|
||||
# FIXME: 'method' should be called 'source'
|
||||
# FIXME: obvis should be an array?
|
||||
# FIXME: add field for tornado, hurricane/cyclone?
|
||||
|
||||
import hashlib
|
||||
import httplib
|
||||
import os, errno
|
||||
import socket
|
||||
import string
|
||||
import subprocess
|
||||
@@ -402,8 +435,8 @@ import time
|
||||
import urllib2
|
||||
|
||||
import weewx
|
||||
from weewx.wxengine import StdService
|
||||
import weeutil.weeutil
|
||||
from weewx.wxengine import StdService
|
||||
from weewx.cheetahgenerator import SearchList
|
||||
|
||||
try:
|
||||
@@ -433,8 +466,8 @@ def loginf(msg):
|
||||
def logerr(msg):
|
||||
logmsg(syslog.LOG_ERR, msg)
|
||||
|
||||
def get_int(config_dict, label, default_value):
|
||||
value = config_dict.get(label, default_value)
|
||||
def toint(label, value, default_value):
|
||||
"""convert to integer but also permit a value of None"""
|
||||
if isinstance(value, str) and value.lower() == 'none':
|
||||
value = None
|
||||
if value is not None:
|
||||
@@ -445,29 +478,28 @@ def get_int(config_dict, label, default_value):
|
||||
value = default_value
|
||||
return value
|
||||
|
||||
# FIXME: WU defines the following:
|
||||
# maxhumidity
|
||||
# minhumidity
|
||||
# feelslike
|
||||
# uvi - uv index
|
||||
# mslp - mean sea level pressure
|
||||
# condition
|
||||
# wx - imported from us nws forecast
|
||||
# fctcode - forecast code
|
||||
# there is overlap between condition, wx, and fctcode. also, each may contain
|
||||
# any combination of precip, obvis, and sky cover.
|
||||
def mkdir_p(path):
|
||||
"""equivalent to 'mkdir -p'"""
|
||||
try:
|
||||
os.makedirs(path)
|
||||
except OSError, e:
|
||||
if e.errno == errno.EEXIST and os.path.isdir(path):
|
||||
pass
|
||||
else:
|
||||
raise
|
||||
|
||||
def save_fc_data(fc, dirname, basename='forecast-data', msgs=None):
|
||||
"""save raw forecast data to disk, typically for diagnostics"""
|
||||
ts = int(time.time())
|
||||
tstr = time.strftime('%Y%m%d%H%M', time.localtime(ts))
|
||||
mkdir_p(dirname)
|
||||
fn = '%s/%s-%s' % (dirname, basename, tstr)
|
||||
with open(fn, 'w') as f:
|
||||
if msgs is not None:
|
||||
for m in msgs:
|
||||
f.write("%s\n" % m)
|
||||
f.write(fc)
|
||||
|
||||
# FIXME: ensure compatibility with uk met office
|
||||
# http://www.metoffice.gov.uk/datapoint/product/uk-3hourly-site-specific-forecast
|
||||
# forecast in 3-hour increments for up to 5 days in the future
|
||||
# UVIndex (1-11)
|
||||
# feels like temperature
|
||||
# weather type (0-30)
|
||||
# visibility (UN, VP, PO, MO, GO, VG, EX)
|
||||
# textual description
|
||||
# wind direction is 16-point compass
|
||||
# air quality index
|
||||
# also see icons used for uk metoffice
|
||||
|
||||
"""Database Schema
|
||||
|
||||
@@ -539,9 +571,6 @@ def get_int(config_dict, label, default_value):
|
||||
zcode used only by zambretti forecast
|
||||
"""
|
||||
|
||||
# FIXME: obvis should be an array?
|
||||
# FIXME: add field for tornado, hurricane/cyclone?
|
||||
|
||||
defaultForecastSchema = [('method', 'VARCHAR(10) NOT NULL'),
|
||||
('usUnits', 'INTEGER NOT NULL'), # weewx.US
|
||||
('dateTime', 'INTEGER NOT NULL'), # epoch
|
||||
@@ -649,7 +678,7 @@ weather_label_dict = {
|
||||
# types of precipitation
|
||||
'rain' : 'Rain',
|
||||
'rainshwrs' : 'Rain Showers',
|
||||
'sprinkles' : 'Rain Sprinkles', # FIXME: no db field
|
||||
'sprinkles' : 'Rain Sprinkles', # FIXME: no db field for this
|
||||
'tstms' : 'Thunderstorms',
|
||||
'drizzle' : 'Drizzle',
|
||||
'snow' : 'Snow',
|
||||
@@ -728,97 +757,148 @@ class Forecast(StdService):
|
||||
interval=1800, max_age=604800):
|
||||
super(Forecast, self).__init__(engine, config_dict)
|
||||
|
||||
# single database for all forecasts
|
||||
d = config_dict.get('Forecast', {})
|
||||
self.interval = int(d.get('interval', interval))
|
||||
self.max_age = get_int(d, 'max_age', max_age)
|
||||
|
||||
dd = config_dict['Forecast'].get(fid, {})
|
||||
self.interval = int(dd.get('interval', self.interval))
|
||||
self.max_age = get_int(dd, 'max_age', self.max_age)
|
||||
|
||||
self.database = d['database']
|
||||
self.table = d.get('table', 'archive')
|
||||
schema_str = d.get('schema', None)
|
||||
self.schema = weeutil.weeutil._get_object(schema_str) \
|
||||
if schema_str is not None else defaultForecastSchema
|
||||
|
||||
self.database = d['database']
|
||||
self.table = d.get('table', 'archive')
|
||||
# these options can be different for each forecast method
|
||||
|
||||
# how often to do the forecast
|
||||
self.interval = self._get_opt(d, fid, 'interval', interval)
|
||||
self.interval = int(self.interval)
|
||||
# how long to keep forecast records
|
||||
self.max_age = self._get_opt(d, fid, 'max_age', max_age)
|
||||
self.max_age = toint('max_age', self.max_age, None)
|
||||
# option to vacuum the sqlite database
|
||||
self.vacuum = self._get_opt(d, fid, 'vacuum', False)
|
||||
self.vacuum = weeutil.weeutil.tobool(self.vacuum)
|
||||
# how often to retry database failures
|
||||
self.db_max_tries = self._get_opt(d, fid, 'database_max_tries', 3)
|
||||
self.db_max_tries = int(self.db_max_tries)
|
||||
# how long to wait between retries, in seconds
|
||||
self.db_retry_wait = self._get_opt(d, fid, 'database_retry_wait', 10)
|
||||
self.db_retry_wait = int(self.db_retry_wait)
|
||||
# use single_thread for debugging
|
||||
self.single_thread = d.get('single_thread', False)
|
||||
self.updating = False
|
||||
|
||||
# option to vacuum the sqlite database when pruning
|
||||
self.vacuum = d.get('vacuum', False)
|
||||
self.single_thread = self._get_opt(d, fid, 'single_thread', False)
|
||||
self.single_thread = weeutil.weeutil.tobool(self.single_thread)
|
||||
# option to save raw forecast to disk
|
||||
self.save_raw = self._get_opt(d, fid, 'save_raw', False)
|
||||
self.save_raw = weeutil.weeutil.tobool(self.save_raw)
|
||||
# option to save failed foreast to disk for diagnosis
|
||||
self.save_failed = self._get_opt(d, fid, 'save_failed', False)
|
||||
self.save_failed = weeutil.weeutil.tobool(self.save_failed)
|
||||
# where to save the raw forecasts
|
||||
self.diag_dir = self._get_opt(d, fid, 'diagnostic_dir', '/var/tmp/fc')
|
||||
|
||||
self.method_id = fid
|
||||
self.last_ts = 0
|
||||
self.updating = False
|
||||
self.last_raw_digest = None
|
||||
self.last_fail_digest = None
|
||||
|
||||
# do the database setup here, as a way to check the schema
|
||||
# compatibility between database and software.
|
||||
archive = Forecast.setup_database(self.database,
|
||||
self.table, self.method_id,
|
||||
self.config_dict, self.schema)
|
||||
dbcol = archive.connection.columnsOf(self.table)
|
||||
memcol = [x[0] for x in self.schema]
|
||||
if dbcol != memcol:
|
||||
raise Exception('%s: schema mismatch: %s != %s' % (self.method_id,
|
||||
dbcol, memcol))
|
||||
|
||||
# find out when the last forecast happened
|
||||
self.last_ts = Forecast.get_last_forecast_ts(archive,
|
||||
self.table,
|
||||
self.method_id)
|
||||
# setup database
|
||||
with Forecast.setup_database(self.database,
|
||||
config_dict['Databases'][self.database],
|
||||
self.table, self.schema,
|
||||
self.method_id) as archive:
|
||||
# ensure schema on disk matches schema in memory
|
||||
dbcol = archive.connection.columnsOf(self.table)
|
||||
memcol = [x[0] for x in self.schema]
|
||||
if dbcol != memcol:
|
||||
raise Exception('%s: schema mismatch: %s != %s' %
|
||||
(self.method_id, dbcol, memcol))
|
||||
# find out when the last forecast happened
|
||||
self.last_ts = Forecast.get_last_forecast_ts(archive,
|
||||
self.table,
|
||||
self.method_id)
|
||||
|
||||
# ensure that the forecast has a chance to update on each new record
|
||||
self.bind(weewx.NEW_ARCHIVE_RECORD, self.update_forecast)
|
||||
|
||||
def _get_opt(self, d, fid, label, default_v):
|
||||
"""get an option from dict, prefer specialized value if one exists"""
|
||||
v = d.get(label, default_v)
|
||||
dd = d.get(fid, {})
|
||||
v = dd.get(label, v)
|
||||
return v
|
||||
|
||||
def save_raw_forecast(self, fc, basename='raw', msgs=None):
|
||||
m = hashlib.md5()
|
||||
m.update(fc)
|
||||
digest = m.hexdigest()
|
||||
if self.last_raw_digest == digest:
|
||||
return
|
||||
save_fc_data(fc, self.diag_dir, basename=basename, msgs=msgs)
|
||||
self.last_raw_digest = digest
|
||||
|
||||
def save_failed_forecast(self, fc, basename='fail', msgs=None):
|
||||
m = hashlib.md5()
|
||||
m.update(fc)
|
||||
digest = m.hexdigest()
|
||||
if self.last_fail_digest == digest:
|
||||
return
|
||||
save_fc_data(fc, self.diag_dir, basename=basename, msgs=msgs)
|
||||
self.last_fail_digest = digest
|
||||
|
||||
def update_forecast(self, event):
|
||||
if self.single_thread:
|
||||
self.do_forecast(event)
|
||||
else:
|
||||
if self.updating:
|
||||
logdbg('%s: update thread already running' % self.method_id)
|
||||
else:
|
||||
t = ForecastThread(self.do_forecast, event)
|
||||
t.setName(self.method_id + 'Thread')
|
||||
logdbg('%s: starting thread' % self.method_id)
|
||||
t.start()
|
||||
|
||||
def do_forecast(self, event):
|
||||
"""do the forecast if it is time, then save to database and prune."""
|
||||
self.updating = True
|
||||
now = time.time()
|
||||
if self.last_ts is None or now - self.interval > self.last_ts:
|
||||
try:
|
||||
fcast = self.get_forecast(event)
|
||||
if fcast is not None:
|
||||
archive = Forecast.setup_database(self.database,
|
||||
self.table,
|
||||
self.method_id,
|
||||
self.config_dict,
|
||||
self.schema)
|
||||
Forecast.save_forecast(archive, fcast)
|
||||
self.last_ts = now
|
||||
if self.max_age is not None:
|
||||
Forecast.prune_forecasts(archive,
|
||||
self.table,
|
||||
self.method_id,
|
||||
now - self.max_age,
|
||||
vacuum=self.vacuum)
|
||||
except Exception, e:
|
||||
logerr('%s: forecast failure: %s' % (self.method_id, e))
|
||||
elif self.updating:
|
||||
logdbg('%s: update thread already running' % self.method_id)
|
||||
elif time.time() - self.interval > self.last_ts:
|
||||
t = ForecastThread(self.do_forecast, event)
|
||||
t.setName(self.method_id + 'Thread')
|
||||
logdbg('%s: starting thread' % self.method_id)
|
||||
t.start()
|
||||
else:
|
||||
logdbg('%s: not yet time to do the forecast' % self.method_id)
|
||||
logdbg('%s: terminating thread' % self.method_id)
|
||||
self.updating = False
|
||||
|
||||
def do_forecast(self, event):
|
||||
self.updating = True
|
||||
archive = None
|
||||
try:
|
||||
records = self.get_forecast(event)
|
||||
if records is None:
|
||||
return
|
||||
archive = Forecast.setup_database(self.database,
|
||||
self.config_dict['Databases'][self.database],
|
||||
self.table, self.schema,
|
||||
self.method_id)
|
||||
Forecast.save_forecast(archive, records, self.method_id,
|
||||
self.db_max_tries, self.db_retry_wait)
|
||||
self.last_ts = int(time.time())
|
||||
if self.max_age is not None:
|
||||
Forecast.prune_forecasts(archive, self.table, self.method_id,
|
||||
self.last_ts - self.max_age,
|
||||
self.db_max_tries, self.db_retry_wait)
|
||||
if self.vacuum:
|
||||
Forecast.vacuum_database(archive, self.method_id)
|
||||
except Exception, e:
|
||||
logerr('%s: forecast failure: %s' % (self.method_id, e))
|
||||
finally:
|
||||
logdbg('%s: terminating thread' % self.method_id)
|
||||
if archive is not None:
|
||||
archive.close()
|
||||
self.updating = False
|
||||
|
||||
def get_forecast(self, event):
|
||||
"""get the forecast, return a forecast record or array of records."""
|
||||
"""get the forecast, return an array of forecast records."""
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def setup_database(dbname, dbcfg, table, schema, method_id='Unknown'):
|
||||
archive = weewx.archive.Archive.open_with_create(dbcfg, schema, table)
|
||||
logdbg("%s: using table '%s' in database '%s'" %
|
||||
(method_id, table, dbname))
|
||||
return archive
|
||||
|
||||
@staticmethod
|
||||
def get_last_forecast_ts(archive, table, method_id):
|
||||
sql = "select dateTime,issued_ts from %s where method = '%s' and dateTime = (select dateTime from %s where method = '%s' order by dateTime desc limit 1)" % (table, method_id, table, method_id)
|
||||
sql = "select dateTime,issued_ts from %s where method = '%s' and dateTime = (select dateTime from %s where method = '%s' order by dateTime desc limit 1) limit 1" % (table, method_id, table, method_id)
|
||||
r = archive.getSql(sql)
|
||||
if r is None:
|
||||
return None
|
||||
@@ -829,36 +909,60 @@ class Forecast(StdService):
|
||||
return int(r[0])
|
||||
|
||||
@staticmethod
|
||||
def save_forecast(archive, record):
|
||||
"""add a forecast record or array of records to the database.
|
||||
|
||||
record - dictionary with keys corresponding to database fields
|
||||
"""
|
||||
if record is None:
|
||||
return
|
||||
archive.addRecord(record)
|
||||
def save_forecast(archive, records,
|
||||
method_id='Unknown', max_tries=3, retry_wait=10):
|
||||
for count in range(max_tries):
|
||||
try:
|
||||
logdbg('%s: saving %d forecast records' %
|
||||
(method_id, len(records)))
|
||||
archive.addRecord(records, log_level=syslog.LOG_DEBUG)
|
||||
loginf('%s: saved %d forecast records' %
|
||||
(method_id, len(records)))
|
||||
break
|
||||
except Exception, e:
|
||||
logerr('%s: save failed (attempt %d of %d): %s' %
|
||||
(method_id, (count+1), max_tries, e))
|
||||
logdbg('%s: waiting %d seconds before retry' %
|
||||
(method_id, retry_wait))
|
||||
time.sleep(retry_wait)
|
||||
else:
|
||||
raise Exception('save failed after %d attempts' % max_tries)
|
||||
|
||||
@staticmethod
|
||||
def prune_forecasts(archive, table, method_id, ts, vacuum=False):
|
||||
"""remove old forecasts from the database
|
||||
def prune_forecasts(archive, table, method_id, ts,
|
||||
max_tries=3, retry_wait=10):
|
||||
"""remove forecasts older than ts from the database"""
|
||||
|
||||
ts - timestamp, in seconds. records older than this will be deleted.
|
||||
"""
|
||||
sql = "delete from %s where method = '%s' and dateTime < %d" % (table, method_id, ts)
|
||||
archive.getSql(sql)
|
||||
loginf('%s: deleted forecasts prior to %d' % (method_id, ts))
|
||||
sql = "delete from %s where method = '%s' and dateTime < %d" % (
|
||||
table, method_id, ts)
|
||||
for count in range(max_tries):
|
||||
try:
|
||||
logdbg('%s: deleting forecasts prior to %d' % (method_id, ts))
|
||||
archive.getSql(sql)
|
||||
loginf('%s: deleted forecasts prior to %d' % (method_id, ts))
|
||||
break
|
||||
except Exception, e:
|
||||
logerr('%s: prune failed (attempt %d of %d): %s' %
|
||||
(method_id, (count+1), max_tries, e))
|
||||
logdbg('%s: waiting %d seconds before retry' %
|
||||
(method_id, retry_wait))
|
||||
time.sleep(retry_wait)
|
||||
else:
|
||||
raise Exception('prune failed after %d attemps' % max_tries)
|
||||
|
||||
@staticmethod
|
||||
def vacuum_database(archive, method_id='Unknown'):
|
||||
# vacuum will only work on sqlite databases. it will compact the
|
||||
# database file. if we do not do this, the file grows even though
|
||||
# we prune records from the database. it should be ok to run this
|
||||
# on a mysql database - it will silently fail.
|
||||
if vacuum:
|
||||
try:
|
||||
archive.getSql('vacuum')
|
||||
except Exception, e:
|
||||
logdbg('vacuuming failed: %s' % e)
|
||||
pass
|
||||
try:
|
||||
logdbg('%s: vacuuming the database' % method_id)
|
||||
archive.getSql('vacuum')
|
||||
except Exception, e:
|
||||
logdbg('%s: vacuuming failed: %s' % (method_id, e))
|
||||
|
||||
# this method is used only by the unit tests
|
||||
@staticmethod
|
||||
def get_saved_forecasts(archive, table, method_id, since_ts=None):
|
||||
"""return saved forecasts since the indicated timestamp
|
||||
@@ -873,13 +977,6 @@ class Forecast(StdService):
|
||||
records.append(r)
|
||||
return records
|
||||
|
||||
@staticmethod
|
||||
def setup_database(database, table, method_id, config_dict, schema):
|
||||
archive = weewx.archive.Archive.open_with_create(config_dict['Databases'][database], schema, table)
|
||||
loginf("%s: using table '%s' in database '%s'" %
|
||||
(method_id, table, database))
|
||||
return archive
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Zambretti Forecaster
|
||||
@@ -900,7 +997,7 @@ class ZambrettiForecast(Forecast):
|
||||
def __init__(self, engine, config_dict):
|
||||
super(ZambrettiForecast, self).__init__(engine, config_dict, Z_KEY,
|
||||
interval=600)
|
||||
d = config_dict['Forecast'].get(Z_KEY, {})
|
||||
d = config_dict.get('Forecast', {}).get(Z_KEY, {})
|
||||
self.hemisphere = d.get('hemisphere', 'NORTH')
|
||||
self.lower_pressure = float(d.get('lower_pressure', 950.0))
|
||||
self.upper_pressure = float(d.get('upper_pressure', 1050.0))
|
||||
@@ -945,7 +1042,7 @@ class ZambrettiForecast(Forecast):
|
||||
record['event_ts'] = ts
|
||||
record['zcode'] = code
|
||||
loginf('%s: generated 1 forecast record' % Z_KEY)
|
||||
return record
|
||||
return [record]
|
||||
|
||||
zambretti_label_dict = {
|
||||
'A' : "Settled fine",
|
||||
@@ -1152,7 +1249,7 @@ class NWSForecast(Forecast):
|
||||
def __init__(self, engine, config_dict):
|
||||
super(NWSForecast, self).__init__(engine, config_dict, NWS_KEY,
|
||||
interval=10800)
|
||||
d = config_dict['Forecast'].get(NWS_KEY, {})
|
||||
d = config_dict.get('Forecast', {}).get(NWS_KEY, {})
|
||||
self.url = d.get('url', NWS_DEFAULT_PFM_URL)
|
||||
self.max_tries = int(d.get('max_tries', 3))
|
||||
self.lid = d.get('lid', None)
|
||||
@@ -1176,6 +1273,8 @@ class NWSForecast(Forecast):
|
||||
logerr('%s: no PFM data for %s from %s' %
|
||||
(NWS_KEY, self.foid, self.url))
|
||||
return None
|
||||
if self.save_raw:
|
||||
self.save_raw_forecast(text, basename='nws-raw')
|
||||
matrix = NWSParseForecast(text, self.lid)
|
||||
if matrix is None:
|
||||
logerr('%s: no PFM found for %s in forecast from %s' %
|
||||
@@ -1183,6 +1282,8 @@ class NWSForecast(Forecast):
|
||||
return None
|
||||
logdbg('%s: forecast matrix: %s' % (NWS_KEY, matrix))
|
||||
records = NWSProcessForecast(self.foid, self.lid, matrix)
|
||||
if len(records) == 0 and self.save_failed:
|
||||
self.save_failed_forecast(text, basename='nws-fail')
|
||||
msg = 'got %d forecast records' % len(records)
|
||||
if 'desc' in matrix or 'location' in matrix:
|
||||
msg += ' for %s %s' % (matrix.get('desc',''),
|
||||
@@ -1435,7 +1536,15 @@ def NWSProcessForecast(foid, lid, matrix):
|
||||
#
|
||||
# There are two WU forecasts - daily (forecast10day) and hourly (hourly10day)
|
||||
#
|
||||
# forecast10day
|
||||
# A forecast from WU contains a number of fields whose contents may overlap
|
||||
# with other fields. These include:
|
||||
# condition - not well defined
|
||||
# wx - imported from us nws forecast
|
||||
# fctcode - forecast code
|
||||
# There is overlap between condition, wx, and fctcode. Also, each may contain
|
||||
# any combination of precip, obvis, and sky cover.
|
||||
#
|
||||
# forecast10day ---------------------------------------------------------------
|
||||
#
|
||||
# date
|
||||
# period
|
||||
@@ -1458,7 +1567,7 @@ def NWSProcessForecast(foid, lid, matrix):
|
||||
# maxhumidity
|
||||
# minhumidity
|
||||
#
|
||||
# hourly10day
|
||||
# hourly10day -----------------------------------------------------------------
|
||||
#
|
||||
# fcttime
|
||||
# dewpoint
|
||||
@@ -1571,7 +1680,7 @@ WU_SKY_DICT = {
|
||||
'sunny':'CL',
|
||||
'mostlysunny':'FW',
|
||||
'partlysunny':'SC',
|
||||
'FIXME':'BK',
|
||||
'FIXME':'BK', # FIXME: NWS defines BK, but WU has nothing equivalent
|
||||
'partlycloudy':'B1',
|
||||
'mostlycloudy':'B2',
|
||||
'cloudy':'OV',
|
||||
@@ -1585,7 +1694,7 @@ class WUForecast(Forecast):
|
||||
def __init__(self, engine, config_dict):
|
||||
super(WUForecast, self).__init__(engine, config_dict, WU_KEY,
|
||||
interval=10800)
|
||||
d = config_dict['Forecast'].get(WU_KEY, {})
|
||||
d = config_dict.get('Forecast', {}).get(WU_KEY, {})
|
||||
self.url = d.get('url', WU_DEFAULT_URL)
|
||||
self.max_tries = int(d.get('max_tries', 3))
|
||||
self.api_key = d.get('api_key', None)
|
||||
@@ -1609,7 +1718,8 @@ class WUForecast(Forecast):
|
||||
raise Exception, '\n'.join(errmsg)
|
||||
|
||||
loginf('%s: interval=%s max_age=%s api_key=%s location=%s fc=%s' %
|
||||
(WU_KEY, self.interval, self.max_age, self.api_key,
|
||||
(WU_KEY, self.interval, self.max_age,
|
||||
'X'*(len(self.api_key)-4) + self.api_key[-4:],
|
||||
self.location, self.forecast_type))
|
||||
|
||||
def get_forecast(self, event):
|
||||
@@ -1620,7 +1730,11 @@ class WUForecast(Forecast):
|
||||
logerr('%s: no forecast data for %s from %s' %
|
||||
(WU_KEY, self.location, self.url))
|
||||
return None
|
||||
records = WUParseForecast(text, location=self.location)
|
||||
if self.save_raw:
|
||||
self.save_raw_forecast(text, basename='wu-raw')
|
||||
records,msgs = WUParseForecast(text, location=self.location)
|
||||
if self.save_failed and len(msgs) > 0:
|
||||
self.save_failed_forecast(text, basename='wu-fail', msgs=msgs)
|
||||
loginf('%s: got %d forecast records' % (WU_KEY, len(records)))
|
||||
return records
|
||||
|
||||
@@ -1643,7 +1757,12 @@ def WUDownloadForecast(api_key, location,
|
||||
|
||||
u = '%s/%s/%s/q/%s.json' % (url, api_key, fc_type, location) \
|
||||
if url == WU_DEFAULT_URL else url
|
||||
loginf("%s: downloading forecast from '%s'" % (WU_KEY, u))
|
||||
masked = list(u)
|
||||
idx = u.find(api_key)
|
||||
if idx >= 0:
|
||||
for i in range(len(api_key)-4):
|
||||
masked[idx+i] = 'X'
|
||||
loginf("%s: downloading forecast from '%s'" % (WU_KEY, ''.join(masked)))
|
||||
for count in range(max_tries):
|
||||
try:
|
||||
response = urllib2.urlopen(u)
|
||||
@@ -1660,14 +1779,15 @@ def WUDownloadForecast(api_key, location,
|
||||
def WUParseForecast(text, issued_ts=None, now=None, location=None):
|
||||
obj = json.loads(text)
|
||||
if not 'response' in obj:
|
||||
logerr('%s: unknown format in response' % WU_KEY)
|
||||
return []
|
||||
msg = "%s: no 'response' in json object" % WU_KEY
|
||||
logerr(msg)
|
||||
return [], [msg]
|
||||
response = obj['response']
|
||||
if 'error' in response:
|
||||
logerr('%s: error in response: %s: %s' %
|
||||
(WU_KEY,
|
||||
response['error']['type'], response['error']['description']))
|
||||
return []
|
||||
msg = '%s: error in response: %s: %s' % (
|
||||
WU_KEY,response['error']['type'],response['error']['description'])
|
||||
logerr(msg)
|
||||
return [], [msg]
|
||||
|
||||
if issued_ts is None or now is None:
|
||||
n = int(time.time())
|
||||
@@ -1676,28 +1796,36 @@ def WUParseForecast(text, issued_ts=None, now=None, location=None):
|
||||
if now is None:
|
||||
now = n
|
||||
|
||||
records = []
|
||||
msgs = []
|
||||
if 'hourly_forecast' in obj:
|
||||
records = WUCreateRecordsFromHourly(obj, issued_ts, now,
|
||||
location=location)
|
||||
records,msgs = WUCreateRecordsFromHourly(obj, issued_ts, now,
|
||||
location=location)
|
||||
elif 'forecast' in obj:
|
||||
records = WUCreateRecordsFromDaily(obj, issued_ts, now,
|
||||
location=location)
|
||||
records,msgs = WUCreateRecordsFromDaily(obj, issued_ts, now,
|
||||
location=location)
|
||||
else:
|
||||
records = []
|
||||
return records
|
||||
msg = "%s: cannot find 'hourly_forecast' or 'forecast'" % WU_KEY
|
||||
logerr(msg)
|
||||
msgs.append(msg)
|
||||
return records,msgs
|
||||
|
||||
def sky2clouds(sky):
|
||||
if 0 <= sky <= 5:
|
||||
try:
|
||||
v = int(sky)
|
||||
except ValueError, e:
|
||||
return None
|
||||
if 0 <= v <= 5:
|
||||
return 'CL'
|
||||
elif 5 < sky <= 25:
|
||||
elif 5 < v <= 25:
|
||||
return 'FW'
|
||||
elif 25 < sky <= 50:
|
||||
elif 25 < v <= 50:
|
||||
return 'SC'
|
||||
elif 50 < sky <= 69:
|
||||
elif 50 < v <= 69:
|
||||
return 'B1'
|
||||
elif 69 < sky <= 87:
|
||||
elif 69 < v <= 87:
|
||||
return 'B2'
|
||||
elif 87 < sky <= 100:
|
||||
elif 87 < v <= 100:
|
||||
return 'OV'
|
||||
return None
|
||||
|
||||
@@ -1879,9 +2007,12 @@ def str2float(n, s):
|
||||
|
||||
def WUCreateRecordsFromHourly(fc, issued_ts, now, location=None):
|
||||
'''create from hourly10day'''
|
||||
msgs = []
|
||||
records = []
|
||||
cnt = 0
|
||||
for period in fc['hourly_forecast']:
|
||||
try:
|
||||
cnt += 1
|
||||
r = {}
|
||||
r['method'] = WU_KEY
|
||||
r['usUnits'] = weewx.US
|
||||
@@ -1890,7 +2021,7 @@ def WUCreateRecordsFromHourly(fc, issued_ts, now, location=None):
|
||||
r['event_ts'] = str2int('epoch', period['FCTTIME']['epoch'])
|
||||
r['hour'] = str2int('hour', period['FCTTIME']['hour'])
|
||||
r['duration'] = 3600
|
||||
r['clouds'] = sky2clouds(int(period['sky']))
|
||||
r['clouds'] = sky2clouds(period['sky'])
|
||||
r['temp'] = str2float('temp', period['temp']['english'])
|
||||
r['dewpoint'] = str2float('dewpoint',period['dewpoint']['english'])
|
||||
r['humidity'] = str2int('humidity', period['humidity'])
|
||||
@@ -1907,14 +2038,20 @@ def WUCreateRecordsFromHourly(fc, issued_ts, now, location=None):
|
||||
r['location'] = location
|
||||
records.append(r)
|
||||
except Exception, e:
|
||||
logerr('%s: failure in hourly forecast: %s' % (WU_KEY, e))
|
||||
return records
|
||||
msg = '%s: failure in hourly forecast period %d: %s' % (
|
||||
WU_KEY, cnt, e)
|
||||
msgs.append(msg)
|
||||
logerr(msg)
|
||||
return records, msgs
|
||||
|
||||
def WUCreateRecordsFromDaily(fc, issued_ts, now, location=None):
|
||||
'''create from forecast10day data'''
|
||||
msgs = []
|
||||
records = []
|
||||
cnt = 0
|
||||
for period in fc['forecast']['simpleforecast']['forecastday']:
|
||||
try:
|
||||
cnt += 1
|
||||
r = {}
|
||||
r['method'] = WU_KEY
|
||||
r['usUnits'] = weewx.US
|
||||
@@ -1939,8 +2076,11 @@ def WUCreateRecordsFromDaily(fc, issued_ts, now, location=None):
|
||||
r['location'] = location
|
||||
records.append(r)
|
||||
except Exception, e:
|
||||
logerr('%s: failure in daily forecast: %s' % (WU_KEY, e))
|
||||
return records
|
||||
msg = '%s: failure in daily forecast period %d: %s' % (
|
||||
WU_KEY, cnt, e)
|
||||
msgs.append(msg)
|
||||
logerr(msg)
|
||||
return records, msgs
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -1966,7 +2106,7 @@ class XTideForecast(Forecast):
|
||||
def __init__(self, engine, config_dict):
|
||||
super(XTideForecast, self).__init__(engine, config_dict, XT_KEY,
|
||||
interval=1209600, max_age=2419200)
|
||||
d = config_dict['Forecast'].get(XT_KEY, {})
|
||||
d = config_dict.get('Forecast', {}).get(XT_KEY, {})
|
||||
self.tideprog = d.get('prog', XT_PROG)
|
||||
self.tideargs = d.get('args', XT_ARGS)
|
||||
self.location = d['location']
|
||||
@@ -2282,7 +2422,9 @@ class ForecastVariables(SearchList):
|
||||
'''
|
||||
fd = generator.config_dict.get('Forecast', {})
|
||||
sd = generator.skin_dict.get('Forecast', {})
|
||||
db = generator._getArchive(fd['database'])
|
||||
|
||||
self.database = generator._getArchive(fd['database'])
|
||||
self.table = fd.get('table','archive')
|
||||
|
||||
self.latitude = generator.stn_info.latitude_f
|
||||
self.longitude = generator.stn_info.longitude_f
|
||||
@@ -2298,8 +2440,8 @@ class ForecastVariables(SearchList):
|
||||
self.labels['Weather'] = dict(weather_label_dict.items() + label_dict.get('Weather', {}).items())
|
||||
self.labels['Zambretti'] = dict(zambretti_label_dict.items() + label_dict.get('Zambretti', {}).items())
|
||||
|
||||
self.database = db
|
||||
self.table = fd.get('table','archive')
|
||||
self.db_max_tries = 3
|
||||
self.db_retry_wait = 5 # seconds
|
||||
|
||||
def get_extension(self, timespan, archivedb, statsdb):
|
||||
return {'forecast': self}
|
||||
@@ -2308,18 +2450,31 @@ class ForecastVariables(SearchList):
|
||||
sql = "select dateTime,issued_ts,event_ts,hilo,offset,usUnits,location from %s where method = 'XTide' and dateTime = (select dateTime from %s where method = 'XTide' order by dateTime desc limit 1) and event_ts >= %d order by dateTime asc" % (self.table, self.table, from_ts)
|
||||
if max_events is not None:
|
||||
sql += ' limit %d' % max_events
|
||||
records = []
|
||||
for rec in self.database.genSql(sql):
|
||||
r = {}
|
||||
r['dateTime'] = self._create_value(context, rec[0], 'group_time')
|
||||
r['issued_ts'] = self._create_value(context, rec[1], 'group_time')
|
||||
r['event_ts'] = self._create_value(context, rec[2], 'group_time')
|
||||
r['hilo'] = rec[3]
|
||||
r['offset'] = self._create_value(context, rec[4], 'group_altitude',
|
||||
unit_system=rec[5])
|
||||
r['location'] = rec[6]
|
||||
records.append(r)
|
||||
return records
|
||||
for count in range(self.db_max_tries):
|
||||
try:
|
||||
records = []
|
||||
for rec in self.database.genSql(sql):
|
||||
r = {}
|
||||
r['dateTime'] = self._create_value(context,
|
||||
rec[0], 'group_time')
|
||||
r['issued_ts'] = self._create_value(context,
|
||||
rec[1], 'group_time')
|
||||
r['event_ts'] = self._create_value(context,
|
||||
rec[2], 'group_time')
|
||||
r['hilo'] = rec[3]
|
||||
r['offset'] = self._create_value(context,
|
||||
rec[4], 'group_altitude',
|
||||
unit_system=rec[5])
|
||||
r['location'] = rec[6]
|
||||
records.append(r)
|
||||
return records
|
||||
except Exception, e:
|
||||
logerr('get tides failed (attempt %d of %d): %s' %
|
||||
((count+1), self.db_max_tries, e))
|
||||
logdbg('waiting %d seconds before retry' %
|
||||
self.db_retry_wait)
|
||||
time.sleep(self.db_retry_wait)
|
||||
return []
|
||||
|
||||
def _getRecords(self, fid, from_ts, to_ts, max_events=1):
|
||||
'''get the latest requested forecast of indicated type for the
|
||||
@@ -2329,14 +2484,23 @@ class ForecastVariables(SearchList):
|
||||
sql = "select * from %s where method = '%s' and event_ts >= %d and event_ts <= %d and dateTime = (select dateTime from %s where method = '%s' order by dateTime desc limit 1) order by event_ts asc" % (self.table, fid, from_ts, to_ts, self.table, fid)
|
||||
if max_events is not None:
|
||||
sql += ' limit %d' % max_events
|
||||
records = []
|
||||
columns = self.database.connection.columnsOf(self.table)
|
||||
for rec in self.database.genSql(sql):
|
||||
r = {}
|
||||
for i,f in enumerate(columns):
|
||||
r[f] = rec[i]
|
||||
records.append(r)
|
||||
return records
|
||||
for count in range(self.db_max_tries):
|
||||
try:
|
||||
records = []
|
||||
columns = self.database.connection.columnsOf(self.table)
|
||||
for rec in self.database.genSql(sql):
|
||||
r = {}
|
||||
for i,f in enumerate(columns):
|
||||
r[f] = rec[i]
|
||||
records.append(r)
|
||||
return records
|
||||
except Exception, e:
|
||||
logerr('get %s failed (attempt %d of %d): %s' %
|
||||
(fid, (count+1), self.db_max_tries, e))
|
||||
logdbg('waiting %d seconds before retry' %
|
||||
self.db_retry_wait)
|
||||
time.sleep(self.db_retry_wait)
|
||||
return []
|
||||
|
||||
def _create_value(self, context, value_str, group,
|
||||
units=None, unit_system=weewx.US):
|
||||
@@ -2390,15 +2554,24 @@ class ForecastVariables(SearchList):
|
||||
and is good for about 6 hours. So there is no difference between the
|
||||
created timestamp and event timestamp.'''
|
||||
sql = "select dateTime,zcode from %s where method = 'Zambretti' order by dateTime desc limit 1" % self.table
|
||||
record = self.database.getSql(sql)
|
||||
if record is None:
|
||||
return { 'dateTime' : '', 'issued_ts' : '', 'event_ts' : '',
|
||||
'code' : '', 'text' : '' }
|
||||
th = self._create_value('zambretti', record[0], 'group_time')
|
||||
code = record[1]
|
||||
text = self.labels['Zambretti'].get(code, code)
|
||||
return { 'dateTime' : th, 'issued_ts' : th, 'event_ts' : th,
|
||||
'code' : code, 'text' : text, }
|
||||
for count in range(self.db_max_tries):
|
||||
try:
|
||||
record = self.database.getSql(sql)
|
||||
if record is not None:
|
||||
th = self._create_value('zambretti', record[0], 'group_time')
|
||||
code = record[1]
|
||||
text = self.labels['Zambretti'].get(code, code)
|
||||
return { 'dateTime' : th, 'issued_ts' : th,
|
||||
'event_ts' : th,
|
||||
'code' : code, 'text' : text, }
|
||||
except Exception, e:
|
||||
logerr('get zambretti failed (attempt %d of %d): %s' %
|
||||
((count+1), self.db_max_tries, e))
|
||||
logdbg('waiting %d seconds before retry' %
|
||||
self.db_retry_wait)
|
||||
time.sleep(self.db_retry_wait)
|
||||
return { 'dateTime' : '', 'issued_ts' : '', 'event_ts' : '',
|
||||
'code' : '', 'text' : '' }
|
||||
|
||||
def weather_periods(self, fid, from_ts=None, to_ts=None, max_events=240):
|
||||
'''Returns forecast records for the indicated source from the
|
||||
|
||||
@@ -299,6 +299,7 @@ class FakeData(object):
|
||||
def gen_fake_zambretti_data():
|
||||
ts = int(time.mktime((2013,8,22,12,0,0,0,0,-1)))
|
||||
codes = ['A', 'B', 'C', 'D', 'E', 'F', 'A', 'A', 'A']
|
||||
records = []
|
||||
for code in codes:
|
||||
record = {}
|
||||
record['method'] = 'Zambretti'
|
||||
@@ -308,7 +309,8 @@ class FakeData(object):
|
||||
record['event_ts'] = ts
|
||||
record['zcode'] = code
|
||||
ts += 300
|
||||
yield record
|
||||
records.append(record)
|
||||
return records
|
||||
|
||||
@staticmethod
|
||||
def gen_fake_nws_data():
|
||||
@@ -15162,17 +15164,17 @@ class ForecastTest(unittest.TestCase):
|
||||
# next record gives us a trend
|
||||
event.record = {'barometer': 29.834685721179159, 'usUnits': 1, 'dateTime': 1378143900, 'windDir': 90.0}
|
||||
record = zf.get_forecast(event)
|
||||
self.assertEqual(record, {'event_ts': 1378143900, 'dateTime': 1378143900, 'zcode': 'C', 'issued_ts': 1378143900, 'method': 'Zambretti', 'usUnits': 1})
|
||||
self.assertEqual(record, [{'event_ts': 1378143900, 'dateTime': 1378143900, 'zcode': 'C', 'issued_ts': 1378143900, 'method': 'Zambretti', 'usUnits': 1}])
|
||||
|
||||
# now the pressure goes up slightly
|
||||
event.record = {'barometer': 29.835649151484603, 'usUnits': 1, 'dateTime': 1378144200, 'windDir': 90.0}
|
||||
record = zf.get_forecast(event)
|
||||
self.assertEqual(record, {'event_ts': 1378144200, 'dateTime': 1378144200, 'zcode': 'K', 'issued_ts': 1378144200, 'method': 'Zambretti', 'usUnits': 1})
|
||||
self.assertEqual(record, [{'event_ts': 1378144200, 'dateTime': 1378144200, 'zcode': 'K', 'issued_ts': 1378144200, 'method': 'Zambretti', 'usUnits': 1}])
|
||||
|
||||
# now the pressure drops
|
||||
event.record = {'barometer': 29.0, 'usUnits': 1, 'dateTime': 1378144500, 'windDir': 90.0}
|
||||
record = zf.get_forecast(event)
|
||||
self.assertEqual(record, {'event_ts': 1378144500, 'dateTime': 1378144500, 'zcode': 'L', 'issued_ts': 1378144500, 'method': 'Zambretti', 'usUnits': 1})
|
||||
self.assertEqual(record, [{'event_ts': 1378144500, 'dateTime': 1378144500, 'zcode': 'L', 'issued_ts': 1378144500, 'method': 'Zambretti', 'usUnits': 1}])
|
||||
|
||||
def test_zambretti_units(self):
|
||||
'''ensure that zambretti works with both US and METRIC'''
|
||||
@@ -15193,12 +15195,12 @@ class ForecastTest(unittest.TestCase):
|
||||
# next record gives us a trend
|
||||
event.record = {'barometer': 1010.20245852, 'usUnits': weewx.METRIC, 'dateTime': 1378143900, 'windDir': 90.0}
|
||||
record = zf.get_forecast(event)
|
||||
self.assertEqual(record, {'event_ts': 1378143900, 'dateTime': 1378143900, 'zcode': 'C', 'issued_ts': 1378143900, 'method': 'Zambretti', 'usUnits': 1})
|
||||
self.assertEqual(record, [{'event_ts': 1378143900, 'dateTime': 1378143900, 'zcode': 'C', 'issued_ts': 1378143900, 'method': 'Zambretti', 'usUnits': 1}])
|
||||
|
||||
# now the pressure goes up slightly
|
||||
event.record = {'barometer': 1010.23508027, 'usUnits': weewx.METRIC, 'dateTime': 1378144200, 'windDir': 90.0}
|
||||
record = zf.get_forecast(event)
|
||||
self.assertEqual(record, {'event_ts': 1378144200, 'dateTime': 1378144200, 'zcode': 'K', 'issued_ts': 1378144200, 'method': 'Zambretti', 'usUnits': 1})
|
||||
self.assertEqual(record, [{'event_ts': 1378144200, 'dateTime': 1378144200, 'zcode': 'K', 'issued_ts': 1378144200, 'method': 'Zambretti', 'usUnits': 1}])
|
||||
|
||||
def test_zambretti_bogus_values(self):
|
||||
'''confirm behavior when we get bogus values'''
|
||||
@@ -15230,7 +15232,7 @@ class ForecastTest(unittest.TestCase):
|
||||
event.record['windDir'] = 180
|
||||
event.record['dateTime'] = 1368303780
|
||||
c = f.get_forecast(event)
|
||||
self.assertEqual(c, {'event_ts': 1368303780, 'dateTime': 1368303780, 'zcode': 'C', 'issued_ts': 1368303780, 'method': 'Zambretti', 'usUnits': 1})
|
||||
self.assertEqual(c, [{'event_ts': 1368303780, 'dateTime': 1368303780, 'zcode': 'C', 'issued_ts': 1368303780, 'method': 'Zambretti', 'usUnits': 1}])
|
||||
event.record['barometer'] = 1030
|
||||
event.record['windDir'] = None
|
||||
event.record['dateTime'] = 1368304080
|
||||
@@ -15492,7 +15494,7 @@ SW
|
||||
(WU_API_KEY, WU_LOCATION))
|
||||
fcast = forecast.WUDownloadForecast(WU_API_KEY, WU_LOCATION, url=url)
|
||||
print fcast
|
||||
records = forecast.WUParseForecast(fcast)
|
||||
records,msgs = forecast.WUParseForecast(fcast)
|
||||
print records
|
||||
|
||||
def xtest_wu_forecast_from_file(self):
|
||||
@@ -15503,7 +15505,7 @@ SW
|
||||
for line in f:
|
||||
lines.append(line)
|
||||
f.close()
|
||||
records = forecast.WUParseForecast(''.join(lines))
|
||||
records,msgs = forecast.WUParseForecast(''.join(lines))
|
||||
print records
|
||||
|
||||
def xtest_wu_download_daily(self):
|
||||
@@ -15522,7 +15524,8 @@ SW
|
||||
|
||||
def test_wu_parse_forecast_daily(self):
|
||||
ts = 1377298279
|
||||
records = forecast.WUParseForecast(WU_BOS_DAILY, issued_ts=ts, now=ts)
|
||||
records,msgs = forecast.WUParseForecast(WU_BOS_DAILY,
|
||||
issued_ts=ts, now=ts)
|
||||
self.assertEqual(records[0:2], [
|
||||
{'clouds': 'B2', 'temp': 61.5, 'hour': 23, 'event_ts': 1368673200, 'qpf': 0.10000000000000001, 'windSpeed': 15.0, 'pop': 50, 'dateTime': 1377298279, 'windDir': u'SSW', 'tempMin': 55.0, 'qsf': 0.0, 'windGust': 19.0, 'duration': 86400, 'humidity': 69, 'issued_ts': 1377298279, 'method': 'WU', 'usUnits': 1, 'tempMax': 68.0},
|
||||
{'clouds': 'FW', 'temp': 65.5, 'hour': 23, 'event_ts': 1368759600, 'qpf': 0.0, 'windSpeed': 19.0, 'pop': 10, 'dateTime': 1377298279, 'windDir': 'W', 'tempMin': 54.0, 'qsf': 0.0, 'windGust': 23.0, 'duration': 86400, 'humidity': 42, 'issued_ts': 1377298279, 'method': 'WU', 'usUnits': 1, 'tempMax': 77.0}
|
||||
@@ -15530,7 +15533,8 @@ SW
|
||||
|
||||
def test_wu_parse_forecast_hourly(self):
|
||||
ts = 1378215570
|
||||
records = forecast.WUParseForecast(WU_BOS_HOURLY, issued_ts=ts, now=ts)
|
||||
records,msgs = forecast.WUParseForecast(WU_BOS_HOURLY,
|
||||
issued_ts=ts, now=ts)
|
||||
self.assertEqual(records[0:2], [
|
||||
{'windDir': u'S', 'clouds': 'OV', 'temp': 72.0, 'hour': 22, 'event_ts': 1378173600, 'uvIndex': 0, 'qpf': None, 'pop': 100, 'dateTime': 1378215570, 'dewpoint': 69.0, 'windSpeed': 3.0, 'obvis': None, 'rainshwrs': 'C', 'duration': 3600, 'tstms': 'S', 'humidity': 90, 'issued_ts': 1378215570, 'method': 'WU', 'usUnits': 1, 'qsf': None},
|
||||
{'windDir': u'S', 'clouds': 'OV', 'temp': 72.0, 'hour': 23, 'event_ts': 1378177200, 'uvIndex': 0, 'qpf': 0.040000000000000001, 'pop': 80, 'dateTime': 1378215570, 'dewpoint': 68.0, 'windSpeed': 1.0, 'obvis': 'PF', 'rainshwrs': 'C', 'duration': 3600, 'tstms': 'S', 'humidity': 87, 'issued_ts': 1378215570, 'method': 'WU', 'usUnits': 1, 'qsf': None}
|
||||
@@ -15545,14 +15549,14 @@ SW
|
||||
|
||||
def test_wu_detect_download_errors(self):
|
||||
'''ensure proper behavior when server replies with error'''
|
||||
records = forecast.WUParseForecast(WU_ERROR_NOKEY)
|
||||
records,msgs = forecast.WUParseForecast(WU_ERROR_NOKEY)
|
||||
self.assertEqual(records, [])
|
||||
|
||||
def test_wu_template_periods_daily(self):
|
||||
'''verify the period behavior'''
|
||||
records = forecast.WUParseForecast(WU_TENANTS_HARBOR_DAILY,
|
||||
issued_ts=1378090800,
|
||||
now=1378090800)
|
||||
records,msgs = forecast.WUParseForecast(WU_TENANTS_HARBOR_DAILY,
|
||||
issued_ts=1378090800,
|
||||
now=1378090800)
|
||||
template = create_template(PERIODS_TEMPLATE, 'WU', '1378090800')
|
||||
self.runTemplateTest('test_wu_template_periods_daily',
|
||||
'user.forecast.WUForecast', records, template,
|
||||
@@ -15574,9 +15578,9 @@ SW
|
||||
|
||||
def test_wu_template_summary_daily(self):
|
||||
'''verify the summary behavior'''
|
||||
records = forecast.WUParseForecast(WU_TENANTS_HARBOR_DAILY,
|
||||
issued_ts=1378090800,
|
||||
now=1378090800)
|
||||
records,msgs = forecast.WUParseForecast(WU_TENANTS_HARBOR_DAILY,
|
||||
issued_ts=1378090800,
|
||||
now=1378090800)
|
||||
template = create_template(SUMMARY_TEMPLATE, 'WU', '1378090800')
|
||||
self.runTemplateTest('test_wu_template_summary_daily',
|
||||
'user.forecast.WUForecast', records, template,
|
||||
@@ -15613,9 +15617,9 @@ SSW
|
||||
|
||||
def test_wu_template_summary_daily_metric(self):
|
||||
'''verify the summary behavior'''
|
||||
records = forecast.WUParseForecast(WU_TENANTS_HARBOR_DAILY,
|
||||
issued_ts=1378090800,
|
||||
now=1378090800)
|
||||
records,msgs = forecast.WUParseForecast(WU_TENANTS_HARBOR_DAILY,
|
||||
issued_ts=1378090800,
|
||||
now=1378090800)
|
||||
template = create_template(SUMMARY_TEMPLATE, 'WU', '1378090800')
|
||||
self.runTemplateTest('test_wu_template_summary_daily_metric',
|
||||
'user.forecast.WUForecast', records, template,
|
||||
@@ -15652,9 +15656,9 @@ SSW
|
||||
|
||||
def test_wu_template_summary_periods_daily(self):
|
||||
'''verify the summary behavior using periods'''
|
||||
records = forecast.WUParseForecast(WU_TENANTS_HARBOR_DAILY,
|
||||
issued_ts=1378090800,
|
||||
now=1378090800)
|
||||
records,msgs = forecast.WUParseForecast(WU_TENANTS_HARBOR_DAILY,
|
||||
issued_ts=1378090800,
|
||||
now=1378090800)
|
||||
template = create_template(SUMMARY_PERIODS_TEMPLATE,'WU','1378090800')
|
||||
self.runTemplateTest('test_wu_template_summary_periods_daily',
|
||||
'user.forecast.WUForecast', records, template,
|
||||
@@ -15676,9 +15680,9 @@ forecast for for the day 01-Sep-2013 00:00 as of 01-Sep-2013 23:00
|
||||
|
||||
def test_wu_template_summary_periods_daily_metric(self):
|
||||
'''verify the summary behavior using periods'''
|
||||
records = forecast.WUParseForecast(WU_TENANTS_HARBOR_DAILY,
|
||||
issued_ts=1378090800,
|
||||
now=1378090800)
|
||||
records,msgs = forecast.WUParseForecast(WU_TENANTS_HARBOR_DAILY,
|
||||
issued_ts=1378090800,
|
||||
now=1378090800)
|
||||
template = create_template(SUMMARY_PERIODS_TEMPLATE,'WU','1378090800')
|
||||
self.runTemplateTest('test_wu_template_summary_periods_daily_metric',
|
||||
'user.forecast.WUForecast', records, template,
|
||||
@@ -15700,9 +15704,9 @@ forecast for for the day 01-Sep-2013 00:00 as of 01-Sep-2013 23:00
|
||||
|
||||
def test_wu_template_table_daily(self):
|
||||
'''exercise the period and summary template elements'''
|
||||
records = forecast.WUParseForecast(WU_TENANTS_HARBOR_DAILY,
|
||||
issued_ts=1378090800,
|
||||
now=1378090800)
|
||||
records,msgs = forecast.WUParseForecast(WU_TENANTS_HARBOR_DAILY,
|
||||
issued_ts=1378090800,
|
||||
now=1378090800)
|
||||
template = create_template(TABLE_TEMPLATE, 'WU', '1378090800')
|
||||
self.runTemplateLineTest('test_wu_template_table_daily',
|
||||
'user.forecast.WUForecast', records, template,
|
||||
@@ -15710,9 +15714,9 @@ forecast for for the day 01-Sep-2013 00:00 as of 01-Sep-2013 23:00
|
||||
|
||||
def test_wu_template_periods_hourly(self):
|
||||
'''verify the period behavior for hourly'''
|
||||
records = forecast.WUParseForecast(WU_BOS_HOURLY,
|
||||
issued_ts=1378173600,
|
||||
now=1378173600)
|
||||
records,msgs = forecast.WUParseForecast(WU_BOS_HOURLY,
|
||||
issued_ts=1378173600,
|
||||
now=1378173600)
|
||||
template = create_template(PERIODS_TEMPLATE, 'WU', '1378173600')
|
||||
self.runTemplateTest('test_wu_template_periods_hourly',
|
||||
'user.forecast.WUForecast', records, template,
|
||||
@@ -15744,9 +15748,9 @@ forecast for for the day 01-Sep-2013 00:00 as of 01-Sep-2013 23:00
|
||||
|
||||
def test_wu_template_summary_hourly(self):
|
||||
'''verify the summary behavior for hourly'''
|
||||
records = forecast.WUParseForecast(WU_BOS_HOURLY,
|
||||
issued_ts=1378173600,
|
||||
now=1378173600)
|
||||
records,msgs = forecast.WUParseForecast(WU_BOS_HOURLY,
|
||||
issued_ts=1378173600,
|
||||
now=1378173600)
|
||||
template = create_template(SUMMARY_TEMPLATE, 'WU', '1378173600')
|
||||
self.runTemplateTest('test_wu_template_summary_hourly',
|
||||
'user.forecast.WUForecast', records, template,
|
||||
@@ -15786,9 +15790,9 @@ S
|
||||
|
||||
def test_wu_template_table_hourly(self):
|
||||
'''exercise the period and summary template elements for hourly'''
|
||||
records = forecast.WUParseForecast(WU_BOS_HOURLY,
|
||||
issued_ts=1378173600,
|
||||
now=1378173600)
|
||||
records,msgs = forecast.WUParseForecast(WU_BOS_HOURLY,
|
||||
issued_ts=1378173600,
|
||||
now=1378173600)
|
||||
template = create_template(TABLE_TEMPLATE, 'WU', '1378173600')
|
||||
self.runTemplateLineTest('test_wu_template_table_hourly',
|
||||
'user.forecast.WUForecast', records, template,
|
||||
@@ -15796,22 +15800,24 @@ S
|
||||
|
||||
def test_wu_template_table(self):
|
||||
'''exercise the period and summary template elements'''
|
||||
records = forecast.WUParseForecast(WU_BOS_HOURLY,
|
||||
issued_ts=1378173600,
|
||||
now=1378173600)
|
||||
records,msgs = forecast.WUParseForecast(WU_BOS_HOURLY,
|
||||
issued_ts=1378173600,
|
||||
now=1378173600)
|
||||
template = create_template(TABLE_TEMPLATE, 'WU', '1378173600')
|
||||
template = template.replace('period.event_ts.raw', 'period.event_ts.raw, periods=$periods')
|
||||
template = template.replace('period.event_ts.raw',
|
||||
'period.event_ts.raw, periods=$periods')
|
||||
self.runTemplateLineTest('test_wu_template_table',
|
||||
'user.forecast.WUForecast', records, template,
|
||||
514)
|
||||
|
||||
def test_wu_inorther26(self):
|
||||
'''test forecast for inorther26'''
|
||||
records = forecast.WUParseForecast(WU_INORTHER26_HOURLY,
|
||||
issued_ts=1384053615,
|
||||
now=1384053615)
|
||||
records,msgs = forecast.WUParseForecast(WU_INORTHER26_HOURLY,
|
||||
issued_ts=1384053615,
|
||||
now=1384053615)
|
||||
template = create_template(TABLE_TEMPLATE, 'WU', '1384053615')
|
||||
template = template.replace('period.event_ts.raw', 'period.event_ts.raw, periods=$periods')
|
||||
template = template.replace('period.event_ts.raw',
|
||||
'period.event_ts.raw, periods=$periods')
|
||||
self.runTemplateLineTest('test_wu_inorther26',
|
||||
'user.forecast.WUForecast', records, template,
|
||||
493)
|
||||
@@ -16106,9 +16112,9 @@ $ts $a.sunrise $a.sunset $gmstr
|
||||
config_dict = create_config(tdir, 'user.forecast.ZambrettiForecast')
|
||||
method_id = 'Zambretti'
|
||||
table = 'archive'
|
||||
dbspec = config_dict['Forecast']['database']
|
||||
archive = forecast.Forecast.setup_database(dbspec, table, method_id,
|
||||
config_dict,
|
||||
archive = forecast.Forecast.setup_database('forecast_sqlite',
|
||||
config_dict['Databases']['forecast_sqlite'],
|
||||
table,
|
||||
forecast.defaultForecastSchema)
|
||||
|
||||
# create a zambretti forecaster and simulator with which to test
|
||||
@@ -16170,7 +16176,8 @@ $ts $a.sunrise $a.sunset $gmstr
|
||||
size1 = os.path.getsize(dbfile)
|
||||
|
||||
# there should be one remaining after a prune
|
||||
forecast.Forecast.prune_forecasts(archive, table, method_id, ts, True)
|
||||
forecast.Forecast.prune_forecasts(archive, table, method_id, ts)
|
||||
forecast.Forecast.vacuum_database(archive, method_id)
|
||||
records = forecast.Forecast.get_saved_forecasts(archive, table,
|
||||
method_id)
|
||||
self.assertEqual(len(records), 1)
|
||||
|
||||
@@ -82,6 +82,7 @@ def main():
|
||||
print 'Driver version %s' % weewx.drivers.ws23xx.DRIVER_VERSION
|
||||
altitude_m = weewx.drivers.ws23xx.getaltitudeM(config_dict)
|
||||
station = weewx.drivers.ws23xx.WS23xx(altitude=altitude_m,
|
||||
config_dict=config_dict,
|
||||
**config_dict['WS23xx'])
|
||||
|
||||
# Do what we need to do...
|
||||
@@ -94,7 +95,7 @@ def main():
|
||||
history(station, ts=ts)
|
||||
elif options.settime:
|
||||
setclock(station, prompt)
|
||||
elif options.interval:
|
||||
elif options.interval is not None:
|
||||
setinterval(station, options.interval, prompt)
|
||||
elif options.clear:
|
||||
clearhistory(station, prompt)
|
||||
@@ -116,7 +117,7 @@ def current(station):
|
||||
print packet
|
||||
break
|
||||
|
||||
def history(station, ts=0, count=0):
|
||||
def history(station, ts=None, count=0):
|
||||
"""Display the indicated number of records or records since timestamp"""
|
||||
print "Querying the station for historical records..."
|
||||
for i,r in enumerate(station.genArchiveRecords(since_ts=ts, count=count)):
|
||||
@@ -147,6 +148,7 @@ def setclock(station, prompt):
|
||||
print "Set clock cancelled."
|
||||
|
||||
def setinterval(station, interval, prompt):
|
||||
print "Changing the interval will clear the station memory."
|
||||
v = station.getArchiveInterval()
|
||||
ans = None
|
||||
while ans not in ['y', 'n']:
|
||||
|
||||
@@ -19,6 +19,9 @@ class AbstractStation(object):
|
||||
@property
|
||||
def archive_interval(self):
|
||||
raise NotImplementedError("Property 'archive_interval' not implemented")
|
||||
|
||||
def genStartupRecords(self, last_ts):
|
||||
return self.genArchiveRecords(last_ts)
|
||||
|
||||
def genLoopPackets(self):
|
||||
raise NotImplementedError("Method 'genLoopPackets' not implemented")
|
||||
|
||||
@@ -504,8 +504,10 @@ class Stats(SearchList):
|
||||
as $day.outTemp.max"""
|
||||
|
||||
def get_extension(self, timespan, archivedb, statsdb):
|
||||
heatbase = self.generator.skin_dict['Units']['DegreeDays'].get('heating_base')
|
||||
coolbase = self.generator.skin_dict['Units']['DegreeDays'].get('heating_base')
|
||||
units_dict = self.generator.skin_dict.get('Units', {})
|
||||
dd_dict = units_dict.get('DegreeDays', {})
|
||||
heatbase = dd_dict.get('heating_base', None)
|
||||
coolbase = dd_dict.get('cooling_base', None)
|
||||
heatbase_t = (float(heatbase[0]), heatbase[1], "group_temperature") if heatbase else default_heatbase
|
||||
coolbase_t = (float(coolbase[0]), coolbase[1], "group_temperature") if coolbase else default_coolbase
|
||||
|
||||
|
||||
@@ -220,6 +220,8 @@ import weeutil.weeutil
|
||||
import weewx.abstractstation
|
||||
import weewx.wxformulas
|
||||
|
||||
DRIVER_VERSION = '1.4'
|
||||
|
||||
def loader(config_dict, engine):
|
||||
altitude_m = getaltitudeM(config_dict)
|
||||
station = FineOffsetUSB(altitude=altitude_m,**config_dict['FineOffsetUSB'])
|
||||
@@ -328,10 +330,8 @@ def pywws2weewx(p, ts, pressure_offset, altitude,
|
||||
packet[k] = None
|
||||
|
||||
# track the pointer used to obtain the data
|
||||
if p.has_key('ptr'):
|
||||
packet['ptr'] = int(p['ptr'])
|
||||
if p.has_key('delay'):
|
||||
packet['delay'] = int(p['delay'])
|
||||
packet['ptr'] = int(p['ptr']) if p.has_key('ptr') else None
|
||||
packet['delay'] = int(p['delay']) if p.has_key('delay') else None
|
||||
|
||||
# station status is an integer
|
||||
if packet['status'] is not None:
|
||||
@@ -359,18 +359,21 @@ def pywws2weewx(p, ts, pressure_offset, altitude,
|
||||
packet['altimeter'] = sp2ap(adjp, altitude)
|
||||
|
||||
# calculate the rain increment from the rain total
|
||||
# watch for spurious rain counter decrement. if small decrement then it
|
||||
# is a sensor glitch. if decrement is significant, then it is a counter
|
||||
# wraparound.
|
||||
# watch for spurious rain counter decrement. if decrement is significant
|
||||
# then it is a counter wraparound. a small decrement is either a sensor
|
||||
# glitch or a read from a previous record.
|
||||
total = packet['rain']
|
||||
packet['rainTotal'] = packet['rain']
|
||||
if packet['rain'] is not None and last_rain is not None:
|
||||
if packet['rain'] < last_rain:
|
||||
pstr = '0x%04x' % packet['ptr'] if packet['ptr'] is not None else 'None'
|
||||
if last_rain - packet['rain'] < rain_max * 0.3 * 0.5:
|
||||
loginf('ignoring spurious rain counter decrement: new: %s old: %s' % (packet['rain'], last_rain))
|
||||
loginf('ignoring spurious rain counter decrement (%s): '
|
||||
'new: %s old: %s' % (pstr, packet['rain'], last_rain))
|
||||
packet['rainTotal'] = last_rain
|
||||
else:
|
||||
loginf('rain counter wraparound detected: new: %s old: %s' % (packet['rain'], last_rain))
|
||||
loginf('rain counter wraparound detected (%s): '
|
||||
'new: %s old: %s' % (pstr, packet['rain'], last_rain))
|
||||
total += rain_max * 0.3
|
||||
packet['rain'] = calculate_rain(total, last_rain)
|
||||
|
||||
@@ -381,13 +384,19 @@ def pywws2weewx(p, ts, pressure_offset, altitude,
|
||||
# report rainfall in log to diagnose rain counter issues
|
||||
if weewx.debug:
|
||||
if packet['rain'] is not None and packet['rain'] > 0:
|
||||
logdbg('got rainfall of %.2f cm (new: %.2f old: %.2f)' % (packet['rain'], packet['rainTotal'], last_rain))
|
||||
logdbg('got rainfall of %.2f cm (new: %.2f old: %.2f)' %
|
||||
(packet['rain'], packet['rainTotal'], last_rain))
|
||||
if packet['rainRate'] is not None and packet['rainRate'] > 0:
|
||||
logdbg('calculated rainrate of %.2f cm/hr (%.2f cm in %d seconds)' % (packet['rainRate'], packet['rain'], int(ts - last_rain_ts)))
|
||||
logdbg('calculated rainrate of %.2f cm/hr '
|
||||
'(%.2f cm in %d seconds)' % (packet['rainRate'],
|
||||
packet['rain'],
|
||||
int(ts - last_rain_ts)))
|
||||
|
||||
# if the rain rate is bogus, ignore the rain and rainRate values
|
||||
if packet['rainRate'] is not None and packet['rainRate'] > max_rain_rate:
|
||||
logerr('maximum rain rate exceeded: max: %.2f rate: %.2f cm/hr (%.2f cm in %d s)' % (max_rain_rate, packet['rainRate'], packet['rain'], int(ts - last_rain_ts)))
|
||||
logerr('maximum rain rate exceeded: max: %.2f rate: %.2f cm/hr '
|
||||
'(%.2f cm in %d s)' % (max_rain_rate, packet['rainRate'],
|
||||
packet['rain'], int(ts - last_rain_ts)))
|
||||
packet['rain'] = None
|
||||
packet['rainRate'] = None
|
||||
|
||||
@@ -667,7 +676,7 @@ class FineOffsetUSB(weewx.abstractstation.AbstractStation):
|
||||
[Optional. Default is 15 seconds]
|
||||
|
||||
wait_before_retry: How long to wait after a failure before retrying.
|
||||
[Optional. Default is 5 seconds]
|
||||
[Optional. Default is 30 seconds]
|
||||
|
||||
max_tries: How many times to try before giving up.
|
||||
[Optional. Default is 3]
|
||||
@@ -701,7 +710,7 @@ class FineOffsetUSB(weewx.abstractstation.AbstractStation):
|
||||
self.polling_interval = int(stn_dict.get('polling_interval', 60))
|
||||
self.max_rain_rate = int(stn_dict.get('max_rain_rate', 24))
|
||||
self.timeout = float(stn_dict.get('timeout', 15.0))
|
||||
self.wait_before_retry = float(stn_dict.get('wait_before_retry', 60.0))
|
||||
self.wait_before_retry = float(stn_dict.get('wait_before_retry', 30.0))
|
||||
self.max_tries = int(stn_dict.get('max_tries', 3))
|
||||
self.interface = int(stn_dict.get('interface', 0))
|
||||
self.vendor_id = int(stn_dict.get('vendor_id', '0x1941'), 0)
|
||||
@@ -738,8 +747,16 @@ class FineOffsetUSB(weewx.abstractstation.AbstractStation):
|
||||
self._magic_numbers = ['55aa']
|
||||
self._last_magic = None
|
||||
|
||||
# FIXME: get last_rain_arc and last_rain_ts_arc from database
|
||||
|
||||
loginf('driver version is %s' % DRIVER_VERSION)
|
||||
loginf('polling mode is %s' % self.polling_mode)
|
||||
if self.polling_mode.lower() == PERIODIC_POLLING.lower():
|
||||
loginf('polling interval is %s' % self.polling_interval)
|
||||
loginf('altitude is %s meters' % str(self.altitude))
|
||||
loginf('pressure offset is %s' % str(self.pressure_offset))
|
||||
|
||||
self.openPort()
|
||||
self._setup()
|
||||
|
||||
# Unfortunately there is no provision to obtain the model from the station
|
||||
# itself, so use what is specified from the configuration file.
|
||||
@@ -796,14 +813,6 @@ class FineOffsetUSB(weewx.abstractstation.AbstractStation):
|
||||
return dev
|
||||
return None
|
||||
|
||||
# FIXME: get last_rain_arc and last_rain_ts_arc from database on startup
|
||||
def _setup(self):
|
||||
loginf('polling mode is %s' % self.polling_mode)
|
||||
if self.polling_mode.lower() == PERIODIC_POLLING.lower():
|
||||
loginf('polling interval is %s' % self.polling_interval)
|
||||
loginf('altitude is %s meters' % str(self.altitude))
|
||||
loginf('pressure offset is %s' % str(self.pressure_offset))
|
||||
|
||||
# There is no point in using the station clock since it cannot be trusted and
|
||||
# since we cannot synchronize it with the computer clock.
|
||||
|
||||
@@ -852,6 +861,7 @@ class FineOffsetUSB(weewx.abstractstation.AbstractStation):
|
||||
self._last_rain_arc, self._last_rain_ts_arc,
|
||||
self.max_rain_rate)
|
||||
data['interval'] = r['interval']
|
||||
data['ptr'] = r['ptr']
|
||||
self._last_rain_arc = data['rainTotal']
|
||||
self._last_rain_ts_arc = ts
|
||||
logdbg('returning archive record %s' % ts)
|
||||
@@ -872,6 +882,8 @@ class FineOffsetUSB(weewx.abstractstation.AbstractStation):
|
||||
If we get USB read failures, retry until we get something valid.
|
||||
"""
|
||||
nerr = 0
|
||||
old_ptr = None
|
||||
interval = self._archive_interval_minutes()
|
||||
while True:
|
||||
try:
|
||||
if self.polling_mode.lower() == ADAPTIVE_POLLING.lower():
|
||||
@@ -881,12 +893,20 @@ class FineOffsetUSB(weewx.abstractstation.AbstractStation):
|
||||
yield data
|
||||
elif self.polling_mode.lower() == PERIODIC_POLLING.lower():
|
||||
new_ptr = self.current_pos()
|
||||
if new_ptr < data_start:
|
||||
raise ObservationError('bad pointer: 0x%04x' % new_ptr)
|
||||
block = self.get_raw_data(new_ptr, unbuffered=True)
|
||||
if len(block) != reading_len[self.data_format]:
|
||||
raise ObservationError('wrong block length: expected: %d actual: %d' % (reading_len[self.data_format], len(block)))
|
||||
nerr = 0
|
||||
data = _decode(block, reading_format[self.data_format])
|
||||
delay = data.get('delay', None)
|
||||
if delay is None:
|
||||
raise ObservationError('no delay found in observation')
|
||||
if new_ptr != old_ptr and delay >= interval:
|
||||
raise ObservationError('ignoring suspected bogus data from 0x%04x (delay=%s interval=%s)' % (new_ptr, delay, interval))
|
||||
old_ptr = new_ptr
|
||||
data['ptr'] = new_ptr
|
||||
nerr = 0
|
||||
yield data
|
||||
time.sleep(self.polling_interval)
|
||||
else:
|
||||
|
||||
@@ -138,6 +138,9 @@ claimInterface.
|
||||
# TODO: consider open/close on each read instead of keeping open
|
||||
# TODO: try doing bulkRead instead of interruptRead
|
||||
|
||||
# TODO: sensor data use 32 bytes, but each historical record is 38 bytes. what
|
||||
# do the other 4 bytes represent?
|
||||
|
||||
# FIXME: speed up transfers:
|
||||
# date;PYTHONPATH=bin python bin/weewx/drivers/te923.py --records 0 > b; date
|
||||
# Tue Nov 26 10:37:36 EST 2013
|
||||
@@ -158,7 +161,7 @@ import weeutil
|
||||
import weewx.abstractstation
|
||||
import weewx.wxformulas
|
||||
|
||||
DRIVER_VERSION = '0.6'
|
||||
DRIVER_VERSION = '0.7'
|
||||
DEBUG_READ = 0
|
||||
DEBUG_DECODE = 0
|
||||
DEBUG_PRESSURE = 0
|
||||
@@ -362,19 +365,20 @@ class TE923(weewx.abstractstation.AbstractStation):
|
||||
DEBUG_PRESSURE = int(stn_dict.get('debug_pressure', 0))
|
||||
|
||||
self.altitude = stn_dict['altitude']
|
||||
self.polling_interval = stn_dict.get('polling_interval', 10)
|
||||
self.model = stn_dict.get('model', 'TE923')
|
||||
self.calc_windchill = stn_dict.get('calculate_windchill', False)
|
||||
self.sensor_map = stn_dict.get('sensor_map', DEFAULT_SENSOR_MAP)
|
||||
self.battery_map = stn_dict.get('battery_map', DEFAULT_BATTERY_MAP)
|
||||
self.max_tries = int(stn_dict.get('max_tries', 5))
|
||||
self.retry_wait = int(stn_dict.get('retry_wait', 30))
|
||||
self.polling_interval = int(stn_dict.get('polling_interval', 10))
|
||||
self.calc_windchill = weeutil.weeutil.tobool(stn_dict.get('calculate_windchill', False))
|
||||
self.sensor_map = stn_dict.get('sensor_map', DEFAULT_SENSOR_MAP)
|
||||
self.battery_map = stn_dict.get('battery_map', DEFAULT_BATTERY_MAP)
|
||||
self.memory_size = stn_dict.get('memory_size', 'small')
|
||||
|
||||
vendor_id = int(stn_dict.get('vendor_id', '0x1130'), 0)
|
||||
product_id = int(stn_dict.get('product_id', '0x6801'), 0)
|
||||
device_id = stn_dict.get('device_id', None)
|
||||
|
||||
loginf('driver version is %s' % DRIVER_VERSION)
|
||||
loginf('polling interval is %s' % str(self.polling_interval))
|
||||
loginf('windchill will be %s' %
|
||||
('calculated' if self.calc_windchill else 'read from station'))
|
||||
@@ -582,34 +586,22 @@ def decode_th(buf, i):
|
||||
(i, 0+offset, buf[0+offset], 1+offset, buf[1+offset],
|
||||
2+offset, buf[2+offset]))
|
||||
if bcd2int(buf[0+offset] & 0x0f) > 9:
|
||||
if DEBUG_DECODE > 1:
|
||||
logdbg("TMP%d buffer 0 & 0x0f > 9" % i)
|
||||
# FIXME: wview uses 0x0c || 0x0b instead of 0x06 || 0x0b
|
||||
# FIXME: wview uses 0x0c || 0x0b instead of 0x06 || 0x0b for invalid
|
||||
if buf[0+offset] & 0x0f == 0x06 or buf[0+offset] & 0x0f == 0x0b:
|
||||
if DEBUG_DECODE > 1:
|
||||
logdbg("TMP%d buffer 0 & 0x0f = 0x0c or 0x0b" % i)
|
||||
data[tstate] = STATE_OUT_OF_RANGE
|
||||
else:
|
||||
if DEBUG_DECODE > 1:
|
||||
logdbg("TMP%d other error in buffer 0" % i)
|
||||
data[tstate] = STATE_INVALID
|
||||
if buf[1+offset] & 0x40 != 0x40 and i > 0:
|
||||
if DEBUG_DECODE > 1:
|
||||
logdbg("TMP%d buffer 1 bit 6 set" % i)
|
||||
data[tstate] = STATE_OUT_OF_RANGE
|
||||
# FIXME: what about missing link for temperature?
|
||||
|
||||
if data[tstate] == STATE_OK:
|
||||
data[tlabel] = bcd2int(buf[0+offset]) / 10.0 \
|
||||
+ bcd2int(buf[1+offset] & 0x0f) * 10.0
|
||||
if DEBUG_DECODE > 1:
|
||||
logdbg("TMP%d before is %0.2f" % (i, data[tlabel]))
|
||||
if buf[1+offset] & 0x20 == 0x20:
|
||||
data[tlabel] += 0.05
|
||||
if buf[1+offset] & 0x80 != 0x80:
|
||||
data[tlabel] *= -1
|
||||
if DEBUG_DECODE > 1:
|
||||
logdbg("TMP%d after is %0.2f" % (i, data[tlabel]))
|
||||
else:
|
||||
data[tlabel] = None
|
||||
|
||||
@@ -770,7 +762,7 @@ def decode_windchill(buf):
|
||||
def decode_status(buf):
|
||||
data = {}
|
||||
if DEBUG_DECODE:
|
||||
logdbg("STT BUFF[22]=%02x" % buf[22])
|
||||
logdbg("STT BUF[22]=%02x" % buf[22])
|
||||
if buf[22] & 0x0f == 0x0f:
|
||||
data['storm'] = None
|
||||
data['forecast'] = None
|
||||
@@ -864,7 +856,7 @@ class Station(object):
|
||||
#
|
||||
# FIXME: must be a better way to know when there is no more data
|
||||
def _raw_read(self, addr, timeout=50):
|
||||
reqbuf = [0x05, 0x0AF, 0x00, 0x00, 0x00, 0x00, 0xAF, 0xFE]
|
||||
reqbuf = [0x05, 0xAF, 0x00, 0x00, 0x00, 0x00, 0xAF, 0xFE]
|
||||
reqbuf[4] = addr / 0x10000
|
||||
reqbuf[3] = (addr - (reqbuf[4] * 0x10000)) / 0x100
|
||||
reqbuf[2] = addr - (reqbuf[4] * 0x10000) - (reqbuf[3] * 0x100)
|
||||
@@ -878,25 +870,23 @@ class Station(object):
|
||||
if ret != 8:
|
||||
raise BadRead('Unexpected response to data request: %s != 8' % ret)
|
||||
|
||||
time.sleep(0.1) # te923tool is 0.3
|
||||
start_ts = time.time()
|
||||
rbuf = []
|
||||
time.sleep(0.3)
|
||||
try:
|
||||
buf = self.handle.interruptRead(self.ENDPOINT_IN,
|
||||
self.READ_LENGTH, timeout)
|
||||
while buf:
|
||||
nbytes = buf[0]
|
||||
if DEBUG_READ:
|
||||
msg = 'raw: '
|
||||
msg += ' '.join(["%02x" % buf[x] for x in range(8)])
|
||||
logdbg(msg)
|
||||
if nbytes > 7 or nbytes > len(buf)-1:
|
||||
raise BadRead("Bogus length during read: %d" % nbytes)
|
||||
rbuf.extend(buf[1:1+nbytes])
|
||||
time.sleep(0.15)
|
||||
while time.time() - start_ts < 5:
|
||||
buf = self.handle.interruptRead(self.ENDPOINT_IN,
|
||||
self.READ_LENGTH, timeout)
|
||||
if buf:
|
||||
nbytes = buf[0]
|
||||
if nbytes > 7 or nbytes > len(buf)-1:
|
||||
raise BadRead("Bogus length during read: %d" % nbytes)
|
||||
rbuf.extend(buf[1:1+nbytes])
|
||||
else:
|
||||
break
|
||||
time.sleep(0.009) # te923tool is 0.15
|
||||
except usb.USBError, e:
|
||||
logdbg(e)
|
||||
logdbg('usb error while reading: %s' % e)
|
||||
|
||||
if len(rbuf) < 34:
|
||||
raise BadRead("Not enough bytes: %d < 34" % len(rbuf))
|
||||
@@ -913,18 +903,16 @@ class Station(object):
|
||||
def _read(self, addr, max_tries=100):
|
||||
if DEBUG_READ:
|
||||
logdbg("reading station at address 0x%06x" % addr)
|
||||
cnt = 0
|
||||
while cnt < max_tries:
|
||||
cnt += 1
|
||||
for cnt in range(max_tries):
|
||||
try:
|
||||
buf = self._raw_read(addr)
|
||||
if DEBUG_READ:
|
||||
logdbg("BUF " + ' '.join(["%02x" % x for x in buf]))
|
||||
return buf
|
||||
except BadRead, e:
|
||||
logdbg(e)
|
||||
logdbg("Bad read (attempt %d of %d): %s" % (cnt+1,max_tries,e))
|
||||
else:
|
||||
raise BadRead("No data after %d attempts to read" % cnt)
|
||||
raise BadRead("No data after %d read attempts" % max_tries)
|
||||
|
||||
def gen_blocks(self, count=None):
|
||||
"""generator that returns consecutive blocks of station memory"""
|
||||
@@ -976,7 +964,14 @@ class Station(object):
|
||||
yield addr,record
|
||||
|
||||
def get_record(self, addr=None, now_year=None, now_month=None):
|
||||
"""return a single record from station"""
|
||||
"""return a single record from station and address of the next
|
||||
|
||||
Each historical record is 38 bytes (0x26) long. Records start at
|
||||
memory address 0x101 (257). The index of the latest record is at
|
||||
address 0xff (255), indicating the offset from the starting address.
|
||||
|
||||
On small memory stations, the last 32 bytes of memory are never used.
|
||||
"""
|
||||
if now_year is None or now_month is None:
|
||||
now = int(time.time())
|
||||
tt = time.localtime(now)
|
||||
@@ -985,6 +980,10 @@ class Station(object):
|
||||
|
||||
if addr is None:
|
||||
buf = self._read(0xfb)
|
||||
# start = buf[5]
|
||||
# if start == 0:
|
||||
# start = 0xd0 # FIXME: only for small memory model
|
||||
# addr = 0x101 + (buf[5]-1) * 0x26
|
||||
addr = (buf[3] * 0x100 + buf[5]) * 0x26 + 0x101
|
||||
|
||||
buf = self._read(addr)
|
||||
|
||||
@@ -21,6 +21,10 @@
|
||||
# pylint: disable-msg=W0603
|
||||
# suppress weewx driver methods not implemented
|
||||
# pylint: disable-msg=W0223
|
||||
# suppress weewx driver methods non-conforming name
|
||||
# pylint: disable-msg=C0103
|
||||
# suppress too many lines in module
|
||||
# pylint: disable-msg=C0302
|
||||
"""Classes and functions to interfacing with an Oregon Scientific WMR200 station
|
||||
|
||||
Oregon Scientific
|
||||
@@ -147,6 +151,13 @@ def loader(config_dict, engine):
|
||||
return station
|
||||
|
||||
|
||||
class WMR200ProtocolError(weewx.WeeWxIOError):
|
||||
"""Used to signal a protocol error condition"""
|
||||
def __init__(self, msg):
|
||||
super(WMR200ProtocolError, self).__init__()
|
||||
self._msg = msg
|
||||
|
||||
|
||||
class UsbDevice(object):
|
||||
"""General class to handles all access to device via USB bus."""
|
||||
def __init__(self):
|
||||
@@ -181,10 +192,10 @@ class UsbDevice(object):
|
||||
except usb.USBError, exception:
|
||||
logcrt(('open_device() Unable to open USB interface.'
|
||||
' Reason: %s' % exception))
|
||||
raise weewx.WeeWxIOError(exception)
|
||||
raise weewx.WakeupError(exception)
|
||||
except AttributeError, exception:
|
||||
logcrt('open_device() Device not specified.')
|
||||
raise weewx.WeeWxIOError(exception)
|
||||
raise weewx.WakeupError(exception)
|
||||
|
||||
# Detach any old claimed interfaces
|
||||
try:
|
||||
@@ -195,9 +206,9 @@ class UsbDevice(object):
|
||||
try:
|
||||
self.handle.claimInterface(self.interface)
|
||||
except usb.USBError, exception:
|
||||
logcrt('open_device() Unable to'
|
||||
' claim USB interface. Reason: %s' % exception)
|
||||
raise weewx.WeeWxIOError(exception)
|
||||
logcrt(('open_device() Unable to'
|
||||
' claim USB interface. Reason: %s' % exception))
|
||||
raise weewx.WakeupError(exception)
|
||||
|
||||
def close_device(self):
|
||||
"""Closes a device for access."""
|
||||
@@ -216,9 +227,9 @@ class UsbDevice(object):
|
||||
the first byte that are valid protocol bytes. Only the valid
|
||||
protocol bytes are returned. """
|
||||
if not self.handle:
|
||||
logerr(('read_device() No USB handle'
|
||||
' for usb_device Read'))
|
||||
raise weewx.WeeWxIOError('No USB handle for usb_device Read')
|
||||
msg = 'read_device() No USB handle for usb_device Read'
|
||||
logerr(msg)
|
||||
raise weewx.WeeWxIOError(msg)
|
||||
|
||||
try:
|
||||
report = self.handle.interruptRead(self.in_endpoint,
|
||||
@@ -227,9 +238,9 @@ class UsbDevice(object):
|
||||
|
||||
# I think this value indicates that the buffer has overflowed.
|
||||
if report[0] == 8:
|
||||
log_msg = 'USB read_device overflow error'
|
||||
logerr(log_msg)
|
||||
raise WMR200AccessError(log_msg)
|
||||
msg = 'USB read_device overflow error'
|
||||
logerr(msg)
|
||||
raise weewx.WeeWxIOError(msg)
|
||||
|
||||
self.byte_cnt_rd += len(report)
|
||||
# The first byte is the size of valid data following.
|
||||
@@ -251,14 +262,14 @@ class UsbDevice(object):
|
||||
# have been exhausted. We have to send a heartbeat command
|
||||
# to tell the weather console to start streaming live data
|
||||
# again.
|
||||
log_msg = 'read_device() USB Error Reason:%s' % ex
|
||||
if ex.args[0].find('No data available') == -1:
|
||||
logerr(log_msg)
|
||||
return None
|
||||
msg = 'read_device() USB Error Reason:%s' % ex
|
||||
logerr(msg)
|
||||
raise weewx.WeeWxIOError(msg)
|
||||
else:
|
||||
# No data avail...not an error but probably ok.
|
||||
logdbg('No data received in'
|
||||
' %d seconds' % int(self.timeout_read))
|
||||
logdbg(('No data received in'
|
||||
' %d seconds' % int(self.timeout_read)))
|
||||
return []
|
||||
|
||||
def write_device(self, buf):
|
||||
@@ -268,9 +279,9 @@ class UsbDevice(object):
|
||||
value = 0x00000220
|
||||
|
||||
if not self.handle:
|
||||
log_msg = 'No USB handle for usb_device Write'
|
||||
logerr(log_msg)
|
||||
raise weewx.WeeWxIOError(log_msg)
|
||||
msg = 'No USB handle for usb_device Write'
|
||||
logerr(msg)
|
||||
raise weewx.WeeWxIOError(msg)
|
||||
|
||||
try:
|
||||
if DEBUG_WRITES:
|
||||
@@ -284,32 +295,11 @@ class UsbDevice(object):
|
||||
0x0000000, # index
|
||||
_WMR200_USB_RESET_TIMEOUT) # timeout
|
||||
except usb.USBError, exception:
|
||||
logerr(('write_device() Unable to'
|
||||
' send USB control message'))
|
||||
logerr('**** %s' % exception)
|
||||
msg = ('write_device() Unable to'
|
||||
' send USB control message %d' % exception)
|
||||
logerr(msg)
|
||||
# Convert to a Weewx error:
|
||||
raise weewx.WakeupError(exception)
|
||||
|
||||
|
||||
class WMR200ProtocolError(weewx.WeeWxIOError):
|
||||
"""Used to signal a protocol error condition"""
|
||||
def __init__(self, msg):
|
||||
super(WMR200ProtocolError, self).__init__()
|
||||
self._msg = msg
|
||||
|
||||
|
||||
class WMR200CheckSumError(weewx.WeeWxIOError):
|
||||
"""Used to signal a protocol error condition"""
|
||||
def __init__(self, msg):
|
||||
super(WMR200CheckSumError, self).__init__()
|
||||
self._msg = msg
|
||||
|
||||
|
||||
class WMR200AccessError(weewx.WeeWxIOError):
|
||||
"""Used to signal a USB or device access error condition"""
|
||||
def __init__(self, msg):
|
||||
super(WMR200AccessError, self).__init__()
|
||||
self._msg = msg
|
||||
raise weewx.WeeWxIOError(exception)
|
||||
|
||||
|
||||
class Packet(object):
|
||||
@@ -322,6 +312,7 @@ class Packet(object):
|
||||
pkt_cmd = 0
|
||||
pkt_name = 'AbstractPacket'
|
||||
pkt_len = 0
|
||||
pkt_id = 0
|
||||
def __init__(self, wmr200):
|
||||
"""Initialize base elements of the packet parser."""
|
||||
# Keep reference to the wmr200 for any special considerations
|
||||
@@ -335,6 +326,9 @@ class Packet(object):
|
||||
self._bogus_packet = False
|
||||
# Add the command byte as the first field
|
||||
self.append_data(self.pkt_cmd)
|
||||
# Packet identifier
|
||||
Packet.pkt_id += 1
|
||||
self.pkt_id = Packet.pkt_id
|
||||
|
||||
@staticmethod
|
||||
def host_timestamp():
|
||||
@@ -408,9 +402,9 @@ class Packet(object):
|
||||
return cksum
|
||||
|
||||
except IndexError:
|
||||
str_val = 'Packet too small to compute 16 bit checksum'
|
||||
logerr(str_val)
|
||||
raise WMR200CheckSumError(str_val)
|
||||
msg = 'Packet too small to compute 16 bit checksum'
|
||||
logerr(msg)
|
||||
raise WMR200ProtocolError(msg)
|
||||
|
||||
def _checksum_field(self):
|
||||
"""Returns the checksum field of the current packet.
|
||||
@@ -420,21 +414,35 @@ class Packet(object):
|
||||
try:
|
||||
return (self._pkt_data[-1] << 8) | self._pkt_data[-2]
|
||||
except IndexError:
|
||||
str_val = 'Packet too small to contain 16 bit checksum'
|
||||
logerr(str_val)
|
||||
raise WMR200CheckSumError(str_val)
|
||||
msg = 'Packet too small to contain 16 bit checksum'
|
||||
logerr(msg)
|
||||
raise WMR200ProtocolError(msg)
|
||||
|
||||
def verify_checksum(self):
|
||||
"""Verifies packet for checksum correctness.
|
||||
|
||||
Raises exception upon checksum failure as this is a catastrophic
|
||||
event."""
|
||||
if self._checksum_calculate() != self._checksum_field():
|
||||
str_val = ('Checksum error act:%x exp:%x'
|
||||
Raises exception upon checksum failure unless configured to drop."""
|
||||
if not self._bogus_packet \
|
||||
and self._checksum_calculate() != self._checksum_field():
|
||||
msg = ('Checksum error act:%x exp:%x'
|
||||
% (self._checksum_calculate(), self._checksum_field()))
|
||||
logerr(str_val)
|
||||
logerr(msg)
|
||||
logerr(self.to_string_raw(' packet:'))
|
||||
raise WMR200CheckSumError(str_val)
|
||||
if self.wmr200.ignore_checksum:
|
||||
logerr('Dropping packet')
|
||||
self._bogus_packet = True
|
||||
return
|
||||
|
||||
raise weewx.CRCError(msg)
|
||||
|
||||
def record_timestamp(self):
|
||||
"""Returns the epoch timestamp in the record."""
|
||||
try:
|
||||
return self._record['dateTime']
|
||||
except (KeyError, NameError):
|
||||
msg = 'record_timestamp() Timestamp not set in record'
|
||||
logerr(msg)
|
||||
raise weewx.ViolatedPrecondition(msg)
|
||||
|
||||
def packet_timestamp(self):
|
||||
"""Pulls the timestamp from the packet.
|
||||
@@ -459,19 +467,23 @@ class Packet(object):
|
||||
logerr(log_msg)
|
||||
raise WMR200ProtocolError(log_msg)
|
||||
|
||||
def time_drift(self):
|
||||
"""Returns the difference between PC time and the packet timestamp.
|
||||
This value is approximate as all timestamps from a given archive
|
||||
interval will be the same while PC time marches onwards."""
|
||||
time_drift = self.host_timestamp() - self.packet_timestamp()
|
||||
loginf('Time drift in seconds between host and console:%d' %
|
||||
time_drift)
|
||||
return time_drift
|
||||
|
||||
def timestamp(self):
|
||||
"""Returns either that timestamp or the PC time based upon
|
||||
configuration. Caches the last timestamp to add to packets that do
|
||||
not provide timestamps."""
|
||||
# Calculate the drift between pc time and the console time.
|
||||
# Only done first time through.
|
||||
if self.wmr200.time_delta is None:
|
||||
# This value is approximate as all timestamps from a given archive
|
||||
# interval will be the same while host time marches onwards.
|
||||
self.wmr200.time_delta = self.host_timestamp() - \
|
||||
self.packet_timestamp()
|
||||
loginf('Time drift in seconds between host and console:%d' %
|
||||
self.wmr200.time_delta)
|
||||
if self.wmr200.time_drift is None:
|
||||
self.wmr200.time_drift = self.time_drift()
|
||||
|
||||
if self.wmr200.use_pc_time:
|
||||
self.wmr200.last_time_epoch = self.host_timestamp()
|
||||
@@ -490,14 +502,20 @@ class Packet(object):
|
||||
"""Debug method method to print the processed packet.
|
||||
|
||||
Must be called after the Process() method."""
|
||||
out = ' Packet cooked: '
|
||||
out += '%s ' % self.pkt_name
|
||||
out += '%s ' % weeutil.weeutil.timestamp_to_string\
|
||||
(self.timestamp())
|
||||
out += 'len:%d ' % self.size_actual()
|
||||
out += 'fields:%d ' % len(self._record)
|
||||
out += str(self._record)
|
||||
logdbg(out)
|
||||
try:
|
||||
out = ' Packet cooked: '
|
||||
out += 'id:%d ' % self.pkt_id
|
||||
out += '%s ' % self.pkt_name
|
||||
out += '%s ' % weeutil.weeutil.timestamp_to_string\
|
||||
(self.record_timestamp())
|
||||
out += 'len:%d ' % self.size_actual()
|
||||
out += 'fields:%d ' % len(self._record)
|
||||
out += str(self._record)
|
||||
logdbg(out)
|
||||
except (KeyError, NameError):
|
||||
msg = 'print_cooked() called before proper setup'
|
||||
logerr(msg)
|
||||
raise weewx.ViolatedPrecondition(msg)
|
||||
|
||||
class PacketLive(Packet):
|
||||
"""Packets with live sensor data from console."""
|
||||
@@ -543,7 +561,12 @@ class PacketArchive(Packet):
|
||||
"""Returns a records field to be processed by the weewx engine."""
|
||||
super(PacketArchive, self).packet_process()
|
||||
self._record.update({'dateTime' : self.packet_timestamp(), })
|
||||
self._record.update({'interval' : \
|
||||
int(self.wmr200.archive_interval / 60), })
|
||||
|
||||
def timestamp_adjust(self, delta):
|
||||
"""Archive records may need time adjustment when using PC time."""
|
||||
self._record['dateTime'] += int(delta)
|
||||
|
||||
class PacketControl(Packet):
|
||||
"""Packets with protocol control info from console."""
|
||||
@@ -593,9 +616,9 @@ class PacketControl(Packet):
|
||||
|
||||
|
||||
class PacketHistoryReady(PacketControl):
|
||||
"""Packet parser for archived data is ready to receive."""
|
||||
"""Packet parser for control command acknowledge."""
|
||||
pkt_cmd = 0xd1
|
||||
pkt_name = 'Archive Avail'
|
||||
pkt_name = 'CmdAck'
|
||||
pkt_len = 1
|
||||
def __init__(self, wmr200):
|
||||
super(PacketHistoryReady, self).__init__(wmr200)
|
||||
@@ -625,24 +648,31 @@ class PacketHistoryData(PacketArchive):
|
||||
def packet_process(self):
|
||||
"""Returns a records field to be processed by the weewx engine."""
|
||||
super(PacketHistoryData, self).packet_process()
|
||||
try:
|
||||
self._record.update(decode_rain(self, self._pkt_data[ 7:20]))
|
||||
self._record.update(decode_wind(self, self._pkt_data[20:27]))
|
||||
self._record.update(decode_uvi(self, self._pkt_data[27:28]))
|
||||
self._record.update(decode_pressure(self, self._pkt_data[28:32]))
|
||||
# Number of sensors starting at zero inclusive.
|
||||
num_sensors = self._pkt_data[32]
|
||||
if DEBUG_PACKETS_ARCHIVE:
|
||||
loginf('Detected temp sensors:%d' % num_sensors)
|
||||
|
||||
self._record.update(decode_rain(self, self._pkt_data[ 7:20]))
|
||||
self._record.update(decode_wind(self, self._pkt_data[20:27]))
|
||||
self._record.update(decode_uvi(self, self._pkt_data[27:28]))
|
||||
self._record.update(decode_pressure(self, self._pkt_data[28:32]))
|
||||
num_sensors = self._pkt_data[32]
|
||||
if DEBUG_PACKETS_ARCHIVE:
|
||||
loginf('Detected temp sensors:%d' % num_sensors)
|
||||
for i in xrange(0, num_sensors+1):
|
||||
base = 33 + i*7
|
||||
self._record.update(decode_temp(self,
|
||||
self._pkt_data[base:base+7]))
|
||||
|
||||
for i in xrange(0, num_sensors):
|
||||
base = 33 + i*7
|
||||
self._record.update(decode_temp(self, self._pkt_data[base:base+7]))
|
||||
# Tell wmr200 console we have processed it and can handle more.
|
||||
self.wmr200.request_archive_data()
|
||||
|
||||
# Tell wmr200 console we have processed it and can handle more.
|
||||
self.wmr200.request_archive_data()
|
||||
if DEBUG_PACKETS_ARCHIVE:
|
||||
loginf(' Archive packet')
|
||||
|
||||
if DEBUG_PACKETS_ARCHIVE:
|
||||
loginf(' Archive packet')
|
||||
except IndexError:
|
||||
msg = ('%s decode index failure' % self.pkt_name)
|
||||
logerr(msg)
|
||||
raise WMR200ProtocolError(msg)
|
||||
|
||||
|
||||
def decode_wind(pkt, pkt_data):
|
||||
@@ -660,25 +690,28 @@ def decode_wind(pkt, pkt_data):
|
||||
avg_speed = ((pkt_data[3] >> 4)
|
||||
| ((pkt_data[4] << 4))) / 10.0
|
||||
# Windchill temperature. The value is in degrees F.
|
||||
if pkt_data[5] != 0:
|
||||
windchill = (pkt_data[5] - 32.0) * (5.0 / 9.0)
|
||||
# Set default to no windchill as it may not exist.
|
||||
# Convert to metric for weewx presentation.
|
||||
windchill = None
|
||||
if pkt_data[6] != 0x20:
|
||||
if pkt_data[6] != 0x80:
|
||||
windchill = (((pkt_data[6] << 8) | pkt_data[5]) - 320) \
|
||||
* (5.0 / 90.0)
|
||||
elif pkt_data[6] & 0x80:
|
||||
windchill = ((((pkt_data[5])*-1) -320) * (5.0/90.0))
|
||||
|
||||
if pkt_data[5] != 0 or pkt_data[6] != 0x20:
|
||||
windchill = (((pkt_data[6] << 8) | pkt_data[5]) - 320) \
|
||||
* (5.0 / 90.0)
|
||||
else:
|
||||
windchill = None
|
||||
# The console returns wind speeds in m/s. Our metric system requires
|
||||
# kph, so the result needs to be multiplied by 3.6.
|
||||
# The console returns wind speeds in m/s. weewx requires
|
||||
# kph, so the speeds needs to be converted.
|
||||
record = {'windSpeed' : avg_speed * 3.60,
|
||||
'windGust' : gust_speed * 3.60,
|
||||
'windDir' : dir_deg,
|
||||
'usUnits' : weewx.METRIC,
|
||||
'windchill' : windchill,
|
||||
}
|
||||
# Sometimes the station emits a wind gust that is less than the
|
||||
# average wind. Ignore it if this is the case.
|
||||
if gust_speed >= record['windSpeed']:
|
||||
record['windGust'] = gust_speed * 3.60
|
||||
# average wind. weewx requires kph, so the result needs to be
|
||||
# converted.
|
||||
if gust_speed < avg_speed:
|
||||
record['windGust'] = avg_speed * 3.60
|
||||
|
||||
if DEBUG_PACKETS_WIND:
|
||||
loginf(' Wind Dir: %s' % (WIND_DIR_MAP[pkt_data[0] & 0x0f]))
|
||||
@@ -688,9 +721,9 @@ def decode_wind(pkt, pkt_data):
|
||||
return record
|
||||
|
||||
except IndexError:
|
||||
str_val = ('%s decode index failure' % pkt.pkt_name())
|
||||
logerr(str_val)
|
||||
raise WMR200ProtocolError(str_val)
|
||||
msg = ('%s decode index failure' % pkt.pkt_name())
|
||||
logerr(msg)
|
||||
raise WMR200ProtocolError(msg)
|
||||
|
||||
class PacketWind(PacketLive):
|
||||
"""Packet parser for wind."""
|
||||
@@ -713,23 +746,21 @@ def decode_rain(pkt, pkt_data):
|
||||
"""Decode the rain portion of a wmr200 packet."""
|
||||
try:
|
||||
# Bytes 0 and 1: high and low byte of the current rainfall rate
|
||||
# in 0.1 in/h
|
||||
rain_rate = ((pkt_data[1] << 8) | pkt_data[0]) / 100.0
|
||||
# Bytes 2 and 3: high and low byte of the last hour rainfall in 0.1in
|
||||
rain_hour = ((pkt_data[3] << 8) | pkt_data[2]) / 100.0
|
||||
# Bytes 4 and 5: high and low byte of the last day rainfall in 0.1in
|
||||
rain_day = ((pkt_data[5] << 8) | pkt_data[4]) / 100.0
|
||||
# Bytes 6 and 7: high and low byte of the total rainfall in 0.1in
|
||||
rain_total = ((pkt_data[7] << 8) | pkt_data[6]) / 100.0
|
||||
# NB: in my experiments with the WMR100, it registers in increments of
|
||||
# 0.04 inches. Per Ejeklint's notes have you divide the packet values by
|
||||
# 10, but this would result in an 0.4 inch bucket --- too big. So, I'm
|
||||
# dividing by 100.
|
||||
# in 0.01 in/h. Convert into metric.
|
||||
rain_rate = (((pkt_data[1] & 0x0f) << 8) | pkt_data[0]) / 100.0 * 2.54
|
||||
# Bytes 2 and 3: high and low byte of the last hour rainfall in 0.01in
|
||||
# Convert into metric.
|
||||
rain_hour = ((pkt_data[3] << 8) | pkt_data[2]) / 100.0 * 2.54
|
||||
# Bytes 4 and 5: high and low byte of the last day rainfall in 0.01in
|
||||
# Convert into metric.
|
||||
rain_day = ((pkt_data[5] << 8) | pkt_data[4]) / 100.0 * 2.54
|
||||
# Bytes 6 and 7: high and low byte of the total rainfall in 0.01in
|
||||
# Convert into metric.
|
||||
rain_total = ((pkt_data[7] << 8) | pkt_data[6]) / 100.0 * 2.54
|
||||
record = {'rainRate' : rain_rate,
|
||||
'hourRain' : rain_hour,
|
||||
'dayRain' : rain_day,
|
||||
'totalRain' : rain_total,
|
||||
'usUnits' : weewx.US}
|
||||
'totalRain' : rain_total}
|
||||
if DEBUG_PACKETS_RAIN:
|
||||
loginf(" Rain rate:%.02f hour_rain:%.02f day_rain:%.02f" %
|
||||
(rain_rate, rain_hour, rain_day))
|
||||
@@ -737,9 +768,9 @@ def decode_rain(pkt, pkt_data):
|
||||
return record
|
||||
|
||||
except IndexError:
|
||||
str_val = ('%s decode index failure' % pkt.pkt_name())
|
||||
logerr(str_val)
|
||||
raise WMR200ProtocolError(str_val)
|
||||
msg = ('%s decode index failure' % pkt.pkt_name())
|
||||
logerr(msg)
|
||||
raise WMR200ProtocolError(msg)
|
||||
|
||||
|
||||
class PacketRain(PacketLive):
|
||||
@@ -783,9 +814,9 @@ def decode_uvi(pkt, pkt_data):
|
||||
return record
|
||||
|
||||
except IndexError:
|
||||
str_val = ('%s index decode index failure' % pkt.pkt_name())
|
||||
logerr(str_val)
|
||||
raise WMR200ProtocolError(str_val)
|
||||
msg = ('%s index decode index failure' % pkt.pkt_name())
|
||||
logerr(msg)
|
||||
raise WMR200ProtocolError(msg)
|
||||
|
||||
|
||||
class PacketUvi(PacketLive):
|
||||
@@ -840,9 +871,9 @@ def decode_pressure(pkt, pkt_data):
|
||||
return record
|
||||
|
||||
except IndexError:
|
||||
str_val = ('%s index decode index failure' % pkt.pkt_name())
|
||||
logerr(str_val)
|
||||
raise WMR200ProtocolError(str_val)
|
||||
msg = ('%s index decode index failure' % pkt.pkt_name())
|
||||
logerr(msg)
|
||||
raise WMR200ProtocolError(msg)
|
||||
|
||||
|
||||
class PacketPressure(PacketLive):
|
||||
@@ -869,7 +900,13 @@ def decode_temp(pkt, pkt_data):
|
||||
# sensors.
|
||||
# Byte 0: low nibble contains sensor ID. 0 for base station.
|
||||
sensor_id = pkt_data[0] & 0x0f
|
||||
# '00 Temp steady
|
||||
# '01 Temp rising
|
||||
# '10 Temp falling
|
||||
temp_trend = (pkt_data[0] >> 6) & 0x3
|
||||
# '00 Humidity steady
|
||||
# '01 Humidity rising
|
||||
# '10 Humidity falling
|
||||
hum_trend = (pkt_data[0] >> 4) & 0x3
|
||||
|
||||
# The high nible contains the sign indicator.
|
||||
@@ -926,9 +963,9 @@ def decode_temp(pkt, pkt_data):
|
||||
return record
|
||||
|
||||
except IndexError:
|
||||
str_val = ('%s index decode index failure' % pkt.pkt_name())
|
||||
logerr(str_val)
|
||||
raise WMR200ProtocolError(str_val)
|
||||
msg = ('%s index decode index failure' % pkt.pkt_name())
|
||||
logerr(msg)
|
||||
raise WMR200ProtocolError(msg)
|
||||
|
||||
|
||||
class PacketTemperature(PacketLive):
|
||||
@@ -975,21 +1012,20 @@ class PacketStatus(PacketLive):
|
||||
'rxCheckPercent' : 1.0 })
|
||||
|
||||
# This information is sent to syslog
|
||||
if self.wmr200.sensor_stat:
|
||||
if self._pkt_data[2] & 0x2:
|
||||
logwar('Sensor 1 fault (temp/hum outdoor)')
|
||||
if self._pkt_data[2] & 0x2 and self.wmr200.sensor_stat:
|
||||
logwar('Sensor 1 fault (temp/hum outdoor)')
|
||||
|
||||
if self._pkt_data[2] & 0x1:
|
||||
logwar('Wind sensor fault')
|
||||
if self._pkt_data[2] & 0x1 and self.wmr200.sensor_stat:
|
||||
logwar('Wind sensor fault')
|
||||
|
||||
if self._pkt_data[3] & 0x20:
|
||||
logwar('UV Sensor fault')
|
||||
if self._pkt_data[3] & 0x20 and self.wmr200.sensor_stat:
|
||||
logwar('UV Sensor fault')
|
||||
|
||||
if self._pkt_data[3] & 0x10:
|
||||
logwar('Rain sensor fault')
|
||||
if self._pkt_data[3] & 0x10 and self.wmr200.sensor_stat:
|
||||
logwar('Rain sensor fault')
|
||||
|
||||
if self._pkt_data[5] & 0x20:
|
||||
logwar('UV sensor: Battery low')
|
||||
if self._pkt_data[5] & 0x20 and self.wmr200.sensor_stat:
|
||||
logwar('UV sensor: Battery low')
|
||||
|
||||
# This information can be passed up to weewx.
|
||||
if self._pkt_data[4] & 0x02:
|
||||
@@ -1027,8 +1063,12 @@ class PacketFactory(object):
|
||||
self.subclass = dict((s.pkt_cmd, s) for s in subclass_list)
|
||||
self.skipped_bytes = 0
|
||||
|
||||
def num_packets(self):
|
||||
"""Returns the number of packets handled by the factory."""
|
||||
return len(self.subclass)
|
||||
|
||||
def get_packet(self, pkt_cmd, wmr200):
|
||||
"""Returns an instance of packet parser indexed from packet command.
|
||||
"""Returns a protocol packet instance from initial packet command byte.
|
||||
|
||||
Returns None if there was no mapping for the protocol command.
|
||||
|
||||
@@ -1168,10 +1208,14 @@ class PollUsbDevice(threading.Thread):
|
||||
# If we have sent several resets with no data,
|
||||
# give up and abort.
|
||||
if read_reset_cnt == 2:
|
||||
raise weewx.WeeWxIOError(('Device unresponsive after'
|
||||
'multiple resets'))
|
||||
except WMR200ProtocolError:
|
||||
logerr('USB overflow')
|
||||
msg = ('Device unresponsive after multiple resets')
|
||||
logerr(msg)
|
||||
raise weewx.RetriesExceeded(msg)
|
||||
|
||||
except:
|
||||
logerr('USB device read error')
|
||||
raise
|
||||
|
||||
loginf('USB device polling thread exiting')
|
||||
|
||||
def _append_usb_device(self, buf):
|
||||
@@ -1213,11 +1257,11 @@ class PollUsbDevice(threading.Thread):
|
||||
time.sleep(1)
|
||||
|
||||
except usb.USBError, exception:
|
||||
logerr(('reset_console() Unable to send USB control'
|
||||
'message'))
|
||||
logerr('**** %s' % exception)
|
||||
msg = ('reset_console() Unable to send USB control'
|
||||
'message %s' % exception)
|
||||
logerr(msg)
|
||||
# Convert to a Weewx error:
|
||||
raise weewx.WakeupError(exception)
|
||||
raise weewx.WeeWxIOError(exception)
|
||||
|
||||
def notify(self):
|
||||
"""Gates thread to read of the device.
|
||||
@@ -1237,17 +1281,15 @@ class WMR200(weewx.abstractstation.AbstractStation):
|
||||
sensor_status: Print sensor faults or failures to syslog. [Optional]
|
||||
use_pc_time: Use the console timestamp or the Pc. [Optional]
|
||||
erase_archive: Erasae archive upon startup. [Optional]
|
||||
archive_interval: Time in seconds between intervals [Optional]
|
||||
ignore_checksum: Ignore checksum failures and drop packet.
|
||||
|
||||
--- User should not typically change anything below here ---
|
||||
|
||||
vendor_id: The USB vendor ID for the WMR [Optional]
|
||||
Default is 0xfde.
|
||||
product_id: The USB product ID for the WM [Optional]
|
||||
Default is 0xca01.
|
||||
interface: The USB interface [Optional]
|
||||
Default is 0]
|
||||
in_endpoint: The IN USB endpoint used by the WMR [Optional]
|
||||
Default is usb.ENDPOINT_IN + 1]
|
||||
"""
|
||||
super(WMR200, self).__init__()
|
||||
|
||||
@@ -1264,7 +1306,17 @@ class WMR200(weewx.abstractstation.AbstractStation):
|
||||
self._erase_archive = \
|
||||
weeutil.weeutil.tobool(stn_dict.get('erase_archive', False))
|
||||
|
||||
# User configurable options but not recommended
|
||||
# Archive interval in seconds.
|
||||
self._archive_interval = int(stn_dict.get('archive_interval', 60))
|
||||
if self._archive_interval not in [60, 300]:
|
||||
logwar('Unverified archive interval:%d sec'
|
||||
% self._archive_interval)
|
||||
|
||||
# Ignore checksum errors.
|
||||
self._ignore_checksum = \
|
||||
weeutil.weeutil.tobool(stn_dict.get('ignore_checksum', False))
|
||||
|
||||
# Device specific hardware options.
|
||||
vendor_id = int(stn_dict.get('vendor_id', '0x0fde'), 0)
|
||||
product_id = int(stn_dict.get('product_id', '0xca01'), 0)
|
||||
interface = int(stn_dict.get('interface', 0))
|
||||
@@ -1279,10 +1331,10 @@ class WMR200(weewx.abstractstation.AbstractStation):
|
||||
self.pkt = None
|
||||
|
||||
# Setup the generator to get a byte stream from the console.
|
||||
self.genByte = self._generate_bytestream
|
||||
self.gen_byte = self._generate_bytestream
|
||||
|
||||
# Calculate time delta in seconds between host and console.
|
||||
self.time_delta = None
|
||||
self.time_drift = None
|
||||
|
||||
# Create USB accessor to communiate with weather console device.
|
||||
self.usb_device = UsbDevice()
|
||||
@@ -1302,9 +1354,17 @@ class WMR200(weewx.abstractstation.AbstractStation):
|
||||
# data stream.
|
||||
self._rdy_to_poke = True
|
||||
|
||||
# Archived packets.
|
||||
# Archived packet queue.
|
||||
self._pkt_archive = []
|
||||
|
||||
# Interval records. An aggregation of all live packets
|
||||
# for a given interval.
|
||||
self._interval_record = {}
|
||||
self._interval_record_list = []
|
||||
|
||||
# Count of times through generate archive records.
|
||||
self._gen_archive_cnt = 0
|
||||
|
||||
# Create the lock to sync between main thread and watchdog thread.
|
||||
self._poke_lock = threading.Lock()
|
||||
|
||||
@@ -1374,7 +1434,12 @@ class WMR200(weewx.abstractstation.AbstractStation):
|
||||
loginf(' Log sensor faults: %s' % self._sensor_stat)
|
||||
loginf(' Using PC Time: %s' % self._use_pc_time)
|
||||
loginf(' Erase archive data: %s' % self._erase_archive)
|
||||
loginf(' Archive interval: %d' % self._archive_interval)
|
||||
|
||||
@property
|
||||
def hardware_name(self):
|
||||
"""weewx api."""
|
||||
return _WMR200_DRIVER_NAME
|
||||
|
||||
@property
|
||||
def altitude(self):
|
||||
@@ -1391,6 +1456,16 @@ class WMR200(weewx.abstractstation.AbstractStation):
|
||||
"""Flag to use pc time rather than weather console time."""
|
||||
return self._use_pc_time
|
||||
|
||||
@property
|
||||
def archive_interval(self):
|
||||
"""weewx api. Time in seconds between archive intervals."""
|
||||
return self._archive_interval
|
||||
|
||||
@property
|
||||
def ignore_checksum(self):
|
||||
"""Flag to drop rather than fail on checksum errors."""
|
||||
return self._ignore_checksum
|
||||
|
||||
def ready_to_poke(self, val):
|
||||
"""Set info that device is ready to be poked."""
|
||||
self._poke_lock.acquire()
|
||||
@@ -1414,11 +1489,11 @@ class WMR200(weewx.abstractstation.AbstractStation):
|
||||
try:
|
||||
self.usb_device.write_device(buf)
|
||||
except usb.USBError, exception:
|
||||
msg = ('Write_cmd() Unable to send USB cmd:0x%02x control message' %
|
||||
cmd)
|
||||
msg = (('_write_cmd() Unable to send USB cmd:0x%02x control'
|
||||
' message' % cmd))
|
||||
logerr(msg)
|
||||
# Convert to a Weewx error:
|
||||
raise weewx.WakeupError(exception)
|
||||
raise weewx.WeeWxIOError(exception)
|
||||
|
||||
def _poke_console(self):
|
||||
"""Send a heartbeat command to the weather console.
|
||||
@@ -1471,7 +1546,12 @@ class WMR200(weewx.abstractstation.AbstractStation):
|
||||
Otherwise add the byte to the current packet.
|
||||
Each USB packet may stradle a protocol packet so make sure
|
||||
we assign the data appropriately."""
|
||||
for byte in self.genByte():
|
||||
if not self._thread_usb_poll.is_alive():
|
||||
msg = 'USB polling thread unexpectedly terminated'
|
||||
logerr(msg)
|
||||
raise weewx.WeeWxIOError(msg)
|
||||
|
||||
for byte in self.gen_byte():
|
||||
if self.pkt:
|
||||
self.pkt.append_data(byte)
|
||||
else:
|
||||
@@ -1515,30 +1595,33 @@ class WMR200(weewx.abstractstation.AbstractStation):
|
||||
# not generated from this driver. e.g. weewx.service.
|
||||
if self.pkt is not None and self.pkt.packet_complete():
|
||||
if DEBUG_PACKETS_RAW:
|
||||
loginf(self.pkt.to_string_raw(' Packet Raw:'))
|
||||
loginf(self.pkt.to_string_raw(' genLoop() Packet Raw:'))
|
||||
|
||||
# This will raise exception if checksum fails.
|
||||
self.pkt.verify_checksum()
|
||||
|
||||
# Drop any bogus packets.
|
||||
if self.pkt.is_bogus:
|
||||
logerr(self.pkt.to_string_raw('Discarding bogus packet:'))
|
||||
logerr(self.pkt.to_string_raw('genLoop() Discarding'
|
||||
' bogus packet:'))
|
||||
else:
|
||||
# This will raise exception if checksum fails.
|
||||
self.pkt.verify_checksum()
|
||||
self.pkt.packet_process()
|
||||
if DEBUG_PACKETS_COOKED:
|
||||
self.pkt.print_cooked()
|
||||
if self.pkt.packet_live_data():
|
||||
logdbg('Presenting weewx live packet %d' %
|
||||
logdbg('genLoop() Presenting weewx live packet cnt:%d' %
|
||||
PacketLive.pkt_cnt)
|
||||
# Add this loop packet to our interval record.
|
||||
self._interval_record.update(self.pkt.packet_record())
|
||||
yield self.pkt.packet_record()
|
||||
elif self.pkt.packet_archive_data():
|
||||
# Append to archive list for next time weewx engine
|
||||
# requests archived packets.
|
||||
self._pkt_archive.append(self.pkt)
|
||||
logdbg(('Retrieved archive packet rx:%d cnt:%d' %
|
||||
(PacketArchive.pkt_cnt,
|
||||
len(self._pkt_archive))))
|
||||
logdbg(('genLoop() Buffering archive packet len:%d'
|
||||
% len(self._pkt_archive)))
|
||||
else:
|
||||
logdbg('Acknowledged control packet cnt:%d' %
|
||||
logdbg('genLoop() Acknowledged control packet cnt:%d' %
|
||||
PacketControl.pkt_cnt)
|
||||
|
||||
# Reset this packet to get ready for next one
|
||||
@@ -1552,45 +1635,75 @@ class WMR200(weewx.abstractstation.AbstractStation):
|
||||
# Pull data from the weather console.
|
||||
self._poll_for_data()
|
||||
|
||||
def hardware_name(self):
|
||||
"""weewx api."""
|
||||
return _WMR200_DRIVER_NAME
|
||||
|
||||
def XXXgenArchiveRecords(self, since_ts):
|
||||
##def genArchiveRecords(self, since_ts):
|
||||
def genArchiveRecords(self, since_ts):
|
||||
"""A generator function to return archive packets from the wmr200.
|
||||
|
||||
weewx api to return archive records.
|
||||
since_ts: A timestamp. All data since (but not including) this time
|
||||
will be returned.
|
||||
since_ts: A timestamp in database time. All data since but not
|
||||
including this time will be returned.
|
||||
Pass in None for all data
|
||||
|
||||
yields: a sequence of dictionaries containing the data
|
||||
"""
|
||||
yields: a sequence of dictionary records containing the console
|
||||
data."""
|
||||
if since_ts:
|
||||
loginf('genArchiveRecords() Getting archive packets since %s'
|
||||
logdbg('genArchive() Getting archive packets since %s'
|
||||
% weeutil.weeutil.timestamp_to_string(since_ts))
|
||||
else :
|
||||
loginf('genArchiveRecords() Getting all archive packets')
|
||||
logdbg('genArchive() Getting all archive packets')
|
||||
since_ts = 0
|
||||
|
||||
cnt = 0
|
||||
loginf(('genArchiveRecords() archive packets:%d' %
|
||||
len(self._pkt_archive)))
|
||||
while len(self._pkt_archive):
|
||||
pkt = self._pkt_archive.pop(0)
|
||||
pkt.print_cooked()
|
||||
cnt += 1
|
||||
if since_ts and pkt.packet_timestamp() > since_ts:
|
||||
loginf(('genArchiveRecords() yielding archive record:%s' %
|
||||
weeutil.weeutil.timestamp_to_string(
|
||||
pkt.packet_timestamp())))
|
||||
yield pkt.packet_record()
|
||||
else:
|
||||
loginf(('genArchiveRecords() dropping archive records:%s' %
|
||||
weeutil.weeutil.timestamp_to_string(
|
||||
pkt.packet_timestamp())))
|
||||
loginf(('genArchiveRecords() Handled archive record cnt:%d' %
|
||||
cnt))
|
||||
# Create the current archive record
|
||||
if self._interval_record:
|
||||
self._interval_record.update(
|
||||
{'interval' : int(self.archive_interval / 60), })
|
||||
logdbg(('genArchive() Creating current interval record:%s'
|
||||
% self._interval_record))
|
||||
# The timestamp is already either pc time or console time
|
||||
# based upon console configuration.
|
||||
self._interval_record_list.append(self._interval_record)
|
||||
self._interval_record = {}
|
||||
|
||||
if len(self._pkt_archive) and self._gen_archive_cnt:
|
||||
# Present the archive packets received from the genLoop()
|
||||
# processing. If PC time is set, we must have at least one
|
||||
# live packet to calculate timestamps in PC time.
|
||||
loginf(('genArchive() Still receiving archive packets len:%d' %
|
||||
len(self._pkt_archive)))
|
||||
|
||||
while self._pkt_archive:
|
||||
pkt = self._pkt_archive.pop(0)
|
||||
# If we are using PC time we need to adjust the record timestamp
|
||||
# with the PC drift.
|
||||
if self.use_pc_time:
|
||||
logdbg(('genArchive() Using pc time so adjusting archive'
|
||||
' record time by %d' % self.time_drift))
|
||||
pkt.timestamp_adjust(self.time_drift)
|
||||
# The archive packets have all been processed already.
|
||||
if DEBUG_PACKETS_COOKED:
|
||||
pkt.print_cooked()
|
||||
if pkt.record_timestamp() > since_ts:
|
||||
logdbg(('genArchive() Yielding received archive record'
|
||||
' after requested timestamp'))
|
||||
yield pkt.packet_record()
|
||||
else:
|
||||
loginf(('genArchive() Ignoring received archive record'
|
||||
' before requested timestamp'))
|
||||
|
||||
# We have no archived packets since last time though this callback.
|
||||
# We have a count to ensure we've been here at least once before.
|
||||
# Its safe to assume that the archived packets have been drained. Now
|
||||
# we can push all our current-archive packets we've accumulated.
|
||||
else:
|
||||
while self._interval_record_list:
|
||||
interval_record = self._interval_record_list.pop(0)
|
||||
# Now yield the current archive records
|
||||
logdbg(('genArchive() Yielding current interval record:%s'
|
||||
% interval_record))
|
||||
yield interval_record
|
||||
|
||||
# Number of times through this callback. This is done to allow time
|
||||
# after startup for archive packets to come through if any are avail.
|
||||
self._gen_archive_cnt += 1
|
||||
|
||||
def closePort(self):
|
||||
"""Closes the USB port to the device.
|
||||
@@ -1603,7 +1716,7 @@ class WMR200(weewx.abstractstation.AbstractStation):
|
||||
self._poll_device_enable = False
|
||||
# Join with the polling thread.
|
||||
self._thread_usb_poll.join()
|
||||
if self._thread_usb_poll.isAlive():
|
||||
if self._thread_usb_poll.is_alive():
|
||||
logerr('USB polling thread still alive')
|
||||
else:
|
||||
loginf('USB polling thread expired')
|
||||
@@ -1612,7 +1725,7 @@ class WMR200(weewx.abstractstation.AbstractStation):
|
||||
self.sock_wr.send('shutdown')
|
||||
# Join with the watchdog thread.
|
||||
self._thread_watchdog.join()
|
||||
if self._thread_watchdog.isAlive():
|
||||
if self._thread_watchdog.is_alive():
|
||||
logerr('Watchdog thread still alive')
|
||||
else:
|
||||
loginf('Watchdog thread expired')
|
||||
|
||||
@@ -27,9 +27,6 @@
|
||||
#
|
||||
# This immplementation copies directly from Russell Stuart's implementation,
|
||||
# but only the parts required to read from and write to the weather station.
|
||||
#
|
||||
# The wview implementation copies from the Open2300 implementation. It reads
|
||||
# each sensor multiple times to avoid spikes in data.
|
||||
|
||||
"""Classes and functions for interfacing with WS-23xx weather stations.
|
||||
|
||||
@@ -37,7 +34,7 @@ LaCrosse made a number of stations in the 23xx series, including:
|
||||
|
||||
WS-2300, WS-2308, WS-2310, WS-2315, WS-2317, WS-2357
|
||||
|
||||
The stations were also sold as the TFA Dostman and TechnoLine 2350.
|
||||
The stations were also sold as the TFA Matrix and TechnoLine 2350.
|
||||
|
||||
The WWVB receiver is located in the console.
|
||||
|
||||
@@ -49,13 +46,15 @@ To do a factory reset, press and hold PRESSURE and WIND for 5 seconds.
|
||||
A single bucket tip is 0.0204 in (0.518 mm).
|
||||
|
||||
The station has 175 history records. That is just over 7 days of data with
|
||||
the default history recording interval of 60 minutes (59 in the console).
|
||||
the default history recording interval of 60 minutes.
|
||||
|
||||
The station supports both wireless and wired communication between the
|
||||
sensors and a station console. Wired connection updates data every 8 seconds.
|
||||
Wireless connection updates data in 16 to 128 second intervals, depending on
|
||||
wind speed and rain activity.
|
||||
|
||||
The connection type can be one of 0=cable, 3=lost, 15=wireless
|
||||
|
||||
sensor update frequency:
|
||||
|
||||
32 seconds when wind speed > 22.36 mph (wireless)
|
||||
@@ -70,36 +69,174 @@ console update frequency:
|
||||
|
||||
It is possible to increase the rate of wireless updates:
|
||||
|
||||
http://www.wikihow.com/Modify-a-Lacrosse-Ws2300-for-Frequent-Wireless-Updates
|
||||
http://www.wxforum.net/index.php?topic=2196.0
|
||||
|
||||
This implementation polls the station. Use the polling_interval parameter
|
||||
to specify how often to poll for data. If not specified, the polling interval
|
||||
will adapt based on connection type and status.
|
||||
Sensors are connected by unshielded phone cables. RF interference can cause
|
||||
random spikes in data, with one symptom being values of 25.5 m/s or 91.8 km/h
|
||||
for the wind speed. To reduce the number of spikes in data, replace with
|
||||
shielded cables:
|
||||
|
||||
Instruments are connected by unshielded phone cables. To reduce the number of
|
||||
spikes in data, replace with shielded cables.
|
||||
http://www.lavrsen.dk/sources/weather/windmod.htm
|
||||
|
||||
The connection type can be one of 0=cable, 3=lost, f=wireless
|
||||
The station records wind speed and direction, but has no notion of gust.
|
||||
|
||||
The station calculates windchill and dewpoint.
|
||||
|
||||
The station has a serial connection to the computer.
|
||||
|
||||
This driver does not keep the serial port open for long periods. Instead, the
|
||||
driver opens the serial port, reads data, then closes the port.
|
||||
|
||||
This driver polls the station. Use the polling_interval parameter to specify
|
||||
how often to poll for data. If not specified, the polling interval will adapt
|
||||
based on connection type and status.
|
||||
|
||||
USB-Serial Converters
|
||||
|
||||
With a USB-serial converter one can connect the station to a computer with
|
||||
only USB ports, but not every converter will work properly. Perhaps the two
|
||||
most common converters are based on the Prolific and LTDI chipsets. Many
|
||||
people report better luck with the LTDI-based converters. Some converters
|
||||
most common converters are based on the Prolific and FTDI chipsets. Many
|
||||
people report better luck with the FTDI-based converters. Some converters
|
||||
that use the Prolific chipset (PL2303) will work, but not all of them.
|
||||
|
||||
Known to work: ATEN UC-232A
|
||||
|
||||
Discrepancies Between Implementations
|
||||
|
||||
As of December 2013, there are significant differences between the open2300,
|
||||
wview, and ws2300 implementations. Current version numbers are as follows:
|
||||
|
||||
open2300 1.11
|
||||
ws2300 1.8
|
||||
wview 5.20.2
|
||||
|
||||
History Interval
|
||||
|
||||
The factory default is 60 minutes. The value stored in the console is one
|
||||
less than the actual value (in minutes). So for the factory default of 60,
|
||||
the console stores 59. The minimum interval is 1.
|
||||
|
||||
ws2300.py reports the actual value from the console, e.g., 59 when the
|
||||
interval is 60. open2300 reports the interval, e.g., 60 when the interval
|
||||
is 60. wview ignores the interval.
|
||||
|
||||
Detecting Bogus Sensor Values
|
||||
|
||||
wview queries the station 3 times for each sensor then accepts the value only
|
||||
if the three values were close to each other.
|
||||
|
||||
open2300 sleeps 10 seconds if a wind measurement indicates invalid or overflow.
|
||||
|
||||
The ws2300.py implementation includes overflow and validity flags for values
|
||||
from the wind sensors. It does not retry based on invalid or overflow.
|
||||
|
||||
Wind Speed
|
||||
|
||||
There is disagreement about how to calculate wind speed and how to determine
|
||||
whether the wind speed is valid.
|
||||
|
||||
This driver introduces a WindConversion object that uses open2300/wview
|
||||
decoding so that wind speeds match that of open2300/wview. ws2300 1.8
|
||||
incorrectly uses bcd2num instead of bin2num. This bug is fixed in this driver.
|
||||
|
||||
The memory map indicates the following:
|
||||
|
||||
addr smpl description
|
||||
0x527 0 Wind overflow flag: 0 = normal
|
||||
0x528 0 Wind minimum code: 0=min, 1=--.-, 2=OFL
|
||||
0x529 0 Windspeed: binary nibble 0 [m/s * 10]
|
||||
0x52A 0 Windspeed: binary nibble 1 [m/s * 10]
|
||||
0x52B 0 Windspeed: binary nibble 2 [m/s * 10]
|
||||
0x52C 8 Wind Direction = nibble * 22.5 degrees
|
||||
0x52D 8 Wind Direction 1 measurement ago
|
||||
0x52E 9 Wind Direction 2 measurement ago
|
||||
0x52F 8 Wind Direction 3 measurement ago
|
||||
0x530 7 Wind Direction 4 measurement ago
|
||||
0x531 7 Wind Direction 5 measurement ago
|
||||
0x532 0
|
||||
|
||||
wview 5.20.2 implementation (wview apparently copied from open2300):
|
||||
|
||||
read 3 bytes starting at 0x527
|
||||
|
||||
0x527 x[0]
|
||||
0x528 x[1]
|
||||
0x529 x[2]
|
||||
|
||||
if ((x[0] != 0x00) ||
|
||||
((x[1] == 0xff) && (((x[2] & 0xf) == 0) || ((x[2] & 0xf) == 1)))) {
|
||||
fail
|
||||
} else {
|
||||
dir = (x[2] >> 4) * 22.5
|
||||
speed = ((((x[2] & 0xf) << 8) + (x[1])) / 10.0 * 2.23693629)
|
||||
maxdir = dir
|
||||
maxspeed = speed
|
||||
}
|
||||
|
||||
open2300 1.10 implementation:
|
||||
|
||||
read 6 bytes starting at 0x527
|
||||
|
||||
0x527 x[0]
|
||||
0x528 x[1]
|
||||
0x529 x[2]
|
||||
0x52a x[3]
|
||||
0x52b x[4]
|
||||
0x52c x[5]
|
||||
|
||||
if ((x[0] != 0x00) ||
|
||||
((x[1] == 0xff) && (((x[2] & 0xf) == 0) || ((x[2] & 0xf) == 1)))) {
|
||||
sleep 10
|
||||
} else {
|
||||
dir = x[2] >> 4
|
||||
speed = ((((x[2] & 0xf) << 8) + (x[1])) / 10.0)
|
||||
dir0 = (x[2] >> 4) * 22.5
|
||||
dir1 = (x[3] & 0xf) * 22.5
|
||||
dir2 = (x[3] >> 4) * 22.5
|
||||
dir3 = (x[4] & 0xf) * 22.5
|
||||
dir4 = (x[4] >> 4) * 22.5
|
||||
dir5 = (x[5] & 0xf) * 22.5
|
||||
}
|
||||
|
||||
ws2300.py 1.8 implementation:
|
||||
|
||||
read 1 nibble starting at 0x527
|
||||
read 1 nibble starting at 0x528
|
||||
read 4 nibble starting at 0x529
|
||||
read 3 nibble starting at 0x529
|
||||
read 1 nibble starting at 0x52c
|
||||
read 1 nibble starting at 0x52d
|
||||
read 1 nibble starting at 0x52e
|
||||
read 1 nibble starting at 0x52f
|
||||
read 1 nibble starting at 0x530
|
||||
read 1 nibble starting at 0x531
|
||||
|
||||
0x527 overflow
|
||||
0x528 validity
|
||||
0x529 speed[0]
|
||||
0x52a speed[1]
|
||||
0x52b speed[2]
|
||||
0x52c dir[0]
|
||||
|
||||
speed: ((x[2] * 100 + x[1] * 10 + x[0]) % 1000) / 10
|
||||
velocity: (x[2] * 100 + x[1] * 10 + x[0]) / 10
|
||||
|
||||
dir = data[0] * 22.5
|
||||
speed = (bcd2num(data) % 10**3 + 0) / 10**1
|
||||
velocity = (bcd2num(data[:3])/10.0, bin2num(data[3:4]) * 22.5)
|
||||
|
||||
bcd2num([a,b,c]) -> c*100+b*10+a
|
||||
|
||||
"""
|
||||
|
||||
# TODO: use pyserial instead of LinuxSerialPort
|
||||
# TODO: put the __enter__ and __exit__ scaffolding on serial port, not Station
|
||||
# FIXME: unless we can get setTime to work, just ignore the console clock
|
||||
# FIXME: detect bogus wind speed/direction
|
||||
# i see these when the wind instrument is disconnected:
|
||||
# ws 26.399999
|
||||
# wsh 21
|
||||
# w0 135
|
||||
|
||||
import optparse
|
||||
import syslog
|
||||
@@ -116,7 +253,7 @@ import weeutil.weeutil
|
||||
import weewx.abstractstation
|
||||
import weewx.wxformulas
|
||||
|
||||
DRIVER_VERSION = '0.6'
|
||||
DRIVER_VERSION = '0.18'
|
||||
DEFAULT_PORT = '/dev/ttyUSB0'
|
||||
|
||||
def logmsg(dst, msg):
|
||||
@@ -136,7 +273,8 @@ def logerr(msg):
|
||||
|
||||
def loader(config_dict, engine):
|
||||
altitude_m = getaltitudeM(config_dict)
|
||||
station = WS23xx(altitude=altitude_m, **config_dict['WS23xx'])
|
||||
station = WS23xx(altitude=altitude_m, config_dict=config_dict,
|
||||
**config_dict['WS23xx'])
|
||||
return station
|
||||
|
||||
# FIXME: the pressure calculations belong in wxformulas
|
||||
@@ -212,6 +350,37 @@ def calculate_rain(newtotal, oldtotal):
|
||||
delta = None
|
||||
return delta
|
||||
|
||||
# FIXME: this goes in weeutil.weeutil
|
||||
def calculate_rain_rate(delta, curr_ts, last_ts):
|
||||
"""Calculate the rain rate based on the time between two rain readings.
|
||||
|
||||
delta: rainfall since last reading, in units of x
|
||||
|
||||
curr_ts: timestamp of current reading, in seconds
|
||||
|
||||
last_ts: timestamp of last reading, in seconds
|
||||
|
||||
return: rain rate in x per hour
|
||||
|
||||
If the period between readings is zero, ignore the rainfall since there
|
||||
is no way to calculate a rate with no period."""
|
||||
|
||||
if curr_ts is None:
|
||||
return None
|
||||
if last_ts is None:
|
||||
last_ts = curr_ts
|
||||
if delta is not None:
|
||||
period = curr_ts - last_ts
|
||||
if period != 0:
|
||||
rate = 3600 * delta / period
|
||||
else:
|
||||
rate = None
|
||||
if delta != 0:
|
||||
loginf('rain rate period is zero, ignoring rainfall of %f' % delta)
|
||||
else:
|
||||
rate = None
|
||||
return rate
|
||||
|
||||
class WS23xx(weewx.abstractstation.AbstractStation):
|
||||
"""Driver for LaCrosse WS23xx stations."""
|
||||
|
||||
@@ -245,26 +414,42 @@ class WS23xx(weewx.abstractstation.AbstractStation):
|
||||
"""
|
||||
self._last_rain = None
|
||||
self._last_cn = None
|
||||
self._poll_wait = 60
|
||||
|
||||
self.altitude = stn_dict['altitude']
|
||||
self.port = stn_dict.get('port', DEFAULT_PORT)
|
||||
self.polling_interval = stn_dict.get('polling_interval', None)
|
||||
self.model = stn_dict.get('model', 'LaCrosse WS23xx')
|
||||
self.calc_windchill = stn_dict.get('calculate_windchill', False)
|
||||
self.calc_dewpoint = stn_dict.get('calculate_dewpoint', False)
|
||||
self.port = stn_dict.get('port', DEFAULT_PORT)
|
||||
self.max_tries = int(stn_dict.get('max_tries', 5))
|
||||
self.retry_wait = int(stn_dict.get('retry_wait', 30))
|
||||
self.polling_interval = stn_dict.get('polling_interval', None)
|
||||
if self.polling_interval is not None:
|
||||
self.polling_interval = int(self.polling_interval)
|
||||
self.calc_windchill = weeutil.weeutil.tobool(stn_dict.get('calculate_windchill', False))
|
||||
self.calc_dewpoint = weeutil.weeutil.tobool(stn_dict.get('calculate_dewpoint', False))
|
||||
self.pressure_offset = stn_dict.get('pressure_offset', None)
|
||||
if self.pressure_offset is not None:
|
||||
self.pressure_offset = float(self.pressure_offset)
|
||||
self.max_tries = int(stn_dict.get('max_tries', 5))
|
||||
self.retry_wait = int(stn_dict.get('retry_wait', 30))
|
||||
self.disable_catchup = weeutil.weeutil.tobool(stn_dict.get('disable_catchup', False))
|
||||
|
||||
loginf('serial port is %s' % str(self.port))
|
||||
loginf('pressure offset is %s' % str(self.pressure_offset))
|
||||
loginf('driver version is %s' % DRIVER_VERSION)
|
||||
loginf('serial port is %s' % self.port)
|
||||
loginf('pressure offset is %s' % self.pressure_offset)
|
||||
loginf('polling interval is %s' % self.polling_interval)
|
||||
loginf('windchill will be %s' %
|
||||
('calculated' if self.calc_windchill else 'read from station'))
|
||||
loginf('dewpoint will be %s' %
|
||||
('calculated' if self.calc_dewpoint else 'read from station'))
|
||||
|
||||
# FIXME: this is a hack until we modify the driver api to have an
|
||||
# explicit genCatchupRecords method
|
||||
self.force_recgen = weeutil.weeutil.tobool(stn_dict.get('force_software_record_generation', True))
|
||||
if self.force_recgen:
|
||||
config_dict = stn_dict['config_dict']
|
||||
recgen = config_dict['StdArchive']['record_generation']
|
||||
if recgen.lower() != 'software':
|
||||
loginf("forcing record_generation to 'software'")
|
||||
config_dict['StdArchive']['record_generation'] = 'software'
|
||||
|
||||
@property
|
||||
def hardware_name(self):
|
||||
return self.model
|
||||
@@ -279,14 +464,11 @@ class WS23xx(weewx.abstractstation.AbstractStation):
|
||||
|
||||
def genLoopPackets(self):
|
||||
ntries = 0
|
||||
wait = 60
|
||||
while ntries < self.max_tries:
|
||||
ntries += 1
|
||||
serial_port = None
|
||||
try:
|
||||
serial_port = LinuxSerialPort(self.port)
|
||||
ws = Ws2300(serial_port)
|
||||
data = get_raw_data(ws, SENSOR_IDS)
|
||||
s = Station(self.port)
|
||||
data = s.get_raw_data(SENSOR_IDS)
|
||||
packet = data_to_packet(data, int(time.time() + 0.5),
|
||||
altitude=self.altitude,
|
||||
pressure_offset=self.pressure_offset,
|
||||
@@ -296,266 +478,89 @@ class WS23xx(weewx.abstractstation.AbstractStation):
|
||||
self._last_rain = packet['rainTotal']
|
||||
ntries = 0
|
||||
yield packet
|
||||
wait = self.get_wait(wait, data['cn'])
|
||||
time.sleep(wait)
|
||||
except Exception, e:
|
||||
|
||||
if self.polling_interval is not None:
|
||||
self._poll_wait = self.polling_interval
|
||||
if data['cn'] != self._last_cn:
|
||||
conn_info = get_conn_info(data['cn'])
|
||||
loginf("connection changed from %s to %s" %
|
||||
(get_conn_info(self._last_cn)[0], conn_info[0]))
|
||||
self._last_cn = data['cn']
|
||||
if self.polling_interval is None:
|
||||
loginf("using %s second polling interval"
|
||||
" for %s connection" %
|
||||
(conn_info[1], conn_info[0]))
|
||||
self._poll_wait = conn_info[1]
|
||||
time.sleep(self._poll_wait)
|
||||
except Ws2300.Ws2300Exception, e:
|
||||
logerr("Failed attempt %d of %d to get LOOP data: %s" %
|
||||
(ntries, self.max_tries, e))
|
||||
logdbg("Waiting %d seconds before retry" % self.retry_wait)
|
||||
time.sleep(self.retry_wait)
|
||||
finally:
|
||||
if serial_port is not None:
|
||||
serial_port.close()
|
||||
serial_port = None
|
||||
s.close()
|
||||
else:
|
||||
msg = "Max retries (%d) exceeded for LOOP data" % self.max_tries
|
||||
logerr(msg)
|
||||
raise weewx.RetriesExceeded(msg)
|
||||
|
||||
def genArchiveRecords(self, since_ts, count=0):
|
||||
serial_port = None
|
||||
try:
|
||||
if self.disable_catchup:
|
||||
raise NotImplementedError
|
||||
with Station(self.port) as s:
|
||||
last_rain = None
|
||||
serial_port = LinuxSerialPort(self.port)
|
||||
ws = Ws2300(serial_port)
|
||||
interval = get_archive_interval(ws)
|
||||
for ts,data in gen_records(ws, since_ts=since_ts, count=count):
|
||||
for ts,data in s.gen_records(since_ts=since_ts, count=count):
|
||||
record = data_to_packet(data, ts,
|
||||
altitude=self.altitude,
|
||||
pressure_offset=self.pressure_offset,
|
||||
last_rain=last_rain,
|
||||
calc_dewpoint=True,
|
||||
calc_windchill=True)
|
||||
record['interval'] = interval
|
||||
record['interval'] = data['interval']
|
||||
last_rain = record['rainTotal']
|
||||
yield record
|
||||
finally:
|
||||
if serial_port is not None:
|
||||
serial_port.close()
|
||||
serial_port = None
|
||||
|
||||
# FIXME: do not use station time until we can set it
|
||||
|
||||
# def getTime(self) :
|
||||
# serial_port = None
|
||||
# try:
|
||||
# serial_port = LinuxSerialPort(self.port)
|
||||
# ws = Ws2300(serial_port)
|
||||
# return get_time(ws)
|
||||
# finally:
|
||||
# if serial_port is not None:
|
||||
# serial_port.close()
|
||||
# serial_port = None
|
||||
# with Station(self.port) as s:
|
||||
# return s.get_time()
|
||||
|
||||
# def setTime(self, ts):
|
||||
# serial_port = None
|
||||
# try:
|
||||
# serial_port = LinuxSerialPort(self.port)
|
||||
# ws = Ws2300(serial_port)
|
||||
# set_time(ws, ts)
|
||||
# finally:
|
||||
# if serial_port is not None:
|
||||
# serial_port.close()
|
||||
# serial_port = None
|
||||
# with Station(self.port) as s:
|
||||
# s.set_time(ts)
|
||||
|
||||
def getArchiveInterval(self):
|
||||
serial_port = None
|
||||
try:
|
||||
serial_port = LinuxSerialPort(self.port)
|
||||
ws = Ws2300(serial_port)
|
||||
return get_archive_interval(ws)
|
||||
finally:
|
||||
if serial_port is not None:
|
||||
serial_port.close()
|
||||
serial_port = None
|
||||
with Station(self.port) as s:
|
||||
return s.get_archive_interval()
|
||||
|
||||
def setArchiveInterval(self, interval):
|
||||
serial_port = None
|
||||
try:
|
||||
serial_port = LinuxSerialPort(self.port)
|
||||
ws = Ws2300(serial_port)
|
||||
set_archive_interval(ws, interval)
|
||||
finally:
|
||||
if serial_port is not None:
|
||||
serial_port.close()
|
||||
serial_port = None
|
||||
with Station(self.port) as s:
|
||||
s.set_archive_interval(interval)
|
||||
|
||||
def getConfig(self):
|
||||
serial_port = None
|
||||
try:
|
||||
serial_port = LinuxSerialPort(self.port)
|
||||
ws = Ws2300(serial_port)
|
||||
data = get_raw_data(ws, Measure.IDS.keys())
|
||||
with Station(self.port) as s:
|
||||
data = s.get_raw_data(Measure.IDS.keys())
|
||||
fdata = {}
|
||||
for key in data:
|
||||
fdata[Measure.IDS[key].name] = data[key]
|
||||
return fdata
|
||||
finally:
|
||||
if serial_port is not None:
|
||||
serial_port.close()
|
||||
serial_port = None
|
||||
|
||||
def getRecordCount(self):
|
||||
serial_port = None
|
||||
try:
|
||||
serial_port = LinuxSerialPort(self.port)
|
||||
ws = Ws2300(serial_port)
|
||||
return get_record_count(ws)
|
||||
finally:
|
||||
if serial_port is not None:
|
||||
serial_port.close()
|
||||
serial_port = None
|
||||
with Station(self.port) as s:
|
||||
return s.get_record_count()
|
||||
|
||||
def get_wait(self, wait, conn):
|
||||
if self.polling_interval is not None:
|
||||
wait = self.polling_interval
|
||||
if conn != self._last_cn:
|
||||
loginf("connection changed from %s to %s" %
|
||||
(get_conn_info(self._last_cn)[0], get_conn_info(conn)[0]))
|
||||
if self.polling_interval is None:
|
||||
conn_info = get_conn_info(conn)
|
||||
loginf("using %s second polling interval for %s connection" %
|
||||
(conn_info[1], conn_info[0]))
|
||||
wait = conn_info[1]
|
||||
self._last_cn = conn
|
||||
return wait
|
||||
def clearHistory(self):
|
||||
with Station(self.port) as s:
|
||||
s.clear_memory()
|
||||
|
||||
|
||||
# ids for current weather conditions and connection type
|
||||
SENSOR_IDS = [
|
||||
'it','ih','ot','oh','pa', 'ws','wsh','w0','rh','rt','dp','wc','cn' ]
|
||||
SENSOR_IDS = [ 'it','ih','ot','oh','pa','wind','rh','rt','dp','wc','cn' ]
|
||||
# polling interval, in seconds, for various connection types
|
||||
POLLING_INTERVAL = { 0:("cable",8), 3:("lost",60), 15:("wireless",30) }
|
||||
|
||||
def get_conn_info(conn_type):
|
||||
return POLLING_INTERVAL.get(conn_type, ("unknown",60))
|
||||
|
||||
def set_time(ws, ts):
|
||||
"""Set station time to indicated unix epoch."""
|
||||
logdbg('setting station clock to %s' %
|
||||
weeutil.weeutil.timestamp_to_string(ts))
|
||||
for m in [Measure.IDS['sd'], Measure.IDS['st']]:
|
||||
data = m.conv.value2binary(ts)
|
||||
cmd = m.conv.write(data, None)
|
||||
ws.write_safe(m.address, *cmd[1:])
|
||||
|
||||
def get_time(ws):
|
||||
"""Return station time as unix epoch."""
|
||||
data = get_raw_data(ws, ['sw'])
|
||||
ts = int(data['sw'])
|
||||
logdbg('station clock is %s' % weeutil.weeutil.timestamp_to_string(ts))
|
||||
return ts
|
||||
|
||||
def set_archive_interval(ws, interval):
|
||||
"""Set the archive interval in minutes."""
|
||||
logdbg('setting hardware archive interval to %s minutes' % interval)
|
||||
for m,v in [(Measure.IDS['hi'],interval), # archive interval
|
||||
(Measure.IDS['hc'],1), # time till next sample
|
||||
(Measure.IDS['hn'],0)]: # number of valid records
|
||||
data = m.conv.value2binary(v)
|
||||
cmd = m.conv.write(data, None)
|
||||
ws.write_safe(m.address, *cmd[1:])
|
||||
|
||||
def get_archive_interval(ws):
|
||||
"""Return archive interval in minutes."""
|
||||
data = get_raw_data(ws, ['hi'])
|
||||
x = int(data['hi'])
|
||||
logdbg('station archive interval is %s minutes' % x)
|
||||
return x
|
||||
|
||||
def clear_memory(ws):
|
||||
"""Clear station memory."""
|
||||
logdbg('clearing console memory')
|
||||
for m,v in [(Measure.IDS['hn'],0)]: # number of valid records
|
||||
data = m.conv.value2binary(v)
|
||||
cmd = m.conv.write(data, None)
|
||||
ws.write_safe(m.address, *cmd[1:])
|
||||
|
||||
def get_record_count(ws):
|
||||
data = get_raw_data(ws, ['hn'])
|
||||
x = int(data['hn'])
|
||||
logdbg('record count is %s' % x)
|
||||
return x
|
||||
|
||||
def gen_records(ws, since_ts=None, count=None, use_computer_clock=True):
|
||||
"""Get latest count records from the station from oldest to newest. If
|
||||
count is 0 or None, return all records.
|
||||
|
||||
The station has a history interval, and it records when the last history
|
||||
sample was saved. So as long as the interval does not change between the
|
||||
first and last records, we are safe to infer timestamps for each record.
|
||||
This assumes that if the station loses power then the memory will be
|
||||
cleared.
|
||||
|
||||
There is no timestamp associated with each record - we have to guess.
|
||||
The station tells us the time until the next record and the epoch of the
|
||||
latest record, based on the station's clock. So we can use that or use
|
||||
the computer clock to guess the timestamp for each record."""
|
||||
|
||||
# FIXME: this is not atomic - if we overlap an interval, data are bogus
|
||||
|
||||
measures = [ Measure.IDS['hi'], Measure.IDS['hw'],
|
||||
Measure.IDS['hc'], Measure.IDS['hn'] ]
|
||||
raw_data = read_measurements(ws, measures)
|
||||
interval = 1 + int(measures[0].conv.binary2value(raw_data[0])) # minutes
|
||||
latest_ts = int(measures[1].conv.binary2value(raw_data[1])) # epoch
|
||||
time_to_next = int(measures[2].conv.binary2value(raw_data[2])) # minutes
|
||||
numrec = int(measures[3].conv.binary2value(raw_data[3]))
|
||||
|
||||
now = int(time.time())
|
||||
cstr = 'station'
|
||||
if use_computer_clock:
|
||||
latest_ts = now - (interval - time_to_next) * 60
|
||||
cstr = 'computer'
|
||||
logdbg("using %s clock with latest_ts of %s" %
|
||||
(cstr, weeutil.weeutil.timestamp_to_string(latest_ts)))
|
||||
|
||||
if since_ts is not None:
|
||||
count = int((now - since_ts) / (interval * 60))
|
||||
logdbg("count is %d to satisfy timestamp of %s" %
|
||||
(count, weeutil.weeutil.timestamp_to_string(since_ts)))
|
||||
if count == 0:
|
||||
return
|
||||
|
||||
if count and count > numrec:
|
||||
count = numrec
|
||||
if count and count > HistoryMeasure.MAX_HISTORY_RECORDS:
|
||||
count = HistoryMeasure.MAX_HISTORY_RECORDS
|
||||
|
||||
HistoryMeasure.set_constants(ws)
|
||||
recno_s = 0
|
||||
recno_e = count if count else HistoryMeasure.MAX_HISTORY_RECORDS
|
||||
last_ts = latest_ts - (recno_e-1) * interval * 60
|
||||
logdbg("downloading %d records from station" % recno_e)
|
||||
|
||||
measures = [HistoryMeasure(n) for n in range(recno_e-1, recno_s-1, -1)]
|
||||
raw_data = read_measurements(ws, measures)
|
||||
for measure, nybbles in zip(measures, raw_data):
|
||||
value = measure.conv.binary2value(nybbles)
|
||||
data_dict = {
|
||||
'it': value.temp_indoor,
|
||||
'ih': value.humidity_indoor,
|
||||
'ot': value.temp_outdoor,
|
||||
'oh': value.humidity_outdoor,
|
||||
'pa': value.pressure_absolute,
|
||||
'rt': value.rain,
|
||||
'ws': value.wind_speed,
|
||||
'w0': value.wind_direction,
|
||||
'wsh': None, # no gust in history
|
||||
'rh': None, # no rain rate in history
|
||||
'dp': None, # no dewpoint in history
|
||||
'wc': None, # no windchill in history
|
||||
}
|
||||
yield last_ts, data_dict
|
||||
last_ts += interval * 60
|
||||
|
||||
def get_raw_data(ws, labels):
|
||||
"""Get raw data from the station, return as dictionary."""
|
||||
measures = [ Measure.IDS[m] for m in labels ]
|
||||
raw_data = read_measurements(ws, measures)
|
||||
data_dict = dict(zip(labels, [ m.conv.binary2value(d) for m, d in zip(measures, raw_data) ]))
|
||||
return data_dict
|
||||
|
||||
def data_to_packet(data, ts, altitude=0, pressure_offset=None, last_rain=None,
|
||||
calc_dewpoint=False, calc_windchill=False):
|
||||
"""Convert raw data to format and units required by weewx.
|
||||
@@ -566,8 +571,9 @@ def data_to_packet(data, ts, altitude=0, pressure_offset=None, last_rain=None,
|
||||
uv index unitless unitless
|
||||
pressure mbar mbar
|
||||
wind speed m/s km/h
|
||||
wind gust m/s km/h
|
||||
wind dir degree degree
|
||||
wind gust None
|
||||
wind gust dir None
|
||||
rain mm cm
|
||||
rain rate cm/h
|
||||
"""
|
||||
@@ -581,15 +587,20 @@ def data_to_packet(data, ts, altitude=0, pressure_offset=None, last_rain=None,
|
||||
packet['outHumidity'] = data['oh']
|
||||
packet['pressure'] = data['pa']
|
||||
|
||||
packet['windSpeed'] = data['ws']
|
||||
if packet['windSpeed'] is not None:
|
||||
packet['windSpeed'] *= 3.6 # weewx wants km/h
|
||||
packet['windDir'] = data['w0'] if packet['windSpeed'] else None
|
||||
ws,wd,wso,wsv = data['wind']
|
||||
if wso == 0 and wsv == 0:
|
||||
packet['windSpeed'] = ws
|
||||
if packet['windSpeed'] is not None:
|
||||
packet['windSpeed'] *= 3.6 # weewx wants km/h
|
||||
packet['windDir'] = wd if packet['windSpeed'] else None
|
||||
else:
|
||||
loginf('invalid wind reading: speed=%s dir=%s overflow=%s invalid=%s' %
|
||||
(ws,wd,wso,wsv))
|
||||
packet['windSpeed'] = None
|
||||
packet['windDir'] = None
|
||||
|
||||
packet['windGust'] = data['wsh']
|
||||
if packet['windGust'] is not None:
|
||||
packet['windGust'] *= 3.6 # weewx wants km/h
|
||||
packet['windGustDir'] = data['w0'] if packet['windGust'] else None
|
||||
packet['windGust'] = None
|
||||
packet['windGustDir'] = None
|
||||
|
||||
packet['rainTotal'] = data['rt']
|
||||
if packet['rainTotal'] is not None:
|
||||
@@ -629,9 +640,173 @@ def data_to_packet(data, ts, altitude=0, pressure_offset=None, last_rain=None,
|
||||
return packet
|
||||
|
||||
|
||||
#==============================================================================
|
||||
class Station(object):
|
||||
"""Wrap the Ws2300 object so we can easily open serial port, read/write,
|
||||
close serial port without all of the try/except/finally scaffolding."""
|
||||
|
||||
def __init__(self, port):
|
||||
logdbg('create LinuxSerialPort')
|
||||
self.serial_port = LinuxSerialPort(port)
|
||||
logdbg('create Ws2300')
|
||||
self.ws = Ws2300(self.serial_port)
|
||||
|
||||
def __enter__(self):
|
||||
logdbg('station enter')
|
||||
return self
|
||||
|
||||
def __exit__(self, type, value, traceback):
|
||||
logdbg('station exit')
|
||||
self.ws = None
|
||||
self.close()
|
||||
|
||||
def close(self):
|
||||
logdbg('close LinuxSerialPort')
|
||||
self.serial_port.close()
|
||||
self.serial_port = None
|
||||
|
||||
def set_time(self, ts):
|
||||
"""Set station time to indicated unix epoch."""
|
||||
logdbg('setting station clock to %s' %
|
||||
weeutil.weeutil.timestamp_to_string(ts))
|
||||
for m in [Measure.IDS['sd'], Measure.IDS['st']]:
|
||||
data = m.conv.value2binary(ts)
|
||||
cmd = m.conv.write(data, None)
|
||||
self.ws.write_safe(m.address, *cmd[1:])
|
||||
|
||||
def get_time(self):
|
||||
"""Return station time as unix epoch."""
|
||||
data = self.get_raw_data(['sw'])
|
||||
ts = int(data['sw'])
|
||||
logdbg('station clock is %s' % weeutil.weeutil.timestamp_to_string(ts))
|
||||
return ts
|
||||
|
||||
def set_archive_interval(self, interval):
|
||||
"""Set the archive interval in minutes."""
|
||||
if int(interval) < 1:
|
||||
raise ValueError, 'archive interval must be greater than zero'
|
||||
logdbg('setting hardware archive interval to %s minutes' % interval)
|
||||
interval -= 1
|
||||
for m,v in [(Measure.IDS['hi'],interval), # archive interval in minutes
|
||||
(Measure.IDS['hc'],1), # time till next sample in minutes
|
||||
(Measure.IDS['hn'],0)]: # number of valid records
|
||||
data = m.conv.value2binary(v)
|
||||
cmd = m.conv.write(data, None)
|
||||
self.ws.write_safe(m.address, *cmd[1:])
|
||||
|
||||
def get_archive_interval(self):
|
||||
"""Return archive interval in minutes."""
|
||||
data = self.get_raw_data(['hi'])
|
||||
x = 1 + int(data['hi'])
|
||||
logdbg('station archive interval is %s minutes' % x)
|
||||
return x
|
||||
|
||||
def clear_memory(self):
|
||||
"""Clear station memory."""
|
||||
logdbg('clearing console memory')
|
||||
for m,v in [(Measure.IDS['hn'],0)]: # number of valid records
|
||||
data = m.conv.value2binary(v)
|
||||
cmd = m.conv.write(data, None)
|
||||
self.ws.write_safe(m.address, *cmd[1:])
|
||||
|
||||
def get_record_count(self):
|
||||
data = self.get_raw_data(['hn'])
|
||||
x = int(data['hn'])
|
||||
logdbg('record count is %s' % x)
|
||||
return x
|
||||
|
||||
def gen_records(self, since_ts=None, count=None, use_computer_clock=True):
|
||||
"""Get latest count records from the station from oldest to newest. If
|
||||
count is 0 or None, return all records.
|
||||
|
||||
The station has a history interval, and it records when the last
|
||||
history sample was saved. So as long as the interval does not change
|
||||
between the first and last records, we are safe to infer timestamps
|
||||
for each record. This assumes that if the station loses power then
|
||||
the memory will be cleared.
|
||||
|
||||
There is no timestamp associated with each record - we have to guess.
|
||||
The station tells us the time until the next record and the epoch of
|
||||
the latest record, based on the station's clock. So we can use that
|
||||
or use the computer clock to guess the timestamp for each record.
|
||||
|
||||
To ensure accurate data, the first record must be read within one
|
||||
minute of the initial read and the remaining records must be read
|
||||
within numrec * interval minutes.
|
||||
"""
|
||||
|
||||
logdbg("gen_records: since_ts=%s count=%s clock=%s" %
|
||||
(since_ts, count, use_computer_clock))
|
||||
measures = [ Measure.IDS['hi'], Measure.IDS['hw'],
|
||||
Measure.IDS['hc'], Measure.IDS['hn'] ]
|
||||
raw_data = read_measurements(self.ws, measures)
|
||||
interval = 1+int(measures[0].conv.binary2value(raw_data[0])) # minute
|
||||
latest_ts = int(measures[1].conv.binary2value(raw_data[1])) # epoch
|
||||
time_to_next = int(measures[2].conv.binary2value(raw_data[2])) # minute
|
||||
numrec = int(measures[3].conv.binary2value(raw_data[3]))
|
||||
|
||||
now = int(time.time())
|
||||
cstr = 'station'
|
||||
if use_computer_clock:
|
||||
latest_ts = now - (interval - time_to_next) * 60
|
||||
cstr = 'computer'
|
||||
logdbg("using %s clock with latest_ts of %s" %
|
||||
(cstr, weeutil.weeutil.timestamp_to_string(latest_ts)))
|
||||
|
||||
if not count:
|
||||
count = HistoryMeasure.MAX_HISTORY_RECORDS
|
||||
if since_ts is not None:
|
||||
count = int((now - since_ts) / (interval * 60))
|
||||
logdbg("count is %d to satisfy timestamp of %s" %
|
||||
(count, weeutil.weeutil.timestamp_to_string(since_ts)))
|
||||
if count == 0:
|
||||
return
|
||||
if count > numrec:
|
||||
count = numrec
|
||||
if count > HistoryMeasure.MAX_HISTORY_RECORDS:
|
||||
count = HistoryMeasure.MAX_HISTORY_RECORDS
|
||||
|
||||
# station is about to overwrite first record, so skip it
|
||||
if time_to_next <= 1 and count == HistoryMeasure.MAX_HISTORY_RECORDS:
|
||||
count -= 1
|
||||
|
||||
logdbg("downloading %d records from station" % count)
|
||||
HistoryMeasure.set_constants(self.ws)
|
||||
measures = [HistoryMeasure(n) for n in range(count-1, -1, -1)]
|
||||
raw_data = read_measurements(self.ws, measures)
|
||||
last_ts = latest_ts - (count-1) * interval * 60
|
||||
last_rain = None
|
||||
for measure, nybbles in zip(measures, raw_data):
|
||||
value = measure.conv.binary2value(nybbles)
|
||||
delta = calculate_rain(value.rain, last_rain)
|
||||
rainrate = calculate_rain_rate(delta, last_ts, last_ts-interval*60)
|
||||
data_dict = {
|
||||
'interval': interval,
|
||||
'it': value.temp_indoor,
|
||||
'ih': value.humidity_indoor,
|
||||
'ot': value.temp_outdoor,
|
||||
'oh': value.humidity_outdoor,
|
||||
'pa': value.pressure_absolute,
|
||||
'rt': value.rain,
|
||||
'wind': (value.wind_speed, value.wind_direction, 0, 0),
|
||||
'rh': rainrate,
|
||||
'dp': None, # no dewpoint in history
|
||||
'wc': None, # no windchill in history
|
||||
}
|
||||
yield last_ts, data_dict
|
||||
last_ts += interval * 60
|
||||
last_rain = value.rain
|
||||
|
||||
def get_raw_data(self, labels):
|
||||
"""Get raw data from the station, return as dictionary."""
|
||||
measures = [ Measure.IDS[m] for m in labels ]
|
||||
raw_data = read_measurements(self.ws, measures)
|
||||
data_dict = dict(zip(labels, [ m.conv.binary2value(d) for m, d in zip(measures, raw_data) ]))
|
||||
return data_dict
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# The following code was adapted from ws2300.py by Russell Stuart
|
||||
#==============================================================================
|
||||
# =============================================================================
|
||||
|
||||
VERSION = "1.8 2013-08-26"
|
||||
|
||||
@@ -1344,14 +1519,32 @@ class WindVelocityConversion(Conversion):
|
||||
def __init__(self):
|
||||
Conversion.__init__(self, "ms,d", 4, "wind speed and direction")
|
||||
def binary2value(self, data):
|
||||
return (bcd2num(data[:3])/10.0, bin2num(data[3:4]) * 22.5)
|
||||
return (bin2num(data[:3])/10.0, bin2num(data[3:4]) * 22.5)
|
||||
def value2binary(self, value):
|
||||
return num2bcd(value[0]*10, 3) + num2bin((value[1] + 11.5) / 22.5, 1)
|
||||
return num2bin(value[0]*10, 3) + num2bin((value[1] + 11.5) / 22.5, 1)
|
||||
def str(self, value):
|
||||
return "%.1f,%g" % value
|
||||
def parse(self, str):
|
||||
return tuple([float(x) for x in str.split(",")])
|
||||
|
||||
# The ws2300 1.8 implementation does not calculate wind speed correctly -
|
||||
# it uses bcd2num instead of bin2num. This conversion object uses bin2num
|
||||
# decoding and it reads all wind data in a single transcation so that we do
|
||||
# not suffer coherency problems.
|
||||
class WindConversion(Conversion):
|
||||
def __init__(self):
|
||||
Conversion.__init__(self, "ms,d,o,v", 12, "wind speed, dir, validity")
|
||||
def binary2value(self, data):
|
||||
overflow = data[0]
|
||||
validity = data[1]
|
||||
speed = bin2num(data[2:5]) / 10.0
|
||||
direction = data[5] * 22.5
|
||||
return (speed, direction, overflow, validity)
|
||||
def str(self, value):
|
||||
return "%.1f,%g,%s,%s" % value
|
||||
def parse(self, str):
|
||||
return tuple([float(x) for x in str.split(",")])
|
||||
|
||||
#
|
||||
# For non-numerical values.
|
||||
#
|
||||
@@ -1547,7 +1740,8 @@ conv_rain = BcdConversion("mm", 6, 2, "rain")
|
||||
conv_temp = BcdConversion("C", 4, 2, "temperature", -3000)
|
||||
conv_per2 = BinConversion("s", 2, 1, "time interval", 5)
|
||||
conv_per3 = BinConversion("min", 3, 0, "time interval")
|
||||
conv_wspd = BcdConversion("m/s", 3, 1, "speed")
|
||||
conv_wspd = BinConversion("m/s", 3, 1, "speed")
|
||||
conv_wind = WindConversion()
|
||||
|
||||
#
|
||||
# Define a measurement on the Ws2300. This encapsulates:
|
||||
@@ -1815,6 +2009,8 @@ Measure(0x6b5, "hc", conv_per3, "history time till sample")
|
||||
Measure(0x6b8, "hw", conv_stmp, "history last sample when")
|
||||
Measure(0x6c2, "hp", conv_rec2, "history last record pointer",reset=0)
|
||||
Measure(0x6c4, "hn", conv_rec2, "history number of records", reset=0)
|
||||
# get all of the wind info in a single invocation
|
||||
Measure(0x527, "wind", conv_wind, "wind")
|
||||
|
||||
#
|
||||
# Read the requests.
|
||||
@@ -1894,25 +2090,19 @@ def main():
|
||||
if options.port:
|
||||
port = options.port
|
||||
|
||||
serial_port = None
|
||||
try:
|
||||
serial_port = LinuxSerialPort(port)
|
||||
ws = Ws2300(serial_port)
|
||||
with Station(port) as s:
|
||||
if options.readings:
|
||||
data = get_raw_data(ws, SENSOR_IDS)
|
||||
data = s.get_raw_data(SENSOR_IDS)
|
||||
print data
|
||||
if options.records is not None:
|
||||
for ts,record in gen_records(ws, count=options.records):
|
||||
for ts,record in s.gen_records(count=options.records):
|
||||
print ts,record
|
||||
if options.measure:
|
||||
data = get_raw_data(ws, [options.measure])
|
||||
data = s.get_raw_data([options.measure])
|
||||
print data
|
||||
if options.hm:
|
||||
for m in Measure.IDS:
|
||||
print "%s\t%s" % (m, Measure.IDS[m].name)
|
||||
finally:
|
||||
if serial_port is not None:
|
||||
serial_port.close()
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
@@ -40,7 +40,7 @@ class ImageGenerator(weewx.reportengine.CachedReportGenerator):
|
||||
def setup(self):
|
||||
|
||||
self.image_dict = self.skin_dict['ImageGenerator']
|
||||
self.title_dict = self.skin_dict['Labels']['Generic']
|
||||
self.title_dict = self.skin_dict.get('Labels', {}).get('Generic', {})
|
||||
self.converter = weewx.units.Converter.fromSkinDict(self.skin_dict)
|
||||
self.formatter = weewx.units.Formatter.fromSkinDict(self.skin_dict)
|
||||
self.unit_helper= weewx.units.UnitInfoHelper(self.formatter, self.converter)
|
||||
|
||||
@@ -64,16 +64,19 @@ class Station(object):
|
||||
self.stn_info = stn_info
|
||||
|
||||
# Add a bunch of formatted attributes:
|
||||
hemispheres = skin_dict['Labels'].get('hemispheres', ('N','S','E','W'))
|
||||
latlon_formats = skin_dict['Labels'].get('latlon_formats')
|
||||
label_dict = skin_dict.get('Labels', {})
|
||||
hemispheres = label_dict.get('hemispheres', ('N','S','E','W'))
|
||||
latlon_formats = label_dict.get('latlon_formats')
|
||||
self.latitude = weeutil.weeutil.latlon_string(stn_info.latitude_f,
|
||||
hemispheres[0:2], 'lat', latlon_formats)
|
||||
hemispheres[0:2],
|
||||
'lat', latlon_formats)
|
||||
self.longitude = weeutil.weeutil.latlon_string(stn_info.longitude_f,
|
||||
hemispheres[2:4], 'lon', latlon_formats)
|
||||
self.altitude = weewx.units.ValueHelper(value_t=stn_info.altitude_vt,
|
||||
formatter=formatter,
|
||||
converter=converter)
|
||||
self.rain_year_str = time.strftime("%b", (0, self.rain_year_start, 1, 0,0,0,0,0,-1))
|
||||
hemispheres[2:4],
|
||||
'lon', latlon_formats)
|
||||
self.altitude = weewx.units.ValueHelper(value_t=stn_info.altitude_vt,
|
||||
formatter=formatter,
|
||||
converter=converter)
|
||||
self.rain_year_str = time.strftime("%b", (0, self.rain_year_start, 1, 0,0,0,0,0,-1))
|
||||
self.uptime = weeutil.weeutil.secs_to_string(time.time() - weewx.launchtime_ts) if weewx.launchtime_ts else ''
|
||||
self.version = weewx.__version__
|
||||
# The following works on Linux only:
|
||||
|
||||
@@ -413,7 +413,7 @@ class StdArchive(StdService):
|
||||
super(StdArchive, self).__init__(engine, config_dict)
|
||||
|
||||
# Get the archive interval from the configuration file
|
||||
software_archive_interval = config_dict['StdArchive'].as_int('archive_interval')
|
||||
self.archive_interval = config_dict['StdArchive'].as_int('archive_interval')
|
||||
|
||||
# If the station supports a hardware archive interval use that instead, but
|
||||
# warn if they mismatch:
|
||||
@@ -455,7 +455,7 @@ class StdArchive(StdService):
|
||||
# data still on the station, but not yet put in the database. Not
|
||||
# all consoles can do this, so be prepared to catch the exception:
|
||||
try:
|
||||
self._catchup()
|
||||
self._catchup(self.engine.console.genStartupRecords)
|
||||
except NotImplementedError:
|
||||
pass
|
||||
|
||||
@@ -517,7 +517,7 @@ class StdArchive(StdService):
|
||||
# be raised if the console does not support it. In that case, fall
|
||||
# back to software generation.
|
||||
try:
|
||||
self._catchup()
|
||||
self._catchup(self.engine.console.genArchiveRecords)
|
||||
except NotImplementedError:
|
||||
self._software_catchup()
|
||||
else:
|
||||
@@ -560,24 +560,26 @@ class StdArchive(StdService):
|
||||
def shutDown(self):
|
||||
self.archive.close()
|
||||
self.statsDb.close()
|
||||
|
||||
def _catchup(self):
|
||||
|
||||
def _catchup(self, generator):
|
||||
"""Pull any unarchived records off the console and archive them.
|
||||
|
||||
If the hardware does not support hardware archives, an exception of type
|
||||
NotImplementedError will be thrown."""
|
||||
If the hardware does not support hardware archives, an exception of
|
||||
type NotImplementedError will be thrown."""
|
||||
|
||||
# Find out when the archive was last updated.
|
||||
lastgood_ts = self.archive.lastGoodStamp()
|
||||
|
||||
try:
|
||||
# Now ask the console for any new records since then. (Not all consoles
|
||||
# support this feature).
|
||||
for record in self.engine.console.genArchiveRecords(lastgood_ts):
|
||||
self.engine.dispatchEvent(weewx.Event(weewx.NEW_ARCHIVE_RECORD, record=record, origin='hardware'))
|
||||
# Now ask the console for any new records since then.
|
||||
# (Not all consoles support this feature).
|
||||
for record in generator(lastgood_ts):
|
||||
self.engine.dispatchEvent(weewx.Event(weewx.NEW_ARCHIVE_RECORD,
|
||||
record=record,
|
||||
origin='hardware'))
|
||||
except weewx.HardwareError, e:
|
||||
syslog.syslog(syslog.LOG_ERR, "wxengine: Internal error detected. Catchup abandoned")
|
||||
syslog.syslog(syslog.LOG_ERR, "**** %s" % e)
|
||||
syslog.syslog(syslog.LOG_ERR, "**** %s" % e)
|
||||
|
||||
def _software_catchup(self):
|
||||
# Extract a record out of the old accumulator.
|
||||
|
||||
@@ -21,13 +21,28 @@ have any UV data.
|
||||
If software is specified for record_generation, do not try to use the stations
|
||||
archive interval.
|
||||
|
||||
Fixed bug when reading cooling_base option.
|
||||
|
||||
Default to sane behavior if skin does not define Labels.
|
||||
|
||||
Fixed bug in setting of CheetahGenerator options.
|
||||
|
||||
Fixed qsf and qpf summary values in forecast module.
|
||||
|
||||
Fixed handling of empty sky cover fields in WU forecasts.
|
||||
|
||||
Forecast module now considers the fctcode, condition, and wx fields for
|
||||
precipitation and obstructions to visibility.
|
||||
|
||||
Added options to forecast module to help diagnose parsing failures and new
|
||||
forecast formats.
|
||||
|
||||
Added retries when saving forecast to database and when reading from database.
|
||||
|
||||
Fixes to the Fine Offset driver to eliminate spikes caused by reading from
|
||||
memory before the pointer had been updated (not the same thing as an unstable
|
||||
read).
|
||||
|
||||
Added driver for LaCrosse 2300 series of weather stations.
|
||||
|
||||
Added driver for Hideki TE923 series of weather stations.
|
||||
|
||||
@@ -57,7 +57,7 @@ SCRIPTNAME=/etc/init.d/$NAME
|
||||
# check using ps not the pid file. pid file could be leftover.
|
||||
do_start()
|
||||
{
|
||||
NPROC=`ps ax | grep $WEEWX_BIN | grep weewx.pid | wc -l`
|
||||
NPROC=`ps ax | grep $WEEWX_BIN | grep $NAME.pid | wc -l`
|
||||
if [ $NPROC != 0 ]; then
|
||||
return 1
|
||||
fi
|
||||
@@ -105,7 +105,7 @@ case "$1" in
|
||||
esac
|
||||
;;
|
||||
status)
|
||||
NPROC=`ps ax | grep $WEEWX_BIN | grep weewx.pid | wc -l`
|
||||
NPROC=`ps ax | grep $WEEWX_BIN | grep $NAME.pid | wc -l`
|
||||
if [ $NPROC -gt 1 ]; then
|
||||
MSG="running multiple times"
|
||||
elif [ $NPROC = 1 ]; then
|
||||
|
||||
@@ -5,10 +5,12 @@
|
||||
compress
|
||||
delaycompress
|
||||
notifempty
|
||||
create 644 root adm
|
||||
# create 644 root adm
|
||||
create 644 syslog adm
|
||||
sharedscripts
|
||||
postrotate
|
||||
/etc/init.d/rsyslog stop
|
||||
/etc/init.d/rsyslog start
|
||||
reload rsyslog > /dev/null 2>&1
|
||||
# /etc/init.d/rsyslog stop
|
||||
# /etc/init.d/rsyslog start
|
||||
endscript
|
||||
}
|
||||
|
||||
@@ -2,93 +2,91 @@
|
||||
# $Id$
|
||||
# logwatch script to process weewx log files
|
||||
# Copyright 2013 Matthew Wall
|
||||
#
|
||||
# Revision History
|
||||
# 0.6 11nov13
|
||||
# * record unstable reads on fousb
|
||||
# 0.5 01nov13
|
||||
# * recognize more restful log output
|
||||
# 0.4 12oct13
|
||||
# * recognize more fousb log output
|
||||
# * recognize more ws28xx log output
|
||||
# * track forecasting counts
|
||||
# 0.3 09oct13
|
||||
# * match cheetahgenerator
|
||||
# * match failed restful uploads
|
||||
# * recognize forecast events
|
||||
# * match weewx HUPs
|
||||
# * recognize new driver startup diagnostics
|
||||
# * recognize new weewx wxengine startup diagnostics
|
||||
# * recognize ws28xx driver entries
|
||||
# 0.2 03jan13
|
||||
# * better labels for counts
|
||||
# 0.1 01jan13
|
||||
# * initial release
|
||||
|
||||
# FIXME: break this into modules instead of a single, monolithic blob
|
||||
|
||||
use strict;
|
||||
|
||||
my %counts;
|
||||
my %errors;
|
||||
|
||||
# keys for individual counts
|
||||
my $STARTUPS = 'wxengine: startups';
|
||||
my $HUP_RESTARTS = 'wxengine: restart from HUP';
|
||||
my $KBD_INTERRUPTS = 'wxengine: keyboard interrupts';
|
||||
my $ARCHIVE_RECORDS_ADDED = 'archive: records added';
|
||||
my $IMAGES_GENERATED = 'genimages: images generated';
|
||||
my $FILES_GENERATED = 'filegenerator: files generated';
|
||||
my $FILES_COPIED = 'reportengine: files copied';
|
||||
my $RECORDS_PUBLISHED = 'restful: records published';
|
||||
my $RECORDS_SKIPPED = 'restful: records skipped';
|
||||
my $RECORDS_FAILED = 'restful: publish failed';
|
||||
my $FOUSB_UNSTABLE_READS = 'fousb: unstable reads';
|
||||
my $FOUSB_MAGIC_NUMBERS = 'fousb: unrecognised magic number';
|
||||
my $FOUSB_RAIN_COUNTER = 'fousb: rain counter decrement';
|
||||
my $FOUSB_LOST_LOG_SYNC = 'fousb: lost log sync';
|
||||
my $FOUSB_LOST_SYNC = 'fousb: lost sync';
|
||||
my $FOUSB_MISSED_DATA = 'fousb: missed data';
|
||||
my $FOUSB_STATION_SYNC = 'fousb: station sync';
|
||||
my $WS23XX_CONNECTION_CHANGE = 'ws23xx: connection change';
|
||||
my $FORECAST_RECORDS = 'forecast: records generated';
|
||||
my $FORECAST_PRUNINGS = 'forecast: prunings';
|
||||
my $FORECAST_DOWNLOADS = 'forecast: downloads';
|
||||
my %counts = (
|
||||
$STARTUPS, 0,
|
||||
$HUP_RESTARTS, 0,
|
||||
$ARCHIVE_RECORDS_ADDED, 0,
|
||||
$IMAGES_GENERATED, 0,
|
||||
$FILES_GENERATED, 0,
|
||||
$FILES_COPIED, 0,
|
||||
$RECORDS_PUBLISHED, 0,
|
||||
$RECORDS_SKIPPED, 0,
|
||||
$FOUSB_UNSTABLE_READS, 0,
|
||||
$FOUSB_MAGIC_NUMBERS, 0,
|
||||
$FOUSB_LOST_LOG_SYNC, 0,
|
||||
$FOUSB_LOST_SYNC, 0,
|
||||
$FOUSB_MISSED_DATA, 0,
|
||||
$FOUSB_STATION_SYNC, 0,
|
||||
$FORECAST_RECORDS, 0,
|
||||
$FORECAST_PRUNINGS, 0,
|
||||
$FORECAST_DOWNLOADS, 0,
|
||||
);
|
||||
my $RECORDS_FAILED = 'restful: publish failed';
|
||||
my %errors;
|
||||
|
||||
# any lines that do not match the patterns we define
|
||||
my @unmatched = ();
|
||||
|
||||
# keep details of fine offset behavior to help debug the consoles
|
||||
# track upload errors to help diagnose network/server issues
|
||||
my @upload_errors = ();
|
||||
|
||||
# keep details of ws23xx behavior
|
||||
my @conn_change = ();
|
||||
|
||||
# keep details of fine offset behavior
|
||||
my @station_status = ();
|
||||
my @unstable_reads = ();
|
||||
my @magic_numbers = ();
|
||||
my @rain_counter = ();
|
||||
|
||||
my %itemized = (
|
||||
'upload failures', \@upload_errors,
|
||||
'fousb station status', \@station_status,
|
||||
'fousb unstable reads', \@unstable_reads,
|
||||
'fousb magic numbers', \@magic_numbers,
|
||||
'fousb rain counter', \@rain_counter,
|
||||
'ws23xx connection changes', \@conn_change,
|
||||
);
|
||||
|
||||
while(defined($_ = <STDIN>)) {
|
||||
chomp;
|
||||
if (/Archive: added archive record/) {
|
||||
$counts{$ARCHIVE_RECORDS_ADDED} += 1;
|
||||
} elsif (/genimages: Generated (\d+) images/) {
|
||||
$counts{$IMAGES_GENERATED} += $1;
|
||||
} elsif (/filegenerator: generated (\d+)/ ||
|
||||
/cheetahgenerator: generated (\d+)/) {
|
||||
$counts{$FILES_GENERATED} += $1;
|
||||
} elsif (/wxengine: Starting up weewx version/) {
|
||||
if (/wxengine: Starting up weewx version/) {
|
||||
$counts{$STARTUPS} += 1;
|
||||
} elsif (/wxengine: Received signal HUP/) {
|
||||
$counts{$HUP_RESTARTS} += 1;
|
||||
} elsif (/wxengine: Keyboard interrupt/) {
|
||||
$counts{$KBD_INTERRUPTS} += 1;
|
||||
} elsif (/Archive: added archive record/) {
|
||||
$counts{$ARCHIVE_RECORDS_ADDED} += 1;
|
||||
} elsif (/genimages: Generated (\d+) images/) {
|
||||
$counts{$IMAGES_GENERATED} += $1;
|
||||
} elsif (/genimages: aggregate interval required for aggregate type/ ||
|
||||
/genimages: line type \S+ skipped/) {
|
||||
$errors{$_} = $errors{$_} ? $errors{$_} + 1 : 1;
|
||||
} elsif (/filegenerator: generated (\d+)/ ||
|
||||
/cheetahgenerator: generated (\d+)/) {
|
||||
$counts{$FILES_GENERATED} += $1;
|
||||
} elsif (/reportengine: copied (\d+) files/) {
|
||||
$counts{$FILES_COPIED} += $1;
|
||||
} elsif (/restful: Skipped record/) {
|
||||
$counts{$RECORDS_SKIPPED} += 1;
|
||||
} elsif (/restful: Published record/) {
|
||||
$counts{$RECORDS_PUBLISHED} += 1;
|
||||
} elsif (/restful: Unable to publish record/) {
|
||||
my $key = $RECORDS_FAILED;
|
||||
if (/restful: Unable to publish record \d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d \S\S\S \(\d+\) to (\S+)/) {
|
||||
$key .= ' to site ' . $1;
|
||||
}
|
||||
$errors{$key} = $errors{$key} ? $errors{$key} + 1 : 1;
|
||||
} elsif (/fousb: station status/) {
|
||||
push @station_status, $_;
|
||||
} elsif (/fousb: unstable read: blocks differ/) {
|
||||
@@ -97,6 +95,9 @@ while(defined($_ = <STDIN>)) {
|
||||
} elsif (/fousb: unrecognised magic number/) {
|
||||
push @magic_numbers, $_;
|
||||
$counts{$FOUSB_MAGIC_NUMBERS} += 1;
|
||||
} elsif (/fousb: rain counter decrement/) {
|
||||
push @rain_counter, $_;
|
||||
$counts{$FOUSB_RAIN_COUNTER} += 1;
|
||||
} elsif (/fousb: lost log sync/) {
|
||||
$counts{$FOUSB_LOST_LOG_SYNC} += 1;
|
||||
} elsif (/fousb: lost sync/) {
|
||||
@@ -105,6 +106,9 @@ while(defined($_ = <STDIN>)) {
|
||||
$counts{$FOUSB_MISSED_DATA} += 1;
|
||||
} elsif (/fousb: synchronising to the weather station/) {
|
||||
$counts{$FOUSB_STATION_SYNC} += 1;
|
||||
} elsif (/ws23xx: connection changed from/) {
|
||||
push @conn_change, $_;
|
||||
$counts{$WS23XX_CONNECTION_CHANGE} += 1;
|
||||
} elsif (/forecast: .* generated 1 forecast record/) {
|
||||
$counts{$FORECAST_RECORDS} += 1;
|
||||
} elsif (/forecast: .* got (\d+) forecast records/) {
|
||||
@@ -113,23 +117,19 @@ while(defined($_ = <STDIN>)) {
|
||||
$counts{$FORECAST_PRUNINGS} += 1;
|
||||
} elsif (/forecast: .* downloading forecast/) {
|
||||
$counts{$FORECAST_DOWNLOADS} += 1;
|
||||
} elsif (/restful: Skipped record/) {
|
||||
$counts{$RECORDS_SKIPPED} += 1;
|
||||
} elsif (/restful: Published record/) {
|
||||
$counts{$RECORDS_PUBLISHED} += 1;
|
||||
} elsif (/genimages: aggregate interval required for aggregate type/ ||
|
||||
/genimages: line type \S+ skipped/) {
|
||||
$errors{$_} = $errors{$_} ? $errors{$_} + 1 : 1;
|
||||
} elsif (/restful: Unable to publish record/) {
|
||||
my $key = $RECORDS_FAILED;
|
||||
if (/restful: Unable to publish record \d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d \S\S\S \(\d+\) to (\S+)/) {
|
||||
$key .= ' to site ' . $1;
|
||||
}
|
||||
$errors{$key} = $errors{$key} ? $errors{$key} + 1 : 1;
|
||||
} elsif (/reportengine: Running reports for latest time/ ||
|
||||
} elsif (/awekas: Failed upload to AWEKAS/ ||
|
||||
/cosm: Failed upload to COSM/ ||
|
||||
/emoncms: Failed upload to EmonCMS/ ||
|
||||
/owm: Failed upload to OpenWeatherMap/ ||
|
||||
/seg: Failed upload to SmartEnergyGroups/ ||
|
||||
/wbug: Failed upload to WeatherBug/) {
|
||||
push @upload_errors, $_;
|
||||
} elsif (/last message repeated/ ||
|
||||
/reportengine: Running reports for latest time/ ||
|
||||
/reportengine: Found configuration file/ ||
|
||||
/reportengine: FTP upload not requested/ ||
|
||||
/reportengine: Running report / || # only when debug=1
|
||||
/reportengine: rsync upload not requested/ ||
|
||||
/restful: station will register with/ ||
|
||||
/restful: Registration interval/ ||
|
||||
/\*\*\*\* Registration interval/ ||
|
||||
@@ -143,10 +143,10 @@ while(defined($_ = <STDIN>)) {
|
||||
/stats: Created schema for statistical database/ ||
|
||||
/stats: Schema exists with/ ||
|
||||
/\*\*\*\* \'station\'/ ||
|
||||
/\*\*\*\* required parameter \'\'station\'\'/ ||
|
||||
/\*\*\*\* Waiting 60 seconds then retrying/ ||
|
||||
/wxengine: Station does not support reading the time/ ||
|
||||
/wxengine: Starting main packet loop/ ||
|
||||
/wxengine: Keyboard interrupt/ ||
|
||||
/wxengine: Shut down StdReport thread/ ||
|
||||
/wxengine: Shut down StdRESTful thread/ ||
|
||||
/wxengine: Loading service/ ||
|
||||
@@ -171,7 +171,9 @@ while(defined($_ = <STDIN>)) {
|
||||
/wxengine: pid file is / ||
|
||||
/wxengine: Use LOOP data in/ ||
|
||||
/wxengine: Received signal/ ||
|
||||
/cheetahgenerator: Running / ||
|
||||
/cheetahgenerator: skip/ ||
|
||||
/fousb: driver version is/ ||
|
||||
/fousb: found station on USB/ ||
|
||||
/fousb: altitude is/ ||
|
||||
/fousb: archive interval is/ ||
|
||||
@@ -189,6 +191,7 @@ while(defined($_ = <STDIN>)) {
|
||||
/fousb: avoid/ ||
|
||||
/fousb: setting sensor clock/ ||
|
||||
/fousb: setting station clock/ ||
|
||||
/fousb: estimated log time/ ||
|
||||
/fousb: returning archive record/ ||
|
||||
/fousb: packet timestamp/ ||
|
||||
/fousb: log timestamp/ ||
|
||||
@@ -196,6 +199,7 @@ while(defined($_ = <STDIN>)) {
|
||||
/fousb: get \d+ records since/ ||
|
||||
/fousb: synchronised to/ ||
|
||||
/fousb: pressures:/ ||
|
||||
/fousb: status / ||
|
||||
/ws28xx: MainThread: driver version is/ ||
|
||||
/ws28xx: MainThread: frequency is/ ||
|
||||
/ws28xx: MainThread: altitude is/ ||
|
||||
@@ -260,6 +264,31 @@ while(defined($_ = <STDIN>)) {
|
||||
/ws28xx: RFComm: CHistoryDataSet.read/ ||
|
||||
/ws28xx: RFComm: testConfigChanged/ ||
|
||||
/ws28xx: RFComm: SetTime/ ||
|
||||
/ws23xx: driver version is / ||
|
||||
/ws23xx: polling interval is / ||
|
||||
/ws23xx: station archive interval is / ||
|
||||
/ws23xx: using computer clock with / ||
|
||||
/ws23xx: using \d+ sec\S* polling interval/ ||
|
||||
/ws23xx: windchill will be / ||
|
||||
/ws23xx: dewpoint will be / ||
|
||||
/ws23xx: pressure offset is / ||
|
||||
/ws23xx: serial port is / ||
|
||||
/ws23xx: downloading \d+ records from station/ ||
|
||||
/ws23xx: count is \d+ to satisfy timestamp/ ||
|
||||
/ws23xx: windchill: / ||
|
||||
/ws23xx: dewpoint: / ||
|
||||
/ws23xx: station clock is / ||
|
||||
/te923: driver version is / ||
|
||||
/te923: polling interval is / ||
|
||||
/te923: windchill will be / ||
|
||||
/te923: sensor map is / ||
|
||||
/te923: battery map is / ||
|
||||
/te923: Found device on USB/ ||
|
||||
/owfs: driver version is / ||
|
||||
/owfs: interface is / ||
|
||||
/owfs: polling interval is / ||
|
||||
/owfs: sensor map is / ||
|
||||
/cmon: cpuinfo: / ||
|
||||
/forecast: .* starting thread/ ||
|
||||
/forecast: .* terminating thread/ ||
|
||||
/forecast: .* not yet time to do the forecast/ ||
|
||||
@@ -268,18 +297,43 @@ while(defined($_ = <STDIN>)) {
|
||||
/forecast: .* tstr=/ ||
|
||||
/forecast: .* interval=\d+ max_age=/ ||
|
||||
/forecast: .* deleted forecasts/ ||
|
||||
/forecast: .* saved \d+ forecast records/ ||
|
||||
/forecast: ZambrettiThread: Zambretti: generating/ ||
|
||||
/forecast: ZambrettiThread: Zambretti: pressure/ ||
|
||||
/forecast: ZambrettiThread: Zambretti: code is/ ||
|
||||
/forecast: NWSThread: NWS: forecast matrix/ ||
|
||||
/forecast: XTideThread: XTide: tide matrix/ ||
|
||||
/forecast: XTideThread: XTide: generating tides/ ||
|
||||
/emoncms: Failed upload attempt/ ||
|
||||
/emoncms: Failed upload to EmonCMS/ ||
|
||||
/\*\*\*\* Failed upload to EmonCMS/ ||
|
||||
/seg: Failed upload attempt/ ||
|
||||
/seg: Failed upload to SmartEnergyGroups/ ||
|
||||
/\*\*\*\* Failed upload to SmartEnergyGroups/ ||
|
||||
/awekas: Failed upload attempt/ ||
|
||||
/awekas: code/ ||
|
||||
/awekas: read/ ||
|
||||
/awekas: url/ ||
|
||||
/awekas: data/ ||
|
||||
/cosm: Failed upload attempt/ ||
|
||||
/cosm: Failed upload to COSM/ ||
|
||||
/\*\*\*\* Failed upload to COSM/) {
|
||||
/cosm: code/ ||
|
||||
/cosm: read/ ||
|
||||
/cosm: url/ ||
|
||||
/cosm: data/ ||
|
||||
/emoncms: Failed upload attempt/ ||
|
||||
/emoncms: code/ ||
|
||||
/emoncms: read/ ||
|
||||
/emoncms: data/ ||
|
||||
/emoncms: url/ ||
|
||||
/owm: Failed upload attempt/ ||
|
||||
/owm: code/ ||
|
||||
/owm: read/ ||
|
||||
/owm: url/ ||
|
||||
/owm: data/ ||
|
||||
/seg: Failed upload attempt/ ||
|
||||
/seg: code/ ||
|
||||
/seg: read/ ||
|
||||
/seg: url/ ||
|
||||
/seg: data/ ||
|
||||
/wbug: Failed upload attempt/ ||
|
||||
/wbug: code/ ||
|
||||
/wbug: read/ ||
|
||||
/wbug: url/ ||
|
||||
/wbug: data/) {
|
||||
# ignore
|
||||
} elsif (! /weewx/) {
|
||||
# ignore
|
||||
@@ -294,13 +348,13 @@ foreach my $k (sort keys %counts) {
|
||||
printf(" %-40s %6d\n", $k, $counts{$k});
|
||||
}
|
||||
|
||||
report("fousb station status", \@station_status) if $#station_status >= 0;
|
||||
report("fousb unstable reads", \@unstable_reads) if $#unstable_reads >= 0;
|
||||
report("fousb magic numbers", \@magic_numbers) if $#magic_numbers >= 0;
|
||||
|
||||
print "\nerrors:\n";
|
||||
foreach my $k (keys %errors) {
|
||||
printf(" %3d %s\n", $errors{$k}, $k);
|
||||
printf(" %-40s %6d\n", $k, $errors{$k});
|
||||
}
|
||||
|
||||
foreach my $k (sort keys %itemized) {
|
||||
report($k, $itemized{$k}) if scalar @{$itemized{$k}} > 0;
|
||||
}
|
||||
|
||||
report("unmatched lines", \@unmatched) if $#unmatched >= 0;
|
||||
|
||||
Reference in New Issue
Block a user