diff --git a/MANIFEST b/MANIFEST index b54b485d..f95ffb25 100644 --- a/MANIFEST +++ b/MANIFEST @@ -50,15 +50,16 @@ bin/weewx/uwxutils.py bin/weewx/wxengine.py bin/weewx/wxformulas.py bin/weewx/drivers/__init__.py -bin/weewx/drivers/ads.py +bin/weewx/drivers/cc3000.py bin/weewx/drivers/fousb.py -bin/weewx/drivers/peetbros.py bin/weewx/drivers/simulator.py bin/weewx/drivers/te923.py +bin/weewx/drivers/ultimeter.py bin/weewx/drivers/vantage.py bin/weewx/drivers/wmr100.py bin/weewx/drivers/wmr200.py bin/weewx/drivers/wmr9x8.py +bin/weewx/drivers/ws1.py bin/weewx/drivers/ws23xx.py bin/weewx/drivers/ws28xx.py docs/changes.txt diff --git a/bin/wee_config_vantage b/bin/wee_config_vantage index ca2ab343..61b27f78 100755 --- a/bin/wee_config_vantage +++ b/bin/wee_config_vantage @@ -330,7 +330,7 @@ def set_rain_year_start(station, rain_year_start): def set_time(station): print "Setting time on console..." - station.setTime(time.time()) + station.setTime() newtime_ts = station.getTime() print "Current console time is %s" % weeutil.weeutil.timestamp_to_string(newtime_ts) diff --git a/bin/wee_config_ws23xx b/bin/wee_config_ws23xx index 9fbc65f7..7a3cd748 100755 --- a/bin/wee_config_ws23xx +++ b/bin/wee_config_ws23xx @@ -132,15 +132,13 @@ def setclock(station, prompt): v = station.getTime() vstr = weeutil.weeutil.timestamp_to_string(v) print "Station clock is", vstr - now = int(time.time() + 0.5) - nstr = weeutil.weeutil.timestamp_to_string(now) if prompt: - ans = raw_input("Set station clock to %s (y/n)? " % nstr) + ans = raw_input("Set station clock (y/n)? ") else: - print "Setting station clock to %s" % nstr + print "Setting station clock" ans = 'y' if ans == 'y' : - station.setTime(now) + station.setTime() v = station.getTime() vstr = weeutil.weeutil.timestamp_to_string(v) print "Station clock is now", vstr diff --git a/bin/wee_config_ws28xx b/bin/wee_config_ws28xx index 618e319f..5ad52a96 100755 --- a/bin/wee_config_ws28xx +++ b/bin/wee_config_ws28xx @@ -23,6 +23,7 @@ issues between weather station console and transceiver. import optparse import syslog import time +import sys import weewx.drivers.ws28xx import weewx.units @@ -49,16 +50,18 @@ def main(): help="pair the USB transceiver with a station console") parser.add_option("--info", dest="info", action="store_true", help="display weather station configuration") + parser.add_option("--set-interval", dest="interval", type=int, metavar="N", + help="set logging interval to N minutes") parser.add_option("--current", dest="current", action="store_true", help="get the current weather conditions") parser.add_option("--history-since", dest="recmin", type=int, metavar="N", help="display history records since N minutes ago") parser.add_option("--history", dest="nrecords", type=int, metavar="N", help="display N history records") - parser.add_option("--format", dest="format", type=str, metavar="FORMAT", - help="format for history, one of raw, table, or dict") parser.add_option("--maxtries", dest="maxtries", type=int, help="maximum number of retries, 0 indicates no max") + parser.add_option("-y", dest="noprompt", action="store_true", + help="answer yes to every prompt") parser.add_option("--debug", dest="debug", action="store_true", help="display diagnostic information while running") @@ -75,26 +78,25 @@ def main(): station = weewx.drivers.ws28xx.WS28xx(altitude=altitude_m, **config_dict['WS28xx']) - if options.format is None: - options.format = 'table' - elif (options.format.lower() != 'raw' and - options.format.lower() != 'table' and - options.format.lower() != 'dict'): - print "Unknown format '%s'. Known formats include 'raw', 'table', and 'dict'." % options.format - exit(1) + if options.noprompt: + prompt = False + else: + prompt = True maxtries = 3 if options.maxtries is None else int(options.maxtries) if options.check: check_transceiver(station, maxtries) elif options.pair: pair(station, maxtries) + elif options.interval is not None: + set_interval(station, maxtries, options.interval, prompt) elif options.current: current(station, maxtries) elif options.nrecords is not None: - history(station, maxtries, count=options.nrecords, fmt=options.format) + history(station, maxtries, count=options.nrecords) elif options.recmin is not None: ts = int(time.time()) - options.recmin * 60 - history(station, maxtries, ts=ts, fmt=options.format) + history(station, maxtries, ts=ts) else: info(station, maxtries) @@ -106,139 +108,157 @@ def check_transceiver(station, maxtries): """See if the transceiver is installed and operational.""" print 'Checking for transceiver...' ntries = 0 - try: - while ntries < maxtries: - ntries += 1 - if station.transceiver_is_present(): - print 'Transceiver is present' - sn = station.transceiver_serial() - print 'serial: ' % sn - tid = station.transceiver_id() - print 'id: %d (0x%04x)' % (tid, tid) - break - print 'Not found (attempt %d of %d) ...' % (ntries, maxtries) - time.sleep(5) - else: - print 'Transceiver not responding.' - except Exception, e: - pass + while ntries < maxtries: + ntries += 1 + if station.transceiver_is_present(): + print 'Transceiver is present' + sn = station.get_transceiver_serial() + print 'serial: %s' % sn + tid = station.get_transceiver_id() + print 'id: %d (0x%04x)' % (tid, tid) + break + print 'Not found (attempt %d of %d) ...' % (ntries, maxtries) + time.sleep(5) + else: + print 'Transceiver not responding.' def pair(station, maxtries): """Pair the transceiver with the station console.""" print 'Pairing transceiver with console...' maxwait = 90 # how long to wait between button presses, in seconds ntries = 0 - try: - while ntries < maxtries or maxtries == 0: - if station.transceiver_is_paired(): - print 'Transceiver is paired to console' - break - ntries += 1 - msg = 'Press and hold the [v] key until "PC" appears' - if maxtries > 0: - msg += ' (attempt %d of %d)' % (ntries, maxtries) - else: - msg += ' (attempt %d)' % ntries - print msg - now = start_ts = int(time.time()) - while now - start_ts < maxwait and not station.transceiver_is_paired(): - time.sleep(5) - now = int(time.time()) + while ntries < maxtries or maxtries == 0: + if station.transceiver_is_paired(): + print 'Transceiver is paired to console' + break + ntries += 1 + msg = 'Press and hold the [v] key until "PC" appears' + if maxtries > 0: + msg += ' (attempt %d of %d)' % (ntries, maxtries) else: - print 'Transceiver not paired to console.' - except Exception, e: - pass + msg += ' (attempt %d)' % ntries + print msg + now = start_ts = int(time.time()) + while now - start_ts < maxwait and not station.transceiver_is_paired(): + time.sleep(5) + now = int(time.time()) + else: + print 'Transceiver not paired to console.' + +def get_interval(station, maxtries): + cfg = get_config(station, maxtries) + if cfg is None: + return None + return weewx.drivers.ws28xx.getHistoryInterval(cfg['history_interval']) + +def get_config(station, maxtries): + start_ts = None + ntries = 0 + while ntries < maxtries or maxtries == 0: + cfg = station.get_config() + if cfg is not None: + return cfg + ntries += 1 + if start_ts is None: + start_ts = int(time.time()) + else: + dur = int(time.time()) - start_ts + print 'No data after %d seconds (press SET to sync)' % dur + time.sleep(30) + return None + +def set_interval(station, maxtries, interval, prompt): + """Set the station archive interval""" + print 'Querying the station...' + v = get_interval(station, maxtries) + if v is None: + return + ans = None + while ans not in ['y', 'n']: + print "Interval is", v + if prompt: + ans = raw_input("Set interval to %d minutes (y/n)? " % interval) + else: + print "Setting interval to %d minutes" % interval + ans = 'y' + if ans == 'y' : + station.set_interval(interval) + v = get_interval(station, maxtries) + if v is None: + print "Cannot confirm change of interval" + return + print "Interval is now", v + elif ans == 'n': + print "Set interval cancelled." def info(station, maxtries): """Query the station then display the settings.""" print 'Querying the station for the configuration...' - start_ts = None - ntries = 0 - try: - while ntries < maxtries or maxtries == 0: - config = station.get_config() - if config is not None: - print_dict(config) - break - if start_ts is None: - start_ts = int(time.time()) - else: - dur = int(time.time()) - start_ts - print 'No data after %d seconds (press SET to sync)' % dur - time.sleep(30) - except Exception: - pass + cfg = get_config(station, maxtries) + if cfg is not None: + print_dict(cfg) def current(station, maxtries): """Get current weather observation.""" print 'Querying the station for current weather data...' start_ts = None ntries = 0 - try: - while ntries < maxtries or maxtries == 0: - packet = station.get_observation() - if packet is not None: - print_dict(packet) - break - if start_ts is None: - start_ts = int(time.time()) - else: - dur = int(time.time()) - start_ts - print 'No data after %d seconds (press SET to sync)' % dur - time.sleep(30) - except Exception: - pass + while ntries < maxtries or maxtries == 0: + packet = station.get_observation() + if packet is not None: + print_dict(packet) + break + ntries += 1 + if start_ts is None: + start_ts = int(time.time()) + else: + dur = int(time.time()) - start_ts + print 'No data after %d seconds (press SET to sync)' % dur + time.sleep(30) -def history(station, maxtries, ts=0, count=0, fmt='raw'): +def history(station, maxtries, ts=0, count=0): """Display the indicated number of records or the records since the specified timestamp (local time, in seconds)""" print "Querying the station for historical records..." - records = [] - start_ts = None ntries = 0 - try: - while ntries < maxtries or maxtries == 0: - records = station.get_history(since_ts=ts, num_rec=count) - if records is not None: - break - if start_ts is None: - start_ts = int(time.time()) - else: - dur = int(time.time()) - start_ts - print 'No data after %d seconds (press SET to sync)' % dur - time.sleep(30) - except Exception: - pass - - for i,r in enumerate(records): - if fmt.lower() == 'raw': - raw_dump(r['datetime'], r['ptr'], r['raw_data']) - elif fmt.lower() == 'table': - table_dump(r['datetime'], r['data'], i==0) + last_n = n = None + last_ts = now = int(time.time()) + station.start_caching_history(since_ts=ts) + t = weewx.drivers.ws28xx.WS28xx.max_records + while n is None or n > 0: + if ntries >= maxtries: + print 'Giving up after %d tries' % ntries + break + time.sleep(30) + now = int(time.time()) + n = station.get_num_history_scanned() + if n == last_n: + ntries += 1 + dur = now - last_ts + print 'No data after %d seconds (press SET to sync)' % dur else: - print r['datetime'], r['data'] - -def raw_dump(date, pos, data): - print date, - print "%04x" % pos, - for item in data: - print "%02x" % item, - print - -def table_dump(date, data, showlabels=False): - if showlabels: - print '# date time', - for key in data: - print key, - print - print date, - for key in data: - print data[key], + ntries = 0 + last_ts = now + last_n = n + ni = station.get_next_history_index() + li = station.get_latest_history_index() + msg = " scanned %s of %s: current=%s latest=%s\r" % (n, t, ni, li) + sys.stdout.write(msg) + sys.stdout.flush() + station.stop_caching_history() + records = station.get_history_cache_records() + station.clear_history_cache() print + print 'Found a total of %d records' % len(records) + for r in records: + print r def print_dict(data): - for key in sorted(data, key=data.get): - print '%s: %s' % (key, data[key]) + for x in sorted(data.keys()): + if x == 'dateTime': + print '%s: %s' % (x, weeutil.weeutil.timestamp_to_string(data[x])) + else: + print '%s: %s' % (x, data[x]) if __name__=="__main__" : diff --git a/bin/weewx/abstractstation.py b/bin/weewx/abstractstation.py index a54e6555..4bfffbcf 100644 --- a/bin/weewx/abstractstation.py +++ b/bin/weewx/abstractstation.py @@ -32,7 +32,7 @@ class AbstractStation(object): def getTime(self): raise NotImplementedError("Method 'getTime' not implemented") - def setTime(self, newtime_ts): + def setTime(self): raise NotImplementedError("Method 'setTime' not implemented") def closePort(self): diff --git a/bin/weewx/drivers/cc3000.py b/bin/weewx/drivers/cc3000.py index 9a3978b1..49bdb1e3 100644 --- a/bin/weewx/drivers/cc3000.py +++ b/bin/weewx/drivers/cc3000.py @@ -24,6 +24,7 @@ catchup on startup. from __future__ import with_statement import serial +import string import syslog import time @@ -37,7 +38,7 @@ INHG_PER_MBAR = 0.0295333727 METER_PER_FOOT = 0.3048 MILE_PER_KM = 0.621371 -DRIVER_VERSION = '0.6' +DRIVER_VERSION = '0.7' DEFAULT_PORT = '/dev/ttyS0' def logmsg(level, msg): @@ -67,18 +68,20 @@ class CC3000(weewx.abstractstation.AbstractStation): '''weewx driver that communicates with a RainWise CC3000 data logger.''' # map rainwise names to weewx names - LABEL_MAP = { 'TIMESTAMP': 'TIMESTAMP', - 'TEMP OUT': 'outTemp', - 'HUMIDITY': 'outHumidity', - 'WIND DIRECTION': 'windDir', - 'WIND SPEED': 'windSpeed', - 'WIND GUST': 'windGust', - 'PRESSURE': 'pressure', - 'TEMP IN': 'inTemp', - 'RAIN': 'day_rain_total', - 'STATION BATTERY': 'consBatteryVoltage', - 'BATTERY BACKUP': 'bkupBatteryVoltage', - } + DEFAULT_LABEL_MAP = { 'TIMESTAMP': 'TIMESTAMP', + 'TEMP OUT': 'outTemp', + 'HUMIDITY': 'outHumidity', + 'WIND DIRECTION': 'windDir', + 'WIND SPEED': 'windSpeed', + 'WIND GUST': 'windGust', + 'PRESSURE': 'pressure', + 'TEMP IN': 'inTemp', + 'RAIN': 'day_rain_total', + 'STATION BATTERY': 'consBatteryVoltage', + 'BATTERY BACKUP': 'bkupBatteryVoltage', + 'SOLAR RADIATION': 'radiation', + 'UV INDEX': 'UV', + } def __init__(self, **stn_dict): self.altitude = stn_dict['altitude'] @@ -91,6 +94,7 @@ class CC3000(weewx.abstractstation.AbstractStation): self.use_station_time = stn_dict.get('use_station_time', True) self.max_tries = int(stn_dict.get('max_tries', 5)) self.retry_wait = int(stn_dict.get('retry_wait', 60)) + self.label_map = stn_dict.get('label_map', self.DEFAULT_LABEL_MAP) self.last_rain = None @@ -165,9 +169,9 @@ class CC3000(weewx.abstractstation.AbstractStation): v = station.get_time() return _to_ts(v) - def setTime(self, ts): + def setTime(self): with Station(self.port) as station: - station.set_time(ts) + station.set_time() def get_current(self): with Station(self.port) as station: @@ -299,7 +303,7 @@ class CC3000(weewx.abstractstation.AbstractStation): for i,v in enumerate(values): if i >= len(self.header): continue - label = self.LABEL_MAP.get(self.header[i]) + label = self.label_map.get(self.header[i]) if label is None: continue if label == 'TIMESTAMP': @@ -323,6 +327,9 @@ def _to_ts(tstr, fmt="%Y/%m/%d %H:%M:%S"): def _format_bytes(buf): return ' '.join(["%0.2X" % ord(c) for c in buf]) +def _fmt(buf): + return filter(lambda x: x in string.printable, buf) + # calculate the crc for a string using CRC-16-CCITT # http://bytes.com/topic/python/insights/887357-python-check-crc-frame-crc-16-ccitt def _crc16(data): @@ -406,11 +413,10 @@ class Station(object): def command(self, cmd): self.write("%s\r" % cmd) data = self.get_data() - logdbg("station replied to command with '%s'" % data) data = data.strip() if data != cmd: - raise weewx.WeeWxIOError("Command failed: cmd='%s' data='%s'" % - (cmd, data)) + raise weewx.WeeWxIOError("Command failed: cmd='%s' reply='%s' (%s)" + % (cmd, _fmt(data), _format_bytes(data))) return self.get_data() def send_cmd(self, cmd): @@ -433,7 +439,10 @@ class Station(object): break else: raise weewx.WeeWxIOError("Unexpected byte 0x%0.2X" % ord(c)) - buf.append(c) + if c in string.printable: + buf.append(c) + else: + loginf("skipping unprintable character 0x%0.2X" % ord(c)) data = ''.join(buf) logdbg("got bytes: '%s'" % _format_bytes(data)) _check_crc(data) @@ -443,7 +452,7 @@ class Station(object): logdbg("set echo to %s" % cmd) data = self.command('ECHO=%s' % cmd) if data != 'OK': - raise weewx.WeeWxIOError("Set ECHO failed: %s" % data) + raise weewx.WeeWxIOError("Set ECHO failed: %s" % _fmt(data)) def get_header(self): data = self.command("HEADER") @@ -462,13 +471,13 @@ class Station(object): def get_memory_status(self): data = self.command("MEM=?") - logdbg("memory status: %s" % data) + logdbg("memory status: %s" % _fmt(data)) return data def clear_memory(self): data = self.command("MEM=CLEAR") if data != 'OK': - raise weewx.WeeWxIOError("Failed to clear memory: %s" % data) + raise weewx.WeeWxIOError("Failed to clear memory: %s" % _fmt(data)) def gen_records(self, nrec): """generator function for getting records from the device""" @@ -499,13 +508,14 @@ class Station(object): data = self.command("TIME=?") return data - def set_time(self, ts): - tstr = time.strftime("%Y/%m/%d %H:%M:%S", time.localtime(ts)) - logdbg("set time to %s (%s)" % (tstr, ts)) + def set_time(self): + tstr = time.strftime("%Y/%m/%d %H:%M:%S", time.localtime(time.time())) + logdbg("set time to %s (%s)" % (tstr, tstr)) s = "TIME=%s" % tstr data = self.command(s) if data != 'OK': - raise weewx.WeeWxIOError("Failed to set time to %s: %s" % (s,data)) + raise weewx.WeeWxIOError("Failed to set time to %s: %s" % + (s, _fmt(data))) def get_units(self): data = self.command("UNITS=?") @@ -516,7 +526,7 @@ class Station(object): data = self.command("UNITS=%s" % units) if data != 'OK': raise weewx.WeeWxIOError("Failed to set units to %s: %s" % - (units, data)) + (units, _fmt(data))) def get_interval(self): data = self.command("LOGINT=?") @@ -527,7 +537,7 @@ class Station(object): data = self.command("LOGINT=%d" % interval) if data != 'OK': raise weewx.WeeWxIOError("Failed to set logging interval: %s" % - data) + _fmt(data)) def get_version(self): data = self.command("VERSION") @@ -607,7 +617,7 @@ if __name__ == '__main__': if options.gettime: print s.get_time() if options.settime: - s.set_time(time.time()) + s.set_time() if options.getint: print s.get_interval() if options.setint: diff --git a/bin/weewx/drivers/fousb.py b/bin/weewx/drivers/fousb.py index bfcc242e..1c07b78a 100644 --- a/bin/weewx/drivers/fousb.py +++ b/bin/weewx/drivers/fousb.py @@ -151,6 +151,20 @@ specified in weewx.conf, and 'temperature' is read from the sensors. The 'barometer' value is reported to wunderground, cwop, etc. +Illuminance and Radiation + +The 30xx stations include a sensor that reports illuminance (lux). The +conversion from lux to radiation is a function of the angle of the sun and +altitude, but this driver uses a single multiplier as an approximation. + +Apparently the display on fine offset stations is incorrect. The display +reports radiation with a lux-to-W/m^2 multiplier of 0.001464. Apparently +Cumulus and WeatherDisplay use a multiplier of 0.0079. The multiplier for +sea level with sun directly overhead is 0.01075. + +This driver uses the sea level multiplier of 0.01075. Use an entry in +StdCalibrate to adjust this for your location and altitude. + From Jim Easterbrook: The weather station memory has two parts: a "fixed block" of 256 bytes @@ -260,7 +274,7 @@ keymap = { 'windDir' : ('wind_dir', 22.5), # station is 0-15, weewx wants deg 'windGustDir' : ('wind_dir', 22.5), # station is 0-15, weewx wants deg 'rain' : ('rain', 0.1), # station is mm, weewx wants cm - 'radiation' : ('illuminance', 0.001464), # lux, weewx wants W/m^2 + 'radiation' : ('illuminance', 0.01075), # lux, weewx wants W/m^2 'UV' : ('uv', 1.0), 'dewpoint' : ('dewpoint', 1.0), 'heatindex' : ('heatindex', 1.0), @@ -410,9 +424,10 @@ def pywws2weewx(p, ts, pressure_offset, altitude, USB_RT_PORT = (usb.TYPE_CLASS | usb.RECIP_OTHER) USB_PORT_FEAT_POWER = 8 -def power_cycle_station(self, hub, port): +def power_cycle_station(hub, port): '''Power cycle the port on the specified hub. This works only with USB hubs that support per-port power switching such as the linksys USB2HUB4.''' + loginf("Attempting to power cycle") busses = usb.busses() if not busses: raise weewx.WeeWxIOError("Power cycle failed: cannot find USB busses") @@ -432,14 +447,18 @@ def power_cycle_station(self, hub, port): request=usb.REQ_CLEAR_FEATURE, value=USB_PORT_FEAT_POWER, index=port, buffer=None, timeout=1000) - time.sleep(10) + loginf("Waiting 30 seconds for station to power down") + time.sleep(30) loginf("Power on port %d on hub %s" % (port, hub)) handle.controlMsg(requestType=USB_RT_PORT, request=usb.REQ_SET_FEATURE, value=USB_PORT_FEAT_POWER, index=port, buffer=None, timeout=1000) + loginf("Waiting 60 seconds for station to power up") + time.sleep(60) finally: del handle + loginf("Power cycle complete") # decode weather station raw data formats def _signed_byte(raw, offset): @@ -658,6 +677,7 @@ class FineOffsetUSB(weewx.abstractstation.AbstractStation): # minimum interval between polling for data change self.min_pause = 0.5 + self.devh = None self._arcint = None self._last_rain_loop = None self._last_rain_ts_loop = None @@ -702,27 +722,39 @@ class FineOffsetUSB(weewx.abstractstation.AbstractStation): def archive_interval(self): return self._archive_interval_minutes() * 60 + # if power cycling is enabled, loop forever until we get a response from + # the weather station. def _archive_interval_minutes(self): - if self._arcint is None: - for i in range(self.max_tries): + if self._arcint is not None: + return self._arcint + if self.pc_hub is not None: + while True: try: - self._arcint = self.get_fixed_block(['read_period']) + self.openPort() + self._get_arcint() break - except usb.USBError, e: - logcrt("get archive interval failed attempt %d of %d: %s" - % (i+1, self.max_tries, e)) - else: - msg = "Unable to read archive interval after %d tries" % self.max_tries - if self.pc_hub is not None: - logerr(msg) - logerr("Attempting to power cycle") + except weewx.WeeWxIOError, e: + self.closePort() power_cycle_station(self.pc_hub, self.pc_port) - raise weewx.WeeWxIOError("Power cycle complete") - else: - raise weewx.WeeWxIOError(msg) + else: + self._get_arcint() return self._arcint + def _get_arcint(self): + for i in range(self.max_tries): + try: + self._arcint = self.get_fixed_block(['read_period']) + return + except usb.USBError, e: + logcrt("get archive interval failed attempt %d of %d: %s" + % (i+1, self.max_tries, e)) + else: + raise weewx.WeeWxIOError("Unable to read archive interval after %d tries" % self.max_tries) + def openPort(self): + if self.devh is not None: + return + dev = self._find_device() if not dev: logcrt("Cannot find USB device with Vendor=0x%04x ProdID=0x%04x Device=%s" % (self.vendor_id, self.product_id, self.device_id)) @@ -770,8 +802,8 @@ class FineOffsetUSB(weewx.abstractstation.AbstractStation): # def getTime(self): # return self.get_clock() -# def setTime(self, ts): -# self.set_clock(ts) +# def setTime(self): +# self.set_clock() def genLoopPackets(self): """Generator function that continuously returns decoded packets.""" diff --git a/bin/weewx/drivers/simulator.py b/bin/weewx/drivers/simulator.py index 614e5c8a..4d45eb02 100644 --- a/bin/weewx/drivers/simulator.py +++ b/bin/weewx/drivers/simulator.py @@ -186,7 +186,7 @@ class Rain(object): n_rain_packets = total_rain / Rain.bucket_tip self.period = int(npackets/n_rain_packets) self.rain_start = 3600* rain_start - self.rain_end = rain_start + 3600 * rain_length + self.rain_end = self.rain_start + 3600 * rain_length self.packet_number = 0 def value_at(self, time_ts): diff --git a/bin/weewx/drivers/te923.py b/bin/weewx/drivers/te923.py index 4081d336..80eb8b72 100644 --- a/bin/weewx/drivers/te923.py +++ b/bin/weewx/drivers/te923.py @@ -161,7 +161,7 @@ import weewx.abstractstation import weewx.units import weewx.wxformulas -DRIVER_VERSION = '0.9' +DRIVER_VERSION = '0.10' DEBUG_READ = 0 DEBUG_DECODE = 0 DEBUG_PRESSURE = 0 @@ -674,7 +674,7 @@ class BadRead(weewx.WeeWxIOError): """Bogus data length, CRC, header block, or other read failure""" class Station(object): - ENDPOINT_IN = 0x01 + ENDPOINT_IN = 0x81 READ_LENGTH = 0x8 def __init__(self, vendor_id=0x1130, product_id=0x6801, diff --git a/bin/weewx/drivers/ultimeter.py b/bin/weewx/drivers/ultimeter.py index 3ec60072..e55c31ec 100644 --- a/bin/weewx/drivers/ultimeter.py +++ b/bin/weewx/drivers/ultimeter.py @@ -14,10 +14,44 @@ # # See http://www.gnu.org/licenses/ -"""Driver for Peet Bros Ultimeter weather stations (except the Ultimeter II). +"""Driver for Peet Bros Ultimeter weather stations (based on the +ADS WS1 driver) except the Ultimeter II (now quite old from early 1990s). -This driver assumes the Ultimeter is emitting data in Peet Bros Data Logger -mode format. +Thanks to Steve (sesykes71) for the testing that made this driver possible. + +Thanks to Jay Nugent (WB8TKL) and KRK6 for weather-2.kr6k-V2.1 + + http://server1.nuge.com/~weather/ + +To use this driver, put this file in bin/user, then put this in weewx.conf: + +[Station] + ... + station_type = PeetBros + +[PeetBros] + port = /dev/ttyS0 + driver = user.peetbros + +The driver assumes the Ultimeter is emitting data in Peet Bros Data Logger +mode format: + +!!000000BE02EB000027700000023A023A0025005800000000 + SSSSXXDDTTTTLLLLPPPPttttHHHHhhhhddddmmmmRRRRWWWW + +SSSS - wind speed (0.1 km/h) +XX - wind direction calibration +DD - wind direction (0-255) +TTTT - outdoor temperature (0.1 F) +LLLL - long term rain (0.01 in) +PPPP - pressure (0.1 mbar) +tttt - indoor temperature (0.1 F) +HHHH - outdoor humidity (0.1 %) +hhhh - indoor humidity (0.1 %) +dddd - date (day of year) +mmmm - time (minute of day) +RRRR - daily rain (0.01 in) +WWWW - one minute wind average (0.1 km/h) Resources for the Ultimeter stations @@ -27,9 +61,6 @@ Ultimeter Models 2100, 2000, 800, & 100 serial specifications: Ultimeter 2000 Pinouts and Parsers: http://www.webaugur.com/ham-radio/52-ultimeter-2000-pinouts-and-parsers.html -Ultimeter II: - not supported by this driver - All models communicate over an RS-232 compatible serial port using three wires--RXD, TXD, and Ground (except Ultimeter II which omits TXD). Port parameters are 2400, 8N1, with no flow control. @@ -40,17 +71,16 @@ and time of the Ultimeter upon initialization and then sets it into Data Logger mode for continuous updates. Modem Mode commands used by the driver - >Addddmmmm Set Date and Time (decimal digits dddd = day of year, mmmm = minute of day; Jan 1 = 0000, Midnight = 0000) >I Set output mode to Data Logger Mode (continuous output) + """ -# FIXME: eliminate the weewx.XXX classes from the station level - from __future__ import with_statement +import optparse import serial import syslog import time @@ -61,16 +91,20 @@ import weewx.units import weewx.uwxutils import weewx.wxformulas -INHG_PER_MBAR = 0.0295333727 -METER_PER_FOOT = 0.3048 -MILE_PER_KM = 0.621371 - -DRIVER_VERSION = '0.11.0' +DRIVER_VERSION = '0.9.4' DEFAULT_PORT = '/dev/ttyS0' -DEBUG_READ = 1 +DEBUG_READ = 0 + +def _is_hex(c): + """Test character for a valid hexadecimal digit.""" + try: + int(c, 16) + return True + except ValueError: + return False def logmsg(level, msg): - syslog.syslog(level, 'ultimeter: %s' % msg) + syslog.syslog(level, 'peetbros: %s' % msg) def logdbg(msg): logmsg(syslog.LOG_DEBUG, msg) @@ -81,10 +115,14 @@ def loginf(msg): def logerr(msg): logmsg(syslog.LOG_ERR, msg) +def logcrt(msg): + logmsg(syslog.LOG_CRIT, msg) + def loader(config_dict, engine): """Get the altitude, in feet, from the Station section of the dict.""" altitude_m = weewx.units.getAltitudeM(config_dict) - altitude_ft = altitude_m / METER_PER_FOOT + altitude_vt = (altitude_m, 'meter', 'group_altitude') + altitude_ft = weewx.units.convert(altitude_vt, 'foot')[0] station = Ultimeter(altitude=altitude_ft, **config_dict['Ultimeter']) return station @@ -94,9 +132,6 @@ class Ultimeter(weewx.abstractstation.AbstractStation): port - serial port [Required. Default is /dev/ttyS0] - model - station model, e.g., 'Ultimeter 2000' or 'Ultimeter 100' - [Optional. Default is Ultimeter] - polling_interval - how often to query the serial interface, seconds [Optional. Default is 1] @@ -113,65 +148,16 @@ class Ultimeter(weewx.abstractstation.AbstractStation): self.polling_interval = float(stn_dict.get('polling_interval', 1)) self.max_tries = int(stn_dict.get('max_tries', 5)) self.pressure_offset = float(stn_dict.get('pressure_offset', 0)) - self.model = stn_dict.get('model', 'Ultimeter') self.last_rain = None + self.last_rain_ts = None loginf('driver version is %s' % DRIVER_VERSION) loginf('using serial port %s' % self.port) - loginf('polling interval is %s' % self.polling_interval) - loginf('pressure offset is %s' % self.pressure_offset) + loginf('polling interval is %s' % str(self.polling_interval)) global DEBUG_READ DEBUG_READ = int(stn_dict.get('debug_read', DEBUG_READ)) - def getTime(self): - with Station(self.port) as station: - return station.get_time() - - def setTime(self, ts): - with Station(self.port) as station: - station.set_time(ts) - def genLoopPackets(self): - return self.glp1() - - def glp1(self): - # this version of genLoopPackets does the maxtries at station level - # and keeps the serial port open for an extended time. - with Station(self.port) as station: - station.set_logger_mode() - while True: - buf = station.get_readings_with_retry() - data = Station.parse_readings(buf) - packet = {'dateTime': int(time.time()+0.5), - 'usUnits' : weewx.US } - packet.update(data) - self._augment_packet(packet) - yield packet - if self.polling_interval: - time.sleep(self.polling_interval) - - def glp2(self): - # this version of genLoopPackets does the maxtries at station level - # and opens the serial port for each data read. - with Station(self.port) as station: - station.set_logger_mode() - while True: - with Station(self.port) as station: - buf = station.get_readings_with_retry() - data = Station.parse_readings(buf) - packet = {'dateTime': int(time.time()+0.5), - 'usUnits' : weewx.US } - packet.update(data) - self._augment_packet(packet) - yield packet - if self.polling_interval: - time.sleep(self.polling_interval) - - def glp3(self): - # this version of genLoopPackets does the maxtries at driver level - # and opens the serial port for each data read. ntries = 0 - with Station(self.port) as station: - station.set_logger_mode() while ntries < self.max_tries: ntries += 1 try: @@ -179,8 +165,8 @@ class Ultimeter(weewx.abstractstation.AbstractStation): 'usUnits' : weewx.US } # open a new connection to the station for each reading with Station(self.port) as station: - buf = station.get_readings() - data = Station.parse_readings(buf) + bytes = station.get_readings() + data = Station.parse_readings(bytes) packet.update(data) self._augment_packet(packet) ntries = 0 @@ -197,17 +183,14 @@ class Ultimeter(weewx.abstractstation.AbstractStation): @property def hardware_name(self): - return self.model + return Station.getName() def _augment_packet(self, packet): """add derived metrics to a packet""" - # the ultimeter appears to report a (calculated) sea-level pressure - # rather than a raw station (sensor) pressure. so we must - # back-calculate to find the station pressure. adjp = packet['barometer'] if self.pressure_offset is not None and adjp is not None: - adjp += self.pressure_offset * INHG_PER_MBAR # convert to inHg - # FIXME: second temperature should be 12-hour mean temperature + adjp += self.pressure_offset * 0.0295333727 # convert to inHg + # FIXME: this is supposed to use mean temperature packet['pressure'] = weewx.uwxutils.TWxUtilsUS.SeaLevelToStationPressure(adjp, self.altitude, packet['outTemp'], packet['outTemp'], packet['outHumidity']) packet['altimeter'] = weewx.wxformulas.altimeter_pressure_US( packet['pressure'], self.altitude, algorithm='aaNOAA') @@ -225,31 +208,10 @@ class Ultimeter(weewx.abstractstation.AbstractStation): packet['rain'] = None self.last_rain = packet['long_term_rain'] - # no wind direction when wind speed is zero - if not packet['windSpeed']: - packet['windDir'] = None - -def _is_valid_char(c): - '''See whether a character is a valid hexadecimal digit or hyphen.''' - if c == '-': - return True - try: - int(c, 16) - return True - except ValueError: - return False - -def _hex2int(s, multiplier=None): - '''Ultimeter puts hyphens in the string when a sensor is not installed. - When we get a hyphen or any other non-hex character, return None.''' - v = None - try: - v = int(s, 16) - if multiplier is not None: - v *= multiplier - except ValueError: - pass - return v + # calculate the rain rate + packet['rainRate'] = weewx.wxformulas.calculate_rain_rate( + packet['rain'], packet['dateTime'], self.last_rain_ts) + self.last_rain_ts = packet['dateTime'] class Station(object): def __init__(self, port): @@ -257,7 +219,6 @@ class Station(object): self.baudrate = 2400 self.timeout = 30 self.serial_port = None - self.max_tries = 5 def __enter__(self): self.open() @@ -266,15 +227,30 @@ class Station(object): def __exit__(self, type, value, traceback): self.close() + @staticmethod + def getName(self): + return "Ultimeter" + def open(self): logdbg("open serial port %s" % self.port) self.serial_port = serial.Serial(self.port, self.baudrate, timeout=self.timeout) -# self.set_logger_mode() + + # Set date and time as internal clock skews. + self.serial_port.write(">A%04d%04d\r" + % (time.localtime().tm_yday - 1, time.localtime().tm_min + + time.localtime().tm_hour * 60)) + + # Set to Data Logger Mode + self.serial_port.write(">I\r") def close(self): if self.serial_port is not None: logdbg("close serial port %s" % self.port) + + # Set to Modem Mode (stops Data Logger output) + self.serial_port.write(">\r") + self.serial_port.close() self.serial_port = None @@ -285,7 +261,7 @@ class Station(object): raise weewx.WeeWxIOError(e) n = len(buf) if n != nchar: - if DEBUG_READ and n: + if DEBUG_READ: logdbg("partial buffer: '%s'" % ' '.join(["%0.2X" % ord(c) for c in buf])) raise weewx.WeeWxIOError("Read expected %d chars, got %d" % @@ -298,86 +274,37 @@ class Station(object): raise weewx.WeeWxIOError("Write expected %d chars, sent %d" % (len(data), n)) - def flush(self): - logdbg("flush serial buffer") - self.serial_port.flushInput() - - def get_time(self): - self.set_logger_mode() - buf = self.get_readings_with_retry() - data = Station.parse_readings(buf) - d = data['day_of_year'] - m = data['minute_of_day'] - tstr = time.localtime() - y = tstr.tm_year - s = tstr.tm_sec - ts = time.mktime((y,0,0,0,0,s,0,0,0)) + d * 86400 + m * 60 - return ts - - def set_time(self, ts): - self.set_modem_mode() - tstr = time.localtime(ts) - tcmd = ">A%04d%04d\r" % ( - tstr.tm_yday - 1, tstr.tm_min + tstr.tm_hour * 60) - logdbg("set station time to %d (%s)" % (ts, tcmd)) - self.write(tcmd) - - # year works only for models 2004 and later - y = tstr.tm_year - ycmd = ">U%s" % y - logdbg("set station year to %s (%s)" % (y, ycmd)) - self.write(ycmd) - - def set_logger_mode(self): - # in logger mode, station sends logger mode records continuously - logdbg("set station to logger mode") - self.write(">I\r") - - def set_modem_mode(self): - # modem mode is available only on models 2004 and later - # not available on pre-2004 models 50/100/500/700/800 - logdbg("set station to modem mode") - self.write(">\r") - - def get_readings_with_retry(self): - ntries = 0 - while ntries < self.max_tries: - ntries += 1 - try: - return self.get_readings() - except weewx.WeeWxIOError, e: - logerr("Failed attempt %d of %d to get readings: %s" % - (ntries, self.max_tries, e)) - else: - msg = "Max retries (%d) exceeded for readings" % self.max_tries - logerr(msg) - raise weewx.RetriesExceeded(msg) - return [] - def get_readings(self): - buf = [] + bytes = [] while True: c = self.read(1) if c == "\r" or c == "\n": break - elif c == '!' and len(buf) > 0: + elif c == '!' and len(bytes) > 0: break elif c == '!': - buf = [] - elif _is_valid_char(c): - buf.append(c) + bytes = [] + elif c == '-': + # Ultimeter may put hyphens in the string if a sensor + # is not installed. Make the reading zero instead. + bytes.append('0') + elif _is_hex(c) is True: + # Ultimeter uses hexadecimal characters for its values. + # Guard against garbage. + bytes.append(c) else: - raise weewx.WeeWxIOError("Invalid character %0.2X" % ord(c)) + bytes = [] if DEBUG_READ: - logdbg("bytes: '%s'" % ' '.join(["%0.2X" % ord(c) for c in buf])) - if len(buf) != 48: - raise weewx.WeeWxIOError("Got %d bytes, expected 48" % len(buf)) - return ''.join(buf) + logdbg("bytes: '%s'" % ' '.join(["%0.2X" % ord(c) for c in bytes])) + if len(bytes) != 48: + raise weewx.WeeWxIOError("Got %d bytes, expected 48" % len(bytes)) + return ''.join(bytes) @staticmethod - def parse_readings(buf): - '''Parse the bytes in data logger format. Each line has 2 header - bytes, 48 data bytes, and a carriage return and newline: + def parse_readings(bytes): + '''Ultimeter stations emit data in PeetBros format. Each line has 52 + characters - 2 header bytes, 48 data bytes, and a carriage return + and line feed (new line): !!000000BE02EB000027700000023A023A0025005800000000\r\n SSSSXXDDTTTTLLLLPPPPttttHHHHhhhhddddmmmmRRRRWWWW @@ -396,24 +323,30 @@ class Station(object): RRRR - daily rain (0.01 in) WWWW - one minute wind average (0.1 kph) + For date, time, and other non-standard readings use labels that + will not interfere with weewx/wview conventions. + "pressure" reported by the Ultimeter 2000 is correlated to the local official barometer reading as part of the setup of the station console so this value is assigned to the 'barometer' key and the pressure and altimeter values are calculated from it. + + My Ultimeter 2000 puts hyphens, '-', in the place of the indoor + humidity (hhhh) since there is no indoor humidty sensor installed. + The driver will identify the hyphens and replace them with the '0' + character. ''' data = {} - data['windSpeed'] = _hex2int(buf[0:4], 0.1 * MILE_PER_KM) # mph - data['windDir'] = _hex2int(buf[6:8], 1.411764) # compass degrees - data['outTemp'] = _hex2int(buf[8:12], 0.1) # degree_F - data['long_term_rain'] = _hex2int(buf[12:16], 0.01) # inch - data['barometer'] = _hex2int(buf[16:20], 0.1 * INHG_PER_MBAR) # inHg - data['inTemp'] = _hex2int(buf[20:24], 0.1) # degree_F - data['outHumidity'] = _hex2int(buf[24:28], 0.1) # percent - data['inHumidity'] = _hex2int(buf[28:32], 0.1) # percent - data['day_of_year'] = _hex2int(buf[32:36]) - data['minute_of_day'] = _hex2int(buf[36:40]) - data['daily_rain'] = _hex2int(buf[40:44], 0.01) # inch - data['wind_average'] = _hex2int(buf[44:48], 0.1 * MILE_PER_KM) # mph + data['windSpeed'] = int(bytes[0:4], 16) * 0.1 * 0.621371 # mph + data['windDir'] = int(bytes[6:8], 16) * 1.411764 # compass degrees + data['outTemp'] = int(bytes[8:12], 16) * 0.1 # degree_F + data['long_term_rain'] = int(bytes[12:16], 16) * 0.01 # inch + data['barometer'] = int(bytes[16:20], 16) * 0.1 * 0.0295333727 # inHg + data['inTemp'] = int(bytes[20:24], 16) * 0.1 # degree_F + data['outHumidity'] = int(bytes[24:28], 16) * 0.1 # percent + data['inHumidity'] = int(bytes[28:32], 16) * 0.1 # percent + data['daily_rain'] = int(bytes[40:44], 16) * 0.01 # inch + data['wind_average'] = int(bytes[44:48], 16) * 0.1 * 0.621371 # mph return data # define a main entry point for basic testing of the station without weewx @@ -422,7 +355,6 @@ class Station(object): # PYTHONPATH=bin python bin/weewx/drivers/ultimeter.py if __name__ == '__main__': - import optparse usage = """%prog [options] [--help]""" @@ -435,26 +367,14 @@ if __name__ == '__main__': parser.add_option('--port', dest='port', metavar='PORT', help='serial port to which the station is connected', default=DEFAULT_PORT) - parser.add_option('--get-current', dest='getcur', action='store_true', - help='display current readings') - parser.add_option('--get-time', dest='gettime', action='store_true', - help='display station time') (options, args) = parser.parse_args() if options.version: - print "PeetBros Ultimeter driver version %s" % DRIVER_VERSION + print "ultimeter driver version %s" % DRIVER_VERSION exit(0) with Station(options.port) as s: - if options.getcur: - s.set_logger_mode() - buf = s.get_readings_with_retry() - print buf - data = Station.parse_readings(buf) - print data - if options.gettime: - ts = s.get_time() - print ts + print s.get_readings() if __name__ == '__main__': main() diff --git a/bin/weewx/drivers/vantage.py b/bin/weewx/drivers/vantage.py index cecd5825..b0509d12 100644 --- a/bin/weewx/drivers/vantage.py +++ b/bin/weewx/drivers/vantage.py @@ -637,24 +637,26 @@ class Vantage(weewx.abstractstation.AbstractStation): syslog.syslog(syslog.LOG_ERR, "vantage: Max retries exceeded while getting time") raise weewx.RetriesExceeded("While getting console time") - def setTime(self, newtime_ts): - """Set the clock on the Davis Vantage console + def setTime(self): + """Set the clock on the Davis Vantage console""" - newtime_ts: The time the internal clock should be set to in unix epoch time.""" - - # Unfortunately, this algorithm takes a little while to execute, so the clock - # usually ends up a few hundred milliseconds slow - newtime_tt = time.localtime(int(newtime_ts + 0.5)) - - # The Davis expects the time in reversed order, and the year is since 1900 - _buffer = struct.pack("= WS28xx.max_records: + nextIdx -= WS28xx.max_records + return nextIdx + +def tstr_to_ts(tstr): + if tstr is None: + return None + return time.mktime(time.strptime(tstr, "%Y-%m-%d %H:%M:%S")) + +def bytes_to_addr(a,b,c): + return ((((a & 0xF) << 8) | b) << 8) | c + +def addr_to_index(addr): + return (addr - 416) / 18 + +def index_to_addr(idx): + return 18 * idx + 416 + def loader(config_dict, engine): altitude_m = weewx.units.getAltitudeM(config_dict) station = WS28xx(altitude=altitude_m, **config_dict['WS28xx']) @@ -937,7 +1040,9 @@ def loader(config_dict, engine): class WS28xx(weewx.abstractstation.AbstractStation): """Driver for LaCrosse WS28xx stations.""" - + + max_records = 1797 + def __init__(self, **stn_dict) : """Initialize the station object. @@ -965,12 +1070,6 @@ class WS28xx(weewx.abstractstation.AbstractStation): comm_interval: Communications mode interval [Optional. Default is 3] - vendor_id: The USB vendor ID for the transceiver. - [Optional. Default is 6666] - - product_id: The USB product ID for the transceiver. - [Optional. Default is 5555] - device_id: The USB device ID for the transceiver. If there are multiple devices with the same vendor and product IDs on the bus, each will have a unique device identifier. Use this identifier @@ -990,14 +1089,15 @@ class WS28xx(weewx.abstractstation.AbstractStation): self.polling_interval = int(stn_dict.get('polling_interval', 30)) self.comm_interval = int(stn_dict.get('comm_interval', 3)) self.frequency = stn_dict.get('transceiver_frequency', 'US') - self.vendor_id = int(stn_dict.get('vendor_id', '0x6666'), 0) - self.product_id = int(stn_dict.get('product_id', '0x5555'), 0) self.device_id = stn_dict.get('device_id', None) self.serial = stn_dict.get('serial', None) self.pressure_offset = stn_dict.get('pressure_offset', None) if self.pressure_offset is not None: self.pressure_offset = float(self.pressure_offset) + self.vendor_id = 0x6666 + self.product_id = 0x5555 + now = int(time.time()) self._service = None self._last_rain = None @@ -1078,7 +1178,89 @@ class WS28xx(weewx.abstractstation.AbstractStation): yield packet time.sleep(self.polling_interval) + # FIXME: return records as they come in instead of all at the end + # FIXME: compare station timestamp with computer timestamp to ensure + # that timestamps on historical records are correct + def genStartupRecords(self, ts): + loginf('Scanning historical records') + maxtries = 3 + ntries = 0 + last_n = n = nrem = None + last_ts = now = int(time.time()) + self.start_caching_history(since_ts=ts) + t = WS28xx.max_records + while nrem is None or nrem > 0: + if ntries >= maxtries: + logerr('No historical data after %d tries' % ntries) + return + time.sleep(60) + now = int(time.time()) + n = self.get_num_history_scanned() + if n == last_n: + ntries += 1 + dur = now - last_ts + loginf('No data after %d seconds (press SET to sync)' % dur) + else: + ntries = 0 + last_ts = now + last_n = n + nrem = self.get_uncached_history_count() + ni = self.get_next_history_index() + li = self.get_latest_history_index() + loginf("Scanned %s of %s records: current=%s latest=%s rem=%s" % + (n, t, ni, li, nrem)) + self.stop_caching_history() + records = self.get_history_cache_records() + self.clear_history_cache() + loginf('Found %d historical records' % len(records)) + last_ts = None + last_rain = None + for r in records: + r['dateTime'] = tstr_to_ts(r['time']) + if last_ts is not None: + r['usUnits'] = weewx.METRIC + r['interval'] = (r['dateTime'] - last_ts) / 60 + # FIXME: put these into a separate function + rain_total = r['rainTotal'] + delta = weewx.wxformulas.calculate_rain(rain_total, last_rain) + last_rain = rain_total + r['rain'] = delta + if r['rain'] is not None: + r['rain'] /= 10 # weewx wants cm + r['heatindex'] = weewx.wxformulas.heatindexC( + r['outTemp'], r['outHumidity']) + r['dewpoint'] = weewx.wxformulas.dewpointC( + r['outTemp'], r['outHumidity']) + r['windchill'] = weewx.wxformulas.windchillC( + r['outTemp'], r['windSpeed']) + adjp = r['pressure'] + if self.pressure_offset is not None and adjp is not None: + adjp += self.pressure_offset + r['barometer'] = weewx.wxformulas.sealevel_pressure_Metric( + adjp, self.altitude, r['outTemp']) + r['altimeter'] = weewx.wxformulas.altimeter_pressure_Metric( + adjp, self.altitude, algorithm='aaNOAA') + del r['time'] + yield r + last_ts = r['dateTime'] + last_rain = r['rainTotal'] + +# FIXME: do not implement hardware record generation until we figure +# out how to query the historical records faster. # def genArchiveRecords(self, since_ts): +# pass + +# FIXME: implement retries for this so that rf thread has time to get +# configuration data from the station +# @property +# def archive_interval(self): +# cfg = self.get_config() +# return getHistoryInterval(cfg['history_interval']) * 60 + +# FIXME: implement set/get time +# def setTime(self): +# pass +# def getTime(self): # pass def startUp(self): @@ -1096,10 +1278,19 @@ class WS28xx(weewx.abstractstation.AbstractStation): self._service = None def transceiver_is_present(self): - return self._service.transceiverIsPresent() + return self._service.DataStore.getTransceiverPresent() def transceiver_is_paired(self): - return self._service.transceiverIsRegistered() + return self._service.DataStore.getDeviceRegistered() + + def get_transceiver_serial(self): + return self._service.DataStore.getTransceiverSerNo() + + def get_transceiver_id(self): + return self._service.DataStore.getDeviceID() + + def get_last_contact(self): + return self._service.getLastStat().last_seen_ts def get_observation(self): data = self._service.getWeatherData() @@ -1110,7 +1301,7 @@ class WS28xx(weewx.abstractstation.AbstractStation): # add elements required for weewx LOOP packets packet = {} packet['usUnits'] = weewx.METRIC - packet['dateTime'] = int(ts + 0.5) + packet['dateTime'] = ts # data from the station sensors packet['inTemp'] = get_datum_diff(data._TempIndoor, @@ -1193,22 +1384,38 @@ class WS28xx(weewx.abstractstation.AbstractStation): def get_config(self): logdbg('get station configuration') cfg = self._service.getConfigData().asDict() - if cfg['checksum_device'] == 0: + cs = cfg.get('checksum_out') + if cs is None or cs == 0: return None return cfg - def get_history(self): - logdbg('get historical records') - return self._service.getHistoryData().asDict() + def start_caching_history(self, since_ts=0, num_rec=0): + self._service.startCachingHistory(since_ts) - def get_last_contact(self): - return self._service.getLastStat().last_seen_ts + def stop_caching_history(self): + self._service.stopCachingHistory() - def get_transceiver_serial(self): - return self._service.DataStore.getTransceiverSerNo() + def get_uncached_history_count(self): + return self._service.getUncachedHistoryCount() - def get_transceiver_id(self): - return self._service.DataStore.getDeviceID() + def get_next_history_index(self): + return self._service.getNextHistoryIndex() + + def get_latest_history_index(self): + return self._service.getLatestHistoryIndex() + + def get_num_history_scanned(self): + return self._service.getNumHistoryScanned() + + def get_history_cache_records(self): + return self._service.getHistoryCacheRecords() + + def clear_history_cache(self): + self._service.clearHistoryCache() + + def set_interval(self, interval): + # FIXME: set the archive interval + pass # The following classes and methods are adapted from the implementation by # eddie de pieri, which is in turn based on the HeavyWeather implementation. @@ -1428,6 +1635,26 @@ def getBatteryStatus(status, flag): return 1 return 0 +history_intervals = { + EHistoryInterval.hi01Min: 1, + EHistoryInterval.hi05Min: 5, + EHistoryInterval.hi10Min: 10, + EHistoryInterval.hi20Min: 20, + EHistoryInterval.hi30Min: 30, + EHistoryInterval.hi60Min: 60, + EHistoryInterval.hi02Std: 120, + EHistoryInterval.hi04Std: 240, + EHistoryInterval.hi06Std: 360, + EHistoryInterval.hi08Std: 480, + EHistoryInterval.hi12Std: 720, + EHistoryInterval.hi24Std: 1440, + } + +def getHistoryInterval(i): + return history_intervals.get(i) + +# NP - not present +# OFL - outside factory limits class CWeatherTraits(object): windDirMap = { 0:"N", 1:"NNE", 2:"NE", 3:"ENE", 4:"E", 5:"ESE", 6:"SE", 7:"SSE", @@ -1507,21 +1734,21 @@ class USBHardware(object): @staticmethod def isOFL2(buf, start, StartOnHiNibble): if StartOnHiNibble : - result = (buf[0][start+0] >> 4) == 15 \ + result = (buf[0][start+0] >> 4) == 15 \ or (buf[0][start+0] & 0xF) == 15 else: - result = (buf[0][start+0] & 0xF) == 15 \ + result = (buf[0][start+0] & 0xF) == 15 \ or (buf[0][start+1] >> 4) == 15 return result @staticmethod def isOFL3(buf, start, StartOnHiNibble): if StartOnHiNibble : - result = (buf[0][start+0] >> 4) == 15 \ + result = (buf[0][start+0] >> 4) == 15 \ or (buf[0][start+0] & 0xF) == 15 \ or (buf[0][start+1] >> 4) == 15 else: - result = (buf[0][start+0] & 0xF) == 15 \ + result = (buf[0][start+0] & 0xF) == 15 \ or (buf[0][start+1] >> 4) == 15 \ or (buf[0][start+1] & 0xF) == 15 return result @@ -1529,13 +1756,13 @@ class USBHardware(object): @staticmethod def isOFL5(buf, start, StartOnHiNibble): if StartOnHiNibble : - result = (buf[0][start+0] >> 4) == 15 \ + result = (buf[0][start+0] >> 4) == 15 \ or (buf[0][start+0] & 0xF) == 15 \ or (buf[0][start+1] >> 4) == 15 \ or (buf[0][start+1] & 0xF) == 15 \ or (buf[0][start+2] >> 4) == 15 else: - result = (buf[0][start+0] & 0xF) == 15 \ + result = (buf[0][start+0] & 0xF) == 15 \ or (buf[0][start+1] >> 4) == 15 \ or (buf[0][start+1] & 0xF) == 15 \ or (buf[0][start+2] >> 4) == 15 \ @@ -1545,12 +1772,12 @@ class USBHardware(object): @staticmethod def isErr2(buf, start, StartOnHiNibble): if StartOnHiNibble : - result = (buf[0][start+0] >> 4) >= 10 \ + result = (buf[0][start+0] >> 4) >= 10 \ and (buf[0][start+0] >> 4) != 15 \ or (buf[0][start+0] & 0xF) >= 10 \ and (buf[0][start+0] & 0xF) != 15 else: - result = (buf[0][start+0] & 0xF) >= 10 \ + result = (buf[0][start+0] & 0xF) >= 10 \ and (buf[0][start+0] & 0xF) != 15 \ or (buf[0][start+1] >> 4) >= 10 \ and (buf[0][start+1] >> 4) != 15 @@ -1559,14 +1786,14 @@ class USBHardware(object): @staticmethod def isErr3(buf, start, StartOnHiNibble): if StartOnHiNibble : - result = (buf[0][start+0] >> 4) >= 10 \ + result = (buf[0][start+0] >> 4) >= 10 \ and (buf[0][start+0] >> 4) != 15 \ or (buf[0][start+0] & 0xF) >= 10 \ and (buf[0][start+0] & 0xF) != 15 \ or (buf[0][start+1] >> 4) >= 10 \ and (buf[0][start+1] >> 4) != 15 else: - result = (buf[0][start+0] & 0xF) >= 10 \ + result = (buf[0][start+0] & 0xF) >= 10 \ and (buf[0][start+0] & 0xF) != 15 \ or (buf[0][start+1] >> 4) >= 10 \ and (buf[0][start+1] >> 4) != 15 \ @@ -1577,7 +1804,7 @@ class USBHardware(object): @staticmethod def isErr5(buf, start, StartOnHiNibble): if StartOnHiNibble : - result = (buf[0][start+0] >> 4) >= 10 \ + result = (buf[0][start+0] >> 4) >= 10 \ and (buf[0][start+0] >> 4) != 15 \ or (buf[0][start+0] & 0xF) >= 10 \ and (buf[0][start+0] & 0xF) != 15 \ @@ -1588,7 +1815,7 @@ class USBHardware(object): or (buf[0][start+2] >> 4) >= 10 \ and (buf[0][start+2] >> 4) != 15 else: - result = (buf[0][start+0] & 0xF) >= 10 \ + result = (buf[0][start+0] & 0xF) >= 10 \ and (buf[0][start+0] & 0xF) != 15 \ or (buf[0][start+1] >> 4) >= 10 \ and (buf[0][start+1] >> 4) != 15 \ @@ -1628,10 +1855,10 @@ class USBHardware(object): def toRain_7_3(buf, start, StartOnHiNibble): '''read 7 nibbles, presentation with 3 decimals; units of mm''' if ( USBHardware.isErr2(buf, start+0, StartOnHiNibble) or - USBHardware.isErr5(buf, start+1, StartOnHiNibble)): + USBHardware.isErr5(buf, start+1, StartOnHiNibble)): result = CWeatherTraits.RainNP() elif ( USBHardware.isOFL2(buf, start+0, StartOnHiNibble) or - USBHardware.isOFL5(buf, start+1, StartOnHiNibble) ): + USBHardware.isOFL5(buf, start+1, StartOnHiNibble) ): result = CWeatherTraits.RainOFL() elif StartOnHiNibble: result = (buf[0][start+0] >> 4)* 1000 \ @@ -1655,12 +1882,12 @@ class USBHardware(object): def toRain_6_2(buf, start, StartOnHiNibble): '''read 6 nibbles, presentation with 2 decimals; units of mm''' if ( USBHardware.isErr2(buf, start+0, StartOnHiNibble) or - USBHardware.isErr2(buf, start+1, StartOnHiNibble) or - USBHardware.isErr2(buf, start+2, StartOnHiNibble) ): + USBHardware.isErr2(buf, start+1, StartOnHiNibble) or + USBHardware.isErr2(buf, start+2, StartOnHiNibble) ): result = CWeatherTraits.RainNP() elif ( USBHardware.isOFL2(buf, start+0, StartOnHiNibble) or - USBHardware.isOFL2(buf, start+1, StartOnHiNibble) or - USBHardware.isOFL2(buf, start+2, StartOnHiNibble) ): + USBHardware.isOFL2(buf, start+1, StartOnHiNibble) or + USBHardware.isOFL2(buf, start+2, StartOnHiNibble) ): result = CWeatherTraits.RainOFL() elif StartOnHiNibble: result = (buf[0][start+0] >> 4)* 1000 \ @@ -1693,7 +1920,7 @@ class USBHardware(object): result = CWeatherTraits.RainOFL() else: val = USBHardware.toFloat_3_1(buf, start, StartOnHiNibble) - result = val + result = val / 10.0 # mm return result @staticmethod @@ -1707,7 +1934,6 @@ class USBHardware(object): result = (buf[0][start+0] & 0xF)*16**2 \ + (buf[0][start+1] >> 4)* 16**1 \ + (buf[0][start+1] & 0xF)* 16**0 - result = result / 10.0 return result @staticmethod @@ -1933,7 +2159,7 @@ class CCurrentWeatherData(object): return self._checksum def read(self, buf): - self._timestamp = time.time() + self._timestamp = int(time.time() + 0.5) self._checksum = CCurrentWeatherData.calcChecksum(buf) nbuf = [0] @@ -2076,10 +2302,7 @@ class CCurrentWeatherData(object): (self._PressureRelative_hPaMinMax._Min._Value, self._PressureRelative_inHgMinMax._Min._Value) = USBHardware.readPressureShared(nbuf, 205, 1) (self._PressureRelative_hPa, self._PressureRelative_inHg) = USBHardware.readPressureShared(nbuf, 210, 1) - if DEBUG_WEATHER_DATA > 0: - self.logWeatherData() - - def logWeatherData(self): + def toLog(self): logdbg("_WeatherState=%s _WeatherTendency=%s _AlarmRingingFlags %04x" % (CWeatherTraits.forecastMap[self._WeatherState], CWeatherTraits.trendMap[self._WeatherTendency], self._AlarmRingingFlags)) logdbg("_TempIndoor= %8.3f _Min=%8.3f (%s) _Max=%8.3f (%s)" % (self._TempIndoor, self._TempIndoorMinMax._Min._Value, self._TempIndoorMinMax._Min._Time, self._TempIndoorMinMax._Max._Value, self._TempIndoorMinMax._Max._Time)) logdbg("_HumidityIndoor= %8.3f _Min=%8.3f (%s) _Max=%8.3f (%s)" % (self._HumidityIndoor, self._HumidityIndoorMinMax._Min._Value, self._HumidityIndoorMinMax._Min._Time, self._HumidityIndoorMinMax._Max._Value, self._HumidityIndoorMinMax._Max._Time)) @@ -2335,46 +2558,41 @@ class CWeatherStationConfig(object): self._ResetMinMaxFlags = (nbuf[0][43]) <<16 | (nbuf[0][44] << 8) | (nbuf[0][45]) self._InBufCS = (nbuf[0][46] << 8) | nbuf[0][47] self._OutBufCS = calc_checksum(buf, 4, end=39) + 7 - if DEBUG_CONFIG_DATA > 0: - self.logConfigData() - self.write() - ###self._ResetMinMaxFlags = 0x000000 - ###logdbg('set _ResetMinMaxFlags to %06x' % self._ResetMinMaxFlags) """ - #Reset DewpointMax 80 00 00 - #Reset DewpointMin 40 00 00 - #not used 20 00 00 - #Reset WindchillMin* 10 00 00 *Reset dateTime only; Min._Value is preserved + Reset DewpointMax 80 00 00 + Reset DewpointMin 40 00 00 + not used 20 00 00 + Reset WindchillMin* 10 00 00 *dateTime only; Min._Value is preserved - #Reset TempOutMax 08 00 00 - #Reset TempOutMin 04 00 00 - #Reset TempInMax 02 00 00 - #Reset TempInMin 01 00 00 + Reset TempOutMax 08 00 00 + Reset TempOutMin 04 00 00 + Reset TempInMax 02 00 00 + Reset TempInMin 01 00 00 - #Reset Gust 00 80 00 - #not used 00 40 00 - #not used 00 20 00 - #not used 00 10 00 + Reset Gust 00 80 00 + not used 00 40 00 + not used 00 20 00 + not used 00 10 00 - #Reset HumOutMax 00 08 00 - #Reset HumOutMin 00 04 00 - #Reset HumInMax 00 02 00 - #Reset HumInMin 00 01 00 + Reset HumOutMax 00 08 00 + Reset HumOutMin 00 04 00 + Reset HumInMax 00 02 00 + Reset HumInMin 00 01 00 - #not used 00 00 80 - #Reset Rain Total 00 00 40 - #Reset last month? 00 00 20 - #Reset last week? 00 00 10 + not used 00 00 80 + Reset Rain Total 00 00 40 + Reset last month? 00 00 20 + Reset last week? 00 00 10 - #Reset Rain24H 00 00 08 - #Reset Rain1H 00 00 04 - #Reset PresRelMax 00 00 02 - #Reset PresRelMin 00 00 01 + Reset Rain24H 00 00 08 + Reset Rain1H 00 00 04 + Reset PresRelMax 00 00 02 + Reset PresRelMin 00 00 01 """ + #self._ResetMinMaxFlags = 0x000000 + #logdbg('set _ResetMinMaxFlags to %06x' % self._ResetMinMaxFlags) - if DEBUG_CONFIG_DATA > 0: - logdbg('Preset Config data') """ setTemps(self,TempFormat,InTempLo,InTempHi,OutTempLo,OutTempHi) setHums(self,InHumLo,InHumHi,OutHumLo,OutHumHi) @@ -2389,12 +2607,14 @@ class CWeatherStationConfig(object): #self.setGust(EWindspeedFormat.wfKmh,040.0) #self.setRain24H(ERainFormat.rfMm,50.0) - # Preset historyInterval to 5 minutes (default: 2 hours) + # Set historyInterval to 5 minutes (default: 2 hours) self._HistoryInterval = EHistoryInterval.hi05Min # Clear all alarm flags, otherwise the datastream from the weather # station will pause during an alarm and connection will be lost. self._WindDirAlarmFlags = 0x0000 self._OtherAlarmFlags = 0x0000 + + self.write() return 1 def testConfigChanged(self,buf): @@ -2433,7 +2653,7 @@ class CWeatherStationConfig(object): nbuf[0][42] = (self._OutBufCS >> 8) & 0xFF nbuf[0][43] = (self._OutBufCS >> 0) & 0xFF buf[0] = nbuf[0] - if self._OutBufCS == self._InBufCS and self._ResetMinMaxFlags == 0: + if self._OutBufCS == self._InBufCS and self._ResetMinMaxFlags == 0: if DEBUG_CONFIG_DATA > 0: logdbg('testConfigChanged: checksum not changed: OutBufCS=%04x' % self._OutBufCS) changed = 0 @@ -2441,12 +2661,12 @@ class CWeatherStationConfig(object): if DEBUG_CONFIG_DATA > 0: logdbg('testConfigChanged: checksum or resetMinMaxFlags changed: OutBufCS=%04x InBufCS=%04x _ResetMinMaxFlags=%06x' % (self._OutBufCS, self._InBufCS, self._ResetMinMaxFlags)) if DEBUG_CONFIG_DATA > 1: - self.logConfigData() + self.toLog() self.write() changed = 1 return changed - def logConfigData(self): + def toLog(self): logdbg('OutBufCS= %04x' % self._OutBufCS) logdbg('InBufCS= %04x' % self._InBufCS) logdbg('ClockMode= %s' % self._ClockMode) @@ -2554,7 +2774,7 @@ class CWeatherStationConfig(object): } -class CHistoryDataSet(object): +class CHistoryData(object): def __init__(self): self.Time = None @@ -2585,11 +2805,9 @@ class CHistoryDataSet(object): self.PressureRelative = USBHardware.toPressure_hPa_5_1(nbuf, 19, 0) self.TempIndoor = USBHardware.toTemperature_3_1(nbuf, 23, 0) self.TempOutdoor = USBHardware.toTemperature_3_1(nbuf, 22, 1) - self.Time = USBHardware.toDateTime(nbuf, 25, 1, 'HistoryDataSet') - if DEBUG_HISTORY_DATA > 0: - self.logHistoryData() + self.Time = USBHardware.toDateTime(nbuf, 25, 1, 'HistoryData') - def logHistoryData(self): + def toLog(self): logdbg("Time %s" % self.Time) logdbg("TempIndoor= %7.1f" % self.TempIndoor) logdbg("HumidityIndoor= %7.0f" % self.HumidityIndoor) @@ -2603,7 +2821,7 @@ class CHistoryDataSet(object): def asDict(self): return { - 'time': self.Time, + 'time': str(self.Time), 'inTemp': self.TempIndoor, 'inHumidity': self.HumidityIndoor, 'outTemp': self.TempOutdoor, @@ -2615,6 +2833,16 @@ class CHistoryDataSet(object): 'windGust': self.Gust } +class HistoryCache: + def __init__(self): + self.clear_records() + def clear_records(self): + self.since_ts = 0 + self.start_index = None + self.next_index = None + self.records = [] + self.num_outstanding_records = None + self.num_scanned = 0 class CDataStore(object): @@ -2630,25 +2858,12 @@ class CDataStore(object): self.SerialNumber = None self.DeviceID = None - class TCommunicationSettings(object): - def __init__(self): - self.CommModeInterval = 3 - self.PreambleDuration = 5000 - self.RegisterWaitTime = 20000 - self.DeviceID = None - - class TRequest(object): - def __init__(self): - self.Type = ERequestType.rtINVALID - self.State = ERequestState.rsError - self.TTL = 90000 - class TLastStat(object): def __init__(self, cache_file): self.LastBatteryStatus = None self.LastLinkQuality = None - self.OutstandingHistorySets = -1 - self.LastHistoryIndex = 0xffff + self.LastHistoryIndex = None + self.LatestHistoryIndex = None self.last_seen_ts = None self.last_weather_ts = 0 self.last_history_ts = 0 @@ -2657,13 +2872,11 @@ class CDataStore(object): def __init__(self, cache_file): self.cache_file = cache_file self.transceiverPresent = False - - self.Request = CDataStore.TRequest() + self.commModeInterval = 3 + self.registeredDeviceID = None self.LastStat = CDataStore.TLastStat(self.cache_file) - self.CommunicationSettings = CDataStore.TCommunicationSettings() self.TransceiverSettings = CDataStore.TTransceiverSettings() self.StationConfig = CWeatherStationConfig(self.cache_file) - self.HistoryData = CHistoryDataSet() self.CurrentWeather = CCurrentWeatherData() def writeLastStat(self): @@ -2676,6 +2889,7 @@ class CDataStore(object): config['LastStat']['LinkQuality'] = str(self.LastStat.LastLinkQuality) config['LastStat']['BatteryStatus'] = str(self.LastStat.LastBatteryStatus) config['LastStat']['HistoryIndex'] = str(self.LastStat.LastHistoryIndex) + config['LastStat']['LatestHistoryIndex'] = str(self.LastStat.LatestHistoryIndex) config['LastStat']['CurrentWeatherTime'] = self.LastStat.last_weather_ts config['LastStat']['HistoryDataTime'] = self.LastStat.last_history_ts config['LastStat']['ConfigTime'] = self.LastStat.last_config_ts @@ -2714,12 +2928,12 @@ class CDataStore(object): self.writeTransceiverSettings() def getRegisteredDeviceID(self): - return self.CommunicationSettings.DeviceID + return self.registeredDeviceID def setRegisteredDeviceID(self, val): - if val != self.CommunicationSettings.DeviceID: + if val != self.registeredDeviceID: loginf("console is paired to device with ID %04x" % val) - self.CommunicationSettings.DeviceID = val + self.registeredDeviceID = val def getTransceiverPresent(self): return self.transceiverPresent @@ -2750,43 +2964,36 @@ class CDataStore(object): self.writeLastStat() def setLastHistoryIndex(self,val): - logdbg("setLastHistoryIndex to %i (0x%x)" % (val, val)) + logdbg("setLastHistoryIndex to %s" % val) self.LastStat.LastHistoryIndex = val self.writeLastStat() def getLastHistoryIndex(self): return self.LastStat.LastHistoryIndex + def setLatestHistoryIndex(self,val): + self.LastStat.LatestHistoryIndex = val + self.writeLastStat() + + def getLatestHistoryIndex(self): + return self.LastStat.LatestHistoryIndex + def setCurrentWeather(self, data): - if DEBUG_WEATHER_DATA > 0: - logdbg('setCurrentWeather') self.CurrentWeather = data - def setHistoryData(self, data): - logdbg('setHistoryData') - self.HistoryData = data - def getDeviceRegistered(self): - if ( self.CommunicationSettings.DeviceID is None + if ( self.registeredDeviceID is None or self.TransceiverSettings.DeviceID is None - or self.CommunicationSettings.DeviceID != self.TransceiverSettings.DeviceID ): + or self.registeredDeviceID != self.TransceiverSettings.DeviceID ): return False return True - def getRequestType(self): - return self.Request.Type - - def setRequestType(self, val): - if DEBUG_COMM > 0: - logdbg('setRequestType to %s' % val) - self.Request.Type = val - def getCommModeInterval(self): - return self.CommunicationSettings.CommModeInterval + return self.commModeInterval def setCommModeInterval(self,val): logdbg("setCommModeInterval to %x" % val) - self.CommunicationSettings.CommModeInterval = val + self.commModeInterval = val def setTransceiverSerNo(self,val): logdbg("setTransceiverSerialNumber to %s" % val) @@ -2857,7 +3064,7 @@ class sHID(object): try: self.devh.detachKernelDriver(interface) except Exception, e: - loginf('Detach kernel driver failed: %s' % e) + pass # attempt to claim the interface try: @@ -3033,7 +3240,7 @@ class sHID(object): index=0x0000000, timeout=self.timeout) - def execute(self,command): + def execute(self, command): buf = [0]*0x0f #*0x15 buf[0] = 0xd9 buf[1] = command @@ -3203,8 +3410,6 @@ class CCommunicationService(object): self.shid = sHID() self.DataStore = CDataStore(cache_file) - self.TimeDifSec = 0 - self.DifHis = 0 self.firstSleep = 1 self.nextSleep = 1 @@ -3214,6 +3419,9 @@ class CCommunicationService(object): self.child = None self.thread_wait = 60.0 # seconds + self.command = None + self.history_cache = HistoryCache() + def buildFirstConfigFrame(self, Buffer, cs): logdbg('buildFirstConfigFrame: cs=%04x' % cs) newBuffer = [0] @@ -3281,39 +3489,48 @@ class CCommunicationService(object): Length = 0x0c return Length - def buildACKFrame(self,Buffer, action, cs, historyIndex): - logdbg("buildACKFrame: action=%x cs=%04x historyIndex=%i" % - (action, cs, historyIndex)) + # the first time through, thisIndex will be latestIndex+2 + def buildACKFrame(self, Buffer, action, cs, hidx=None): + logdbg("buildACKFrame: action=%x cs=%04x historyIndex=%s" % + (action, cs, hidx)) newBuffer = [0] newBuffer[0] = [0]*9 for i in xrange(0,2): newBuffer[0][i] = Buffer[0][i] - # when last weather is stale, change action to get current weather - now = int(time.time()) - age = now - self.DataStore.LastStat.last_weather_ts - if action != EAction.aGetCurrent and age >= 30 and newBuffer[0][1] != 0xF0: - logdbg('morphing action from %d to 5 (age=%s)' % (action, age)) - action = EAction.aGetCurrent - # FIXME: for now, never ask for historical records - if action == EAction.aGetHistory: - logdbg('morphing action from %d to 5' % action) - action = EAction.aGetCurrent + # when last weather is stale, change action to get current weather, + # but only if not getting history + if self.command != EAction.aGetHistory: + now = int(time.time()) + age = now - self.DataStore.LastStat.last_weather_ts + if action != EAction.aGetCurrent and age >= 30 and newBuffer[0][1] != 0xF0: + logdbg('buildACKFrame: morphing action from %d to 5 (age=%s)' % (action, age)) + action = EAction.aGetCurrent comInt = self.DataStore.getCommModeInterval() - if historyIndex >= 1797: - historyAddress = 0xffffff + if hidx is None: + if self.command == EAction.aGetHistory: + hidx = self.history_cache.next_index + elif self.DataStore.getLastHistoryIndex() is not None: + hidx = get_next_index(self.DataStore.getLastHistoryIndex()) + if hidx is None or hidx < 0 or hidx >= WS28xx.max_records: + haddr = 0xffffff else: - historyAddress = 18 * historyIndex + 416 + # hidx is the previous history index, not the next desired one, + # so use the last to get the next + hidx = get_prev_index(hidx) + haddr = index_to_addr(hidx) + + logdbg('buildACKFrame: idx: %s addr: 0x%04x' % (hidx, haddr)) newBuffer[0][2] = action & 0xF newBuffer[0][3] = (cs >> 8) & 0xFF newBuffer[0][4] = (cs >> 0) & 0xFF newBuffer[0][5] = (comInt >> 4) & 0xFF - newBuffer[0][6] = (historyAddress >> 16) & 0x0F | 16 * (comInt & 0xF) - newBuffer[0][7] = (historyAddress >> 8 ) & 0xFF - newBuffer[0][8] = (historyAddress >> 0 ) & 0xFF + newBuffer[0][6] = (haddr >> 16) & 0x0F | 16 * (comInt & 0xF) + newBuffer[0][7] = (haddr >> 8 ) & 0xFF + newBuffer[0][8] = (haddr >> 0 ) & 0xFF #d5 00 09 f0 f0 03 00 32 00 3f ff ff Buffer[0]=newBuffer[0] @@ -3333,15 +3550,15 @@ class CCommunicationService(object): newLength = [0] now = int(time.time()) self.DataStore.StationConfig.read(newBuffer) + if DEBUG_CONFIG_DATA > 0: + self.DataStore.StationConfig.toLog() self.DataStore.setLastStatCache(seen_ts=now, quality=(Buffer[0][3] & 0x7f), battery=(Buffer[0][2] & 0xf), config_ts=now) - idx = self.DataStore.getLastHistoryIndex() cs = newBuffer[0][47] | (newBuffer[0][46] << 8) - self.DataStore.setRequestType(ERequestType.rtGetCurrent) self.setSleep(0.380,0.200) - newLength[0] = self.buildACKFrame(newBuffer, EAction.aGetCurrent, cs, idx) + newLength[0] = self.buildACKFrame(newBuffer, EAction.aGetCurrent, cs) Buffer[0] = newBuffer[0] Length[0] = newLength[0] @@ -3356,10 +3573,13 @@ class CCommunicationService(object): chksum = CCurrentWeatherData.calcChecksum(Buffer) age = now - self.DataStore.LastStat.last_weather_ts if age >= 10 or chksum != self.DataStore.CurrentWeather.checksum(): + if DEBUG_WEATHER_DATA > 1: + self.shid.dump('CurWea', Buffer[0], fmt='long') data = CCurrentWeatherData() data.read(Buffer) -# self.shid.dump('CurWea', Buffer[0], fmt='long') self.DataStore.setCurrentWeather(data) + if DEBUG_WEATHER_DATA > 0: + data.toLog() # update the connection cache self.DataStore.setLastStatCache(seen_ts=now, @@ -3375,83 +3595,105 @@ class CCommunicationService(object): cfgBuffer = [0] cfgBuffer[0] = [0]*44 - idx = self.DataStore.getLastHistoryIndex() changed = self.DataStore.StationConfig.testConfigChanged(cfgBuffer) inBufCS = self.DataStore.StationConfig.getInBufCS() if inBufCS == 0 or inBufCS != cs: logdbg('handleCurrentData: inBufCS of station does not match') - self.DataStore.setRequestType(ERequestType.rtGetConfig) self.setSleep(0.400,0.400) - newLength[0] = self.buildACKFrame(newBuffer, EAction.aGetConfig, cs, idx) + newLength[0] = self.buildACKFrame(newBuffer, EAction.aGetConfig, cs) elif changed: logdbg('handleCurrentData: outBufCS of station changed') - self.DataStore.setRequestType(ERequestType.rtSetConfig) self.setSleep(0.420,0.005) - newLength[0] = self.buildACKFrame(newBuffer, EAction.aReqSetConfig, cs, idx) - else: - self.DataStore.setRequestType(ERequestType.rtGetCurrent) + newLength[0] = self.buildACKFrame(newBuffer, EAction.aReqSetConfig, cs) + elif self.command == EAction.aGetHistory: self.setSleep(0.380,0.200) - newLength[0] = self.buildACKFrame(newBuffer, EAction.aGetCurrent, cs, idx) + newLength[0] = self.buildACKFrame(newBuffer, EAction.aGetHistory, cs) + else: + self.setSleep(0.380,0.200) + newLength[0] = self.buildACKFrame(newBuffer, EAction.aGetCurrent, cs) Length[0] = newLength[0] Buffer[0] = newBuffer[0] - def handleHistoryData(self,Buffer,Length): + def handleHistoryData(self, buf, buflen): logdbg('handleHistoryData: %s' % self.timing()) + now = int(time.time()) - newBuffer = [0] - newBuffer[0] = Buffer[0] - newLength = [0] - data = CHistoryDataSet() - data.read(newBuffer) - cs = newBuffer[0][5] | (newBuffer[0][4] << 8) - latestAddr = ((((Buffer[0][6] & 0xF) << 8) | Buffer[0][7]) << 8) | Buffer[0][8] - thisAddr = ((((Buffer[0][9] & 0xF) << 8) | Buffer[0][10]) << 8) | Buffer[0][11] - thisIndex = (thisAddr - 415) / 18 - latestIndex = (latestAddr - 415) / 18 - - if ( latestIndex >= thisIndex ): - self.DifHis = latestIndex - thisIndex - else: - self.DifHis = latestIndex + 1797 - thisIndex - - if self.DifHis > 0: - logdbg('handleHistoryData: Time=%s OutstandingHistorySets=%4i' % - (data.Time, self.DifHis)) - - if self.DifHis > 0: - # FIXME: for now skip the history records - thisIndex = latestIndex - self.DifHis = 0 - self.setSleep(0.300,0.020) - self.DataStore.setLastHistoryIndex(thisIndex) - else: - self.setSleep(0.380,0.200) - if thisIndex != self.DataStore.getLastHistoryIndex(): - self.DataStore.setHistoryData(data) - self.DataStore.setLastHistoryIndex(thisIndex) - self.DataStore.setLastStatCache(seen_ts=now, - quality=(Buffer[0][3] & 0x7f), - battery=(Buffer[0][2] & 0xf), + quality=(buf[0][3] & 0x7f), + battery=(buf[0][2] & 0xf), history_ts=now) - if thisIndex == latestIndex: - self.TimeDifSec = (data.Time - datetime.fromtimestamp(now)).seconds - if self.TimeDifSec > 43200: - self.TimeDifSec = self.TimeDifSec - 86400 + 1 - logdbg('handleHistoryData: timeDifSec=%4s Time=%s' % - (self.TimeDifSec, data.Time)) + newbuf = [0] + newbuf[0] = buf[0] + newlen = [0] + data = CHistoryData() + data.read(newbuf) + if DEBUG_HISTORY_DATA > 0: + data.toLog() + + cs = newbuf[0][5] | (newbuf[0][4] << 8) + latestAddr = bytes_to_addr(buf[0][6], buf[0][7], buf[0][8]) + thisAddr = bytes_to_addr(buf[0][9], buf[0][10], buf[0][11]) + latestIndex = addr_to_index(latestAddr) + thisIndex = addr_to_index(thisAddr) + + nrec = latestIndex - thisIndex + if latestIndex < thisIndex: + nrec += WS28xx.max_records + logdbg('handleHistoryData: time=%s' + ' this=%d (0x%04x) latest=%d (0x%04x) nrec=%d' % + (data.Time, thisIndex, thisAddr, latestIndex, latestAddr, nrec)) + + # track the latest history index + self.DataStore.setLastHistoryIndex(thisIndex) + self.DataStore.setLatestHistoryIndex(latestIndex) + + # the first time through, the next index will be latest+2 + nextIndex = get_next_index(thisIndex) + if self.command == EAction.aGetHistory: + if self.history_cache.start_index is None: + # if there is no start index, start from the record after the + # latest one. + # FIXME: figure out exactly which record we should start from. + # this is non-trivial since the station retains records after + # it has been power cycled. + idx = get_next_index(latestIndex+1) +# idx = get_next_index(latestIndex-70) # for testing + self.history_cache.start_index = idx + self.history_cache.next_index = idx + logdbg('handleHistoryData: set start_index=%d' % idx) + nextIndex = idx + elif self.history_cache.next_index is not None: + if self.history_cache.next_index == thisIndex: + self.history_cache.num_scanned += 1 + # get the next history record + ts = tstr_to_ts(str(data.Time)) + if ts is not None and self.history_cache.since_ts < ts: + # append to the history if timestamp in desired range + logdbg('handleHistoryData: appending history record' + ' %s: %s' % (thisIndex, data.asDict())) + self.history_cache.records.append(data.asDict()) + self.history_cache.num_outstanding_records = nrec + else: + logdbg('handleHistoryData: skip record: since_ts=%s this_ts=%s' % (weeutil.weeutil.timestamp_to_string(self.history_cache.since_ts), weeutil.weeutil.timestamp_to_string(ts))) + self.history_cache.next_index = get_next_index(thisIndex) + else: + logdbg('handleHistoryData: index mismatch: %s != %s' % + (self.history_cache.next_index, thisIndex)) + nextIndex = self.history_cache.next_index + + logdbg('handleHistoryData: next=%s' % nextIndex) + + # if caching, request next history, otherwise request current weather + self.setSleep(0.380,0.200) + if self.command == EAction.aGetHistory: + newlen[0] = self.buildACKFrame(newbuf, EAction.aGetHistory, cs, nextIndex) else: - logdbg('handleHistoryData: no recent history data: Time=%s' % - data.Time) + newlen[0] = self.buildACKFrame(newbuf, EAction.aGetCurrent, cs) - self.DataStore.setRequestType(ERequestType.rtGetCurrent) - idx = thisIndex - newLength[0] = self.buildACKFrame(newBuffer, EAction.aGetCurrent, cs, idx) - - Length[0] = newLength[0] - Buffer[0] = newBuffer[0] + buflen[0] = newlen[0] + buf[0] = newbuf[0] def handleNextAction(self,Buffer,Length): logdbg('handleNextAction') @@ -3464,22 +3706,20 @@ class CCommunicationService(object): cs = newBuffer[0][5] | (newBuffer[0][4] << 8) if (Buffer[0][2] & 0xEF) == EResponseType.rtReqFirstConfig: logdbg('handleNextAction: a1 (first-time config)') - newLength[0] = self.buildFirstConfigFrame(newBuffer, cs) self.setSleep(0.085,0.005) + newLength[0] = self.buildFirstConfigFrame(newBuffer, cs) elif (Buffer[0][2] & 0xEF) == EResponseType.rtReqSetConfig: logdbg('handleNextAction: a2 (set config data)') - newLength[0] = self.buildConfigFrame(newBuffer) self.setSleep(0.085,0.005) + newLength[0] = self.buildConfigFrame(newBuffer) elif (Buffer[0][2] & 0xEF) == EResponseType.rtReqSetTime: logdbg('handleNextAction: a3 (set time data)') - newLength[0] = self.buildTimeFrame(newBuffer, cs) self.setSleep(0.085,0.005) + newLength[0] = self.buildTimeFrame(newBuffer, cs) else: logdbg('handleNextAction: %02x' % (Buffer[0][2] & 0xEF)) - idx = self.DataStore.getLastHistoryIndex() - self.DataStore.setRequestType(ERequestType.rtGetCurrent) self.setSleep(0.380,0.200) - newLength[0] = self.buildACKFrame(newBuffer, EAction.aGetCurrent, cs, idx) + newLength[0] = self.buildACKFrame(newBuffer, EAction.aGetCurrent, cs) Length[0] = newLength[0] Buffer[0] = newBuffer[0] @@ -3491,15 +3731,14 @@ class CCommunicationService(object): newBuffer[0] = Buffer[0] newLength = [0] newLength[0] = Length[0] - reqType = self.DataStore.getRequestType() if Length[0] == 0: - raise BadResponse('zero length for requestType=%x' % reqType) + raise BadResponse('zero length buffer') bufferID = (Buffer[0][0] <<8) | Buffer[0][1] respType = (Buffer[0][2] & 0xE0) if DEBUG_COMM > 0: - logdbg("generateResponse: id=%04x resp=%x req=%x length=%x" % - (bufferID, respType, reqType, Length[0])) + logdbg("generateResponse: id=%04x resp=%x length=%x" % + (bufferID, respType, Length[0])) deviceID = self.DataStore.getDeviceID() if bufferID != 0xF0F0: self.DataStore.setRegisteredDeviceID(bufferID) @@ -3548,7 +3787,7 @@ class CCommunicationService(object): # message is probably corrupt raise BadResponse('unknown response type %x' % respType) else: - msg = 'message from console contains unknown device ID (id=%04x resp=%x req=%x)' % (bufferID, respType, reqType) + msg = 'message from console contains unknown device ID (id=%04x resp=%x)' % (bufferID, respType) logdbg(msg) log_frame(Length[0],Buffer[0]) raise BadResponse(msg) @@ -3686,15 +3925,31 @@ class CCommunicationService(object): def getConfigData(self): return self.DataStore.StationConfig - # FIXME: make this thread-safe - def getHistoryData(self): - return self.DataStore.HistoryData + def startCachingHistory(self, since_ts=0, num_rec=0): + self.history_cache.clear_records() + self.history_cache.since_ts = since_ts + self.command = EAction.aGetHistory - def transceiverIsPresent(self): - return self.DataStore.getTransceiverPresent() + def stopCachingHistory(self): + self.command = None - def transceiverIsRegistered(self): - return self.DataStore.getDeviceRegistered() + def getUncachedHistoryCount(self): + return self.history_cache.num_outstanding_records + + def getNextHistoryIndex(self): + return self.history_cache.next_index + + def getNumHistoryScanned(self): + return self.history_cache.num_scanned + + def getLatestHistoryIndex(self): + return self.DataStore.LastStat.LatestHistoryIndex + + def getHistoryCacheRecords(self): + return self.history_cache.records + + def clearHistoryCache(self): + self.history_cache.clear_records() def startRFThread(self): if self.child is not None: @@ -3761,8 +4016,7 @@ class CCommunicationService(object): self.pollCount += 1 if StateBuffer[0][0] == 0x16: break - else: - time.sleep(self.nextSleep) + time.sleep(self.nextSleep) else: return diff --git a/bin/weewx/reportengine.py b/bin/weewx/reportengine.py index 9748bf30..112cd410 100644 --- a/bin/weewx/reportengine.py +++ b/bin/weewx/reportengine.py @@ -230,10 +230,12 @@ class RsyncGenerator(ReportGenerator): # We don't try to collect performance statistics about rsync, because rsync # will report them for us. Check the debug log messages. try: + if self.skin_dict.has_key('HTML_ROOT'): + html_root = self.skin_dict['HTML_ROOT'] + else: + html_root = self.config_dict['StdReport']['HTML_ROOT'] rsyncData = weeutil.rsyncupload.RsyncUpload( - local_root = os.path.join( - self.config_dict['WEEWX_ROOT'], - self.config_dict['StdReport']['HTML_ROOT']), + local_root = os.path.join(self.config_dict['WEEWX_ROOT'], html_root), remote_root = self.skin_dict['path'], server = self.skin_dict['server'], user = self.skin_dict.get('user', None), diff --git a/bin/weewx/wxengine.py b/bin/weewx/wxengine.py index 3b165259..edc087fb 100644 --- a/bin/weewx/wxengine.py +++ b/bin/weewx/wxengine.py @@ -651,8 +651,8 @@ class StdTimeSynch(StdService): # Zero out the time of last synch, and get the time between synchs. self.last_synch_ts = 0 - self.clock_check = int(config_dict['Station'].get('clock_check', 14400)) - self.max_drift = int(config_dict['Station'].get('max_drift', 5)) + self.clock_check = int(config_dict['StdTimeSynch'].get('clock_check', 14400)) + self.max_drift = int(config_dict['StdTimeSynch'].get('max_drift', 5)) self.bind(weewx.STARTUP, self.startup) self.bind(weewx.PRE_LOOP, self.pre_loop) @@ -675,12 +675,13 @@ class StdTimeSynch(StdService): try: console_time = self.engine.console.getTime() if console_time is None: return - diff = console_time - now_ts + # getTime can take a long time to run, so we use the current system time + diff = console_time - time.time() syslog.syslog(syslog.LOG_INFO, "wxengine: Clock error is %.2f seconds (positive is fast)" % diff) - if abs(now_ts - console_time) > self.max_drift: + if abs(diff) > self.max_drift: try: - self.engine.console.setTime(now_ts) + self.engine.console.setTime() except NotImplementedError: syslog.syslog(syslog.LOG_DEBUG, "wxengine: Station does not support setting the time") except NotImplementedError: diff --git a/docs/changes.txt b/docs/changes.txt index b8d52c13..a0dfeefb 100644 --- a/docs/changes.txt +++ b/docs/changes.txt @@ -6,7 +6,28 @@ weewx change history Removed the no longer needed serviced StdRESTful, obsolete since V2.6 -2.x.x +X.X.X XX/XX/XX + +Enabled multiple rsync instances for a single weewx instance. + +Added catchup to the WS28xx driver, but still no hardware record generation. + +Moved clock synchronization options clock_check and max_drift back to +section [StdTimeSynch]. + +Changed lux-to-W/m^2 conversion factor in the fine offset driver. + +Added rain rate calculation to Ultimeter driver. + +Changed setTime to retrieve system time directly rather than using a value +passed by the engine. This greatly improves the accuracy of StdTimeSync, +particularly in network based implementations. + +Fixed ENDPOINT_IN in the te923 driver. This should provide better +compatibility with a wider range of pyusb versions. + + +2.6.4 06/16/14 The WMR100 driver now calculates SLP in software. This fixes a problem with the WMRS200 station, which does not allow the user to set altitude. @@ -34,9 +55,9 @@ Prompt for metric/US units for debian installations. For WS28xx stations, return 0 for battery ok and 1 for battery failure. -If the console has successfully been opened up but then on subsequent opens -suffers an I/O error, weewx will now attempt a retry (before it would just -exit). +If a connection to the console has been successfully opened, but then on +subsequent connection attempts suffers an I/O error, weewx will now attempt +a retry (before it would just exit). 2.6.3 04/10/14 diff --git a/docs/customizing.htm b/docs/customizing.htm index 617234d7..03d3b1c3 100644 --- a/docs/customizing.htm +++ b/docs/customizing.htm @@ -23,7 +23,7 @@ table#stattypes td {

Customizing weewx
- Version: 3.0.0a1 + Version: 2.6.4

Table of Contents

@@ -157,8 +157,9 @@ table#stattypes td { that it they do not actually generate anything. Instead, they use the reporting service engine to arrange for things to be transferred to a remote server.

+

Skins

-

Each report has a Skin associated with it. For most reports, +

Each report has a skin associated with it. For most reports, the relationship with the skin is an obvious one: it contains the templates, any auxiliary files such as background GIFs or CSS style sheets, and a skin configuration file, skin.conf. @@ -190,12 +191,22 @@ table#stattypes td { }); +

Templates

+

A template is a text file that is processed by + weewx to create a new file. A template may + be used to generate HTML, XML, CSV, javascript, or any other type of + text file. A template typically contains variables that are replaced + by when creating the new file. Templates may also contain programming + logic.

+

Each template file lives in the skin directory of the skin that uses + it. By convention, a template file ends with the .tmpl extension.

+

Generators

-

To create their output, skins rely on one or more Generators, code +

To create their output, skins rely on one or more Generators that actually create useful things such as HTML files or plot images. Generators can also copy files around or FTP/rsync them to remote - locations. The default install of weewx includes - the following generators:

+ locations. The default install of weewx + includes the following generators:

@@ -314,15 +325,22 @@ table#stattypes td { changing the template files. The former is generally easier, but occasionally the latter is necessary.

Changing options

-

Changing an option means either modifying the main configuration file weewx.conf, or the skin configuration file for the - standard skin that comes with the distribution (nominally, file $SKIN_ROOT/Standard/skin.conf).

+

Changing an option means either modifying the main configuration file + weewx.conf, or the skin configuration file + skin.conf.

+

Each skin will have a skin.conf that defines + its default configuration. The examples in this guide refer to the + standard skin that comes with the distribution.

Changing options in skin.conf

-

With this approach, the user edits the skin configuration file for the - standard skin that comes with weewx, located - in $SKIN_ROOT/Standard/skin.conf, - using a text editor. For example, suppose you wish to use metric units +

With this approach, edit the skin configuration file with a text + editor. Changes made in this way will be used by + weewx the next time it generates reports, + which is typically the next archive interval; there is no need to + restart weewx to see the results of the + changes.

+

For the standard skin that comes with weewx, + the file is $SKIN_ROOT/Standard/skin.conf.

+

For example, suppose you wish to use metric units in the presentation layer, instead of the default US Customary Units. The section that controls units is [Units][[Groups]]. It looks like this:

@@ -382,15 +400,17 @@ table#stattypes td { ...

Overriding options in skin.conf from weewx.conf

-

This approach is very similar, except that instead of changing the skin - configuration file directly, you override its options by editing the - main configuration file, weewx.conf. The +

This approach is very similar, except that instead of changing the + skin configuration file directly, you override its options by editing + the main configuration file, weewx.conf. The advantage of this approach is that you can use the same skin to produce - several different output, each with separate options.

+ several different output, each with separate options.

+

With this approach, you must restart weewx + to see the effects of any changes.

Revisiting our example, suppose you want two reports, one in US - Customary, the other in Metric. The former will go in the directory $HTML_ROOT, the latter in a directory, $HTML_ROOT/metric. + Customary, the other in Metric. The former will go in the directory + $HTML_ROOT, the latter in a directory, + $HTML_ROOT/metric. If you just simply modify skin.conf, you can get one, but not both at the same time. Alternatively, you could create a whole new skin by copying all the files to a new skin directory @@ -434,13 +454,10 @@ table#stattypes td { group_speed = meter_per_second group_speed2 = meter_per_second2 group_temperature = degree_C - - [[FTP]] - ... - ... (as before) -

We have done two things different from the stock reports. First (1), we - have renamed the first report from StandardReport to - USReport for clarity; and (2) we have + +

We have done two things different from the stock reports. First (1), + we have renamed the first report from StandardReport to + USReport for clarity; and second (2), we have introduced a new report MetricReport, just like the first, except it puts its results in a different spot and uses different units. Both use the same skin, the Standard @@ -607,7 +624,7 @@ or in foobar units: $day.barometer.min.foobar

- + - + - + @@ -843,8 +860,8 @@ or in foobar units: $day.barometer.min.foobar
Comment
(no tag) (no tag) Value is returned as a string, formatted using an appropriate string format from skin.conf. A unit label (e.g., °F) from skin.conf is also attached @@ -644,7 +661,7 @@ or in foobar units: $day.barometer.min.foobar
.nolabel(string_format, NONE_string).nolabel(string_format, NONE_string) Value is returned as a string, using the string format specified with string_format. If the value is None, the string NONE_string will be @@ -677,7 +694,7 @@ or in foobar units: $day.barometer.min.foobar Returned Value
(no tag)(no tag) From skin.conf From skin.conf From skin.conf

Note: