#!/usr/bin/env python # -*- coding: utf-8 -*- # Meteostick driver for weewx # # Copyright 2016-2020 Matthew Wall, Luc Heijst # # Thanks to Frank Bandle for testing during the development of this driver. # Thanks to kobuki for validation, testing, and general sanity checks. # # This program is free software: you can redistribute it and/or modify it under # the terms of the GNU General Public License as published by the Free Software # Foundation, either version 3 of the License, or any later version. # # This program is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. # # See http://www.gnu.org/licenses/ """Meteostick is a USB device that receives radio transmissions from Davis weather stations. The meteostick has a preset radio frequency (RF) treshold value which is twice the RF sensity value in dB. Valid values for RF sensity range from 0 to 125. Both positive and negative parameter values will be treated as the same actual (negative) dB values. The default RF sensitivity value is 90 (-90 dB). Values between 95 and 125 tend to give too much noise and false readings (the higher value the more noise). Values lower than 50 likely result in no readings at all. The meteostick outputs data in one of 3 formats: human-readable, machine, and raw. The machine format is, in fact, human-readable as well. This driver supports only the raw format. The raw format provides more data and seems to result in higher quality readings. """ # FIXME: eliminate the service component - there is no need to bind to events from __future__ import division from __future__ import print_function # Python 2/3 compatiblity from __future__ import with_statement import math import serial import string import time import weewx import weewx.drivers import weewx.engine import weewx.wxformulas import weewx.units from weewx.crc16 import crc16 try: # Test for new-style weewx logging by trying to import weeutil.logger import weeutil.logger import logging log = logging.getLogger(__name__) def logdbg(msg): log.debug(msg) def loginf(msg): log.info(msg) def logerr(msg): log.error(msg) except ImportError: # Old-style weewx logging import syslog def logmsg(level, msg): syslog.syslog(level, 'mstk: %s:' % msg) def logdbg(msg): logmsg(syslog.LOG_DEBUG, msg) def loginf(msg): logmsg(syslog.LOG_INFO, msg) def logerr(msg): logmsg(syslog.LOG_ERR, msg) DRIVER_NAME = 'Meteostick' DRIVER_VERSION = '0.67' DEBUG_SERIAL = 0 DEBUG_RAIN = 0 DEBUG_PARSE = 0 DEBUG_RFS = 0 MPH_TO_MPS = 1609.34 / 3600.0 # meter/mile * hour/second def loader(config_dict, engine): return MeteostickDriver(engine, config_dict) def confeditor_loader(): return MeteostickConfEditor() def configurator_loader(config_dict): return MeteostickConfigurator() def dbg_serial(verbosity, msg): if DEBUG_SERIAL >= verbosity: logdbg(msg) def dbg_parse(verbosity, msg): if DEBUG_PARSE >= verbosity: logdbg(msg) def _fmt(data): if not data: return '' return ' '.join(['%02x' % int(ord(x)) for x in data]) # default temperature for soil moisture and leaf wetness sensors that # do not have a temperature sensor. # Also used to normalize raw values for a standard temperature. DEFAULT_SOIL_TEMP = 24 # C RAW = 0 # indices of table with raw values POT = 1 # indices of table with potentials # Lookup table for soil_moisture_raw values to get a soil_moisture value based # upon a linear formula. Correction factor = 0.009 SM_MAP = {RAW: ( 99.2, 140.1, 218.7, 226.9, 266.8, 391.7, 475.6, 538.2, 596.1, 673.7, 720.1), POT: ( 0.0, 1.0, 9.0, 10.0, 15.0, 35.0, 55.0, 75.0, 100.0, 150.0, 200.0)} # Lookup table for leaf_wetness_raw values to get a leaf_wetness value based # upon a linear formula. Correction factor = 0.0 LW_MAP = {RAW: (857.0, 864.0, 895.0, 911.0, 940.0, 952.0, 991.0, 1013.0), POT: ( 15.0, 14.0, 5.0, 4.0, 3.0, 2.0, 1.0, 0.0)} def calculate_thermistor_temp(temp_raw): """ Decode the raw thermistor temperature, then calculate the actual thermistor temperature and the leaf_soil potential, using Davis' formulas. see: https://github.com/cmatteri/CC1101-Weather-Receiver/wiki/Soil-Moisture-Station-Protocol :param temp_raw: raw value from sensor for leaf wetness and soil moisture """ # Convert temp_raw to a resistance (R) in kiloOhms a = 18.81099 b = 0.0009988027 r = a / (1.0 / temp_raw - b) / 1000 # k ohms # Steinhart-Hart parameters s1 = 0.002783573 s2 = 0.0002509406 try: thermistor_temp = 1 / (s1 + s2 * math.log(r)) - 273 dbg_parse(3, 'r (k ohm) %s temp_raw %s thermistor_temp %s' % (r, temp_raw, thermistor_temp)) return thermistor_temp except ValueError as e: logerr('thermistor_temp failed for temp_raw %s r (k ohm) %s' 'error: %s' % (temp_raw, r, e)) return DEFAULT_SOIL_TEMP def lookup_potential(sensor_name, norm_fact, sensor_raw, sensor_temp, lookup): """Look up potential based upon a normalized raw value (i.e. temp corrected for DEFAULT_SOIL_TEMP) and a linear function between two points in the lookup table. :param lookup: a table with both sensor_raw_norm values and corresponding potential values. the table is composed for a specific norm-factor. :param sensor_temp: sensor temp in C :param sensor_raw: sensor raw potential value :param norm_fact: temp correction factor for normalizing sensor-raw values :param sensor_name: string used in debug messages """ # normalize raw value for standard temperature (DEFAULT_SOIL_TEMP) sensor_raw_norm = sensor_raw * (1 + norm_fact * (sensor_temp - DEFAULT_SOIL_TEMP)) numcols = len(lookup[RAW]) if sensor_raw_norm >= lookup[RAW][numcols - 1]: potential = lookup[POT][numcols - 1] # preset potential to last value dbg_parse(3, "%s: temp=%s fact=%s raw=%s norm=%s potential=%s >= RAW=%s" % (sensor_name, sensor_temp, norm_fact, sensor_raw, sensor_raw_norm, potential, lookup[RAW][numcols - 1])) else: potential = lookup[POT][0] # preset potential to first value # lookup sensor_raw_norm value in table for x in range(0, numcols): if sensor_raw_norm < lookup[RAW][x]: if x == 0: # 'pre zero' phase; potential = first value dbg_parse(3, "%s: temp=%s fact=%s raw=%s norm=%s potential=%s < RAW=%s" % (sensor_name, sensor_temp, norm_fact, sensor_raw, sensor_raw_norm, potential, lookup[RAW][0])) break else: # determine the potential value potential_per_raw = (lookup[POT][x] - lookup[POT][x - 1]) / (lookup[RAW][x] - lookup[RAW][x - 1]) potential_offset = (sensor_raw_norm - lookup[RAW][x - 1]) * potential_per_raw potential = lookup[POT][x - 1] + potential_offset dbg_parse(3, "%s: temp=%s fact=%s raw=%s norm=%s potential=%s RAW=%s to %s POT=%s to %s " % (sensor_name, sensor_temp, norm_fact, sensor_raw, sensor_raw_norm, potential, lookup[RAW][x - 1], lookup[RAW][x], lookup[POT][x - 1], lookup[POT][x])) break return potential RAW_CHANNEL = 0 # unused channel for the receiver stats in raw format class MeteostickDriver(weewx.drivers.AbstractDevice, weewx.engine.StdService): NUM_CHAN = 10 # 8 channels, one fake channel (9), one unused channel (0) DEFAULT_RAIN_BUCKET_TYPE = 1 DEFAULT_SENSOR_MAP = { 'pressure': 'pressure', 'inTemp': 'temp_in', # temperature inside meteostick 'windSpeed': 'wind_speed', 'windDir': 'wind_dir', 'outTemp': 'temperature', 'outHumidity': 'humidity', 'inHumidity': 'humidity_in', # To use a rainRate calculation from this driver that closely matches # that of a Davis station, uncomment the rainRate field then specify # rainRate = hardware in section [StdWXCalculate] of weewx.conf #'rainRate': 'rain_rate', 'radiation': 'solar_radiation', 'UV': 'uv', 'rxCheckPercent': 'pct_good', 'soilTemp1': 'soil_temp_1', 'soilTemp2': 'soil_temp_2', 'soilTemp3': 'soil_temp_3', 'soilTemp4': 'soil_temp_4', 'soilMoist1': 'soil_moisture_1', 'soilMoist2': 'soil_moisture_2', 'soilMoist3': 'soil_moisture_3', 'soilMoist4': 'soil_moisture_4', 'leafWet1': 'leaf_wetness_1', 'leafWet2': 'leaf_wetness_2', 'leafTemp1': 'leaf_temp_1', 'leafTemp2': 'leaf_temp_2', 'extraTemp1': 'temp_1', 'extraTemp2': 'temp_2', 'extraTemp3': 'temp_3', 'extraHumid1': 'humid_1', 'extraHumid2': 'humid_2', 'txBatteryStatus': 'bat_iss', 'windBatteryStatus': 'bat_anemometer', 'rainBatteryStatus': 'bat_leaf_soil', 'outTempBatteryStatus': 'bat_th_1', 'inTempBatteryStatus': 'bat_th_2', 'referenceVoltage': 'solar_power', 'supplyVoltage': 'supercap_volt'} def __init__(self, engine, config_dict): loginf('driver version is %s' % DRIVER_VERSION) if engine: weewx.engine.StdService.__init__(self, engine, config_dict) stn_dict = config_dict.get(DRIVER_NAME, {}) global DEBUG_PARSE DEBUG_PARSE = int(stn_dict.get('debug_parse', DEBUG_PARSE)) global DEBUG_SERIAL DEBUG_SERIAL = int(stn_dict.get('debug_serial', DEBUG_SERIAL)) global DEBUG_RAIN DEBUG_RAIN = int(stn_dict.get('debug_rain', DEBUG_RAIN)) global DEBUG_RFS DEBUG_RFS = int(stn_dict.get('debug_rf_sensitivity', DEBUG_RFS)) bucket_type = int(stn_dict.get('rain_bucket_type', self.DEFAULT_RAIN_BUCKET_TYPE)) if bucket_type not in [0, 1]: raise ValueError("unsupported rain bucket type %s" % bucket_type) self.rain_per_tip = 0.254 if bucket_type == 0 else 0.2 # mm loginf('using rain_bucket_type %s' % bucket_type) self.sensor_map = dict(self.DEFAULT_SENSOR_MAP) if 'sensor_map' in stn_dict: self.sensor_map.update(stn_dict['sensor_map']) loginf('sensor map is: %s' % self.sensor_map) self.max_tries = int(stn_dict.get('max_tries', 10)) self.retry_wait = int(stn_dict.get('retry_wait', 10)) self.last_rain_count = None self.first_rf_stats = True self._init_rf_stats() self.station = Meteostick(**stn_dict) self.station.open() self.station.reset() self.station.configure() # bind to new archive record events so that we can update the rf # stats on each archive record. if engine: self.bind(weewx.NEW_ARCHIVE_RECORD, self.new_archive_record) def closePort(self): if self.station is not None: self.station.close() self.station = None @property def hardware_name(self): return 'Meteostick' def genLoopPackets(self): while True: readings = self.station.get_readings_with_retry(self.max_tries, self.retry_wait) data = self.station.parse_readings(readings, self.rain_per_tip) if 'channel' in data: self._update_rf_stats(data['channel'], data['rf_signal'], data['rf_missed']) if data: dbg_parse(2, "data: %s" % data) packet = self._data_to_packet(data) if packet is not None: dbg_parse(3, "packet: %s" % packet) yield packet def _data_to_packet(self, data): packet = dict() # map sensor observations to database field names for k in self.sensor_map: if self.sensor_map[k] in data: packet[k] = data[self.sensor_map[k]] # convert the rain count to a rain delta measure if 'rain_count' in data: if self.last_rain_count is not None: rain_count = data['rain_count'] - self.last_rain_count else: rain_count = 0 # handle rain counter wrap around from 127 to 0 if rain_count < 0: if DEBUG_RAIN: logdbg("rain counter wraparound detected rain_count=%s" % rain_count) rain_count += 128 self.last_rain_count = data['rain_count'] packet['rain'] = float(rain_count) * self.rain_per_tip if DEBUG_RAIN: logdbg("rain=%s rain_count=%s last_rain_count=%s" % (packet['rain'], rain_count, self.last_rain_count)) elif len(packet) <= 1: # No data found dbg_parse(3, "skip packet for data: %s" % data) return None packet['dateTime'] = int(time.time() + 0.5) packet['usUnits'] = weewx.METRICWX return packet def _init_rf_stats(self): self.rf_stats = { 'min': [0] * self.NUM_CHAN, # rf sensitivity has negative values 'max': [-125] * self.NUM_CHAN, 'sum': [0] * self.NUM_CHAN, 'cnt': [0] * self.NUM_CHAN, 'last': [0] * self.NUM_CHAN, 'avg': [0] * self.NUM_CHAN, 'missed': [0] * self.NUM_CHAN, 'pctgood': [None] * self.NUM_CHAN, 'ts': int(time.time())} # unlike the rf sensitivity measures, pct_good is positive def _update_rf_stats(self, ch, signal, missed): # update the rf statistics self.rf_stats['min'][ch] = min(signal, self.rf_stats['min'][ch]) self.rf_stats['max'][ch] = max(signal, self.rf_stats['max'][ch]) self.rf_stats['sum'][ch] += signal self.rf_stats['cnt'][ch] += 1 self.rf_stats['last'][ch] = signal self.rf_stats['missed'][ch] += missed def _update_rf_summaries(self): # Update the summary stats, skip channels that do not matter. # The pctgood is a measure of rf quality. # For raw format, the values of pctgood will be calculated per active # channel when the summaries are calculated, based upon the number of # received good packets and number of missed packets. for ch in range(1, self.NUM_CHAN): # skip channel 0 (it is not used) if self.rf_stats['cnt'][ch] > 0: self.rf_stats['avg'][ch] = int(self.rf_stats['sum'][ch] / self.rf_stats['cnt'][ch]) for ch in range(1, self.NUM_CHAN - 1): # no ch if self.rf_stats['cnt'][ch] > 0: self.rf_stats['pctgood'][ch] = \ int(0.5 + 100.0 * self.rf_stats['cnt'][ch] / (self.rf_stats['cnt'][ch] + self.rf_stats['missed'][ch])) if -self.rf_stats['min'][ch] >= self.station.rfs and self.rf_stats['missed'][ch] > 0: loginf('WARNING: rf_sensitivity (%s) might be too low for channel %s (%s signals missed)' % (self.station.rfs, ch, self.rf_stats['missed'][ch])) def _report_rf_stats(self): logdbg("RF summary: rf_sensitivity=%s (values in dB)" % self.station.rfs) logdbg("Station max min avg last count [missed] [good]") for x in [('iss', self.station.channels['iss']), ('wind', self.station.channels['anemometer']), ('leaf_soil', self.station.channels['leaf_soil']), ('temp_hum_1', self.station.channels['temp_hum_1']), ('temp_hum_2', self.station.channels['temp_hum_2'])]: if x[1] != 0: self._report_channel(x[0], x[1]) def _report_channel(self, label, ch): if self.rf_stats['pctgood'][ch] is None \ or (-self.rf_stats['min'][ch] >= self.station.rfs and self.rf_stats['pctgood'][ch] < 85): msg = "WARNING: rf_sensitivity might be too low for this channel" else: msg = "" logdbg("%s %5d %5d %5d %5d %5d %7d %s %s" % (label.ljust(15), self.rf_stats['max'][ch], self.rf_stats['min'][ch], self.rf_stats['avg'][ch], self.rf_stats['last'][ch], self.rf_stats['cnt'][ch], self.rf_stats['missed'][ch], self.rf_stats['pctgood'][ch], msg)) def new_archive_record(self, event): self._update_rf_summaries() # calculate rf summaries # Do not store first results after startup; the data are not complete if not self.first_rf_stats: event.record['rxCheckPercent'] = self.rf_stats['pctgood'][self.station.channels['iss']] logdbg("data['rxCheckPercent']: %s" % event.record['rxCheckPercent']) self.first_rf_stats = False if DEBUG_RFS: self._report_rf_stats() self._init_rf_stats() # flush rf statistics class Meteostick(object): DEFAULT_PORT = '/dev/ttyUSB0' DEFAULT_BAUDRATE = 115200 DEFAULT_FREQUENCY = 'EU' DEFAULT_RF_SENSITIVITY = 90 MAX_RF_SENSITIVITY = 125 def __init__(self, **cfg): self.port = cfg.get('port', self.DEFAULT_PORT) loginf('using serial port %s' % self.port) self.baudrate = cfg.get('baudrate', self.DEFAULT_BAUDRATE) loginf('using baudrate %s' % self.baudrate) freq = cfg.get('transceiver_frequency', self.DEFAULT_FREQUENCY) if freq not in ['EU', 'US', 'AU']: raise ValueError("invalid frequency %s" % freq) self.frequency = freq loginf('using frequency %s' % self.frequency) rfs = int(cfg.get('rf_sensitivity', self.DEFAULT_RF_SENSITIVITY)) absrfs = abs(rfs) if absrfs > self.MAX_RF_SENSITIVITY: raise ValueError("invalid RF sensitivity %s" % rfs) self.rfs = absrfs self.rf_threshold = absrfs * 2 loginf('using rf sensitivity %s (-%s dB)' % (rfs, absrfs)) channels = dict() channels['iss'] = int(cfg.get('iss_channel', 1)) channels['anemometer'] = int(cfg.get('anemometer_channel', 0)) channels['leaf_soil'] = int(cfg.get('leaf_soil_channel', 0)) channels['temp_hum_1'] = int(cfg.get('temp_hum_1_channel', 0)) channels['temp_hum_2'] = int(cfg.get('temp_hum_2_channel', 0)) if channels['anemometer'] == 0: channels['wind_channel'] = channels['iss'] else: channels['wind_channel'] = channels['anemometer'] self.channels = channels loginf('using iss_channel %s' % channels['iss']) loginf('using anemometer_channel %s' % channels['anemometer']) loginf('using leaf_soil_channel %s' % channels['leaf_soil']) loginf('using temp_hum_1_channel %s' % channels['temp_hum_1']) loginf('using temp_hum_2_channel %s' % channels['temp_hum_2']) self.transmitters = Meteostick.ch_to_xmit( channels['iss'], channels['anemometer'], channels['leaf_soil'], channels['temp_hum_1'], channels['temp_hum_2']) loginf('using transmitters %02x' % self.transmitters) self.timeout = 3 # seconds self.serial_port = None @staticmethod def ch_to_xmit(iss_channel, anemometer_channel, leaf_soil_channel, temp_hum_1_channel, temp_hum_2_channel): transmitters = 0 transmitters += 1 << (iss_channel - 1) if anemometer_channel != 0: transmitters += 1 << (anemometer_channel - 1) if leaf_soil_channel != 0: transmitters += 1 << (leaf_soil_channel - 1) if temp_hum_1_channel != 0: transmitters += 1 << (temp_hum_1_channel - 1) if temp_hum_2_channel != 0: transmitters += 1 << (temp_hum_2_channel - 1) return transmitters @staticmethod def _check_crc(msg, chksum): crc_result = crc16(msg) if crc_result != chksum: logerr('CRC result is 0x%04x, should be 0x%04x' % (crc_result, chksum)) raise ValueError("CRC error") def __enter__(self): self.open() return self def __exit__(self, _, value, traceback): self.close() def open(self): dbg_serial(1, "open serial port %s" % self.port) self.serial_port = serial.Serial(self.port, self.baudrate, timeout=self.timeout) def close(self): if self.serial_port is not None: dbg_serial(1, "close serial port %s" % self.port) self.serial_port.close() self.serial_port = None def get_readings(self): buf = self.serial_port.readline().decode('utf-8') if len(buf) > 0: dbg_serial(2, "station said: %s" % ' '.join(["%0.2X" % ord(c) for c in buf])) return buf.strip() def get_readings_with_retry(self, max_tries=5, retry_wait=10): for ntries in range(0, max_tries): try: return self.get_readings() except serial.serialutil.SerialException as e: loginf("Failed attempt %d of %d to get readings: %s" % (ntries + 1, max_tries, e)) time.sleep(retry_wait) else: msg = "Max retries (%d) exceeded for readings" % max_tries logerr(msg) raise weewx.RetriesExceeded(msg) def reset(self, max_wait=30): """Reset the device, leaving it in a state that we can talk to it.""" loginf("establish communication with the meteostick") # flush any previous data in the input buffer self.serial_port.flushInput() # Send a reset command self.serial_port.write(b'r\n') # Wait until we see the ? character start_ts = time.time() ready = False response = '' while not ready: time.sleep(0.1) while self.serial_port.inWaiting() > 0: c = self.serial_port.read(1).decode('utf-8') if c == '?': ready = True elif c in string.printable: response += c if time.time() - start_ts > max_wait: raise weewx.WakeupError("No 'ready' response from meteostick after %s seconds" % max_wait) loginf("reset: %s" % response.split('\n')[0]) dbg_serial(2, "full response to reset: %s" % response) # Discard any serial input from the device time.sleep(0.2) self.serial_port.flushInput() return response def configure(self): """Configure the device to send data continuously.""" loginf("configure meteostick to logger mode") # Show default settings (they might change with a new firmware version) self.send_command('?') # Set RF threshold self.send_command('x' + str(self.rf_threshold)) # Listen to configured transmitters self.send_command('t' + str(self.transmitters)) # Filter transmissions from anything other than configured transmitters self.send_command('f1') # Listen to configured repeaters self.send_command('r1') # repeater 1 # Set device to produce 10-bytes raw data command = 'o3' self.send_command(command) # Set the frequency. Valid frequencies are US, EU and AU command = 'm0' # default to US if self.frequency == 'AU': command = 'm2' elif self.frequency == 'EU': command = 'm1' self.send_command(command) # From now on the device will produce lines with received data def send_command(self, cmd): cmd2 = (cmd + "\r").encode('utf-8') self.serial_port.write(cmd2) time.sleep(0.2) response = self.serial_port.read(self.serial_port.inWaiting()).decode('utf-8') dbg_serial(1, "cmd: '%s': %s" % (cmd, response)) self.serial_port.flushInput() @staticmethod def get_parts(raw): raw_str = str(raw) dbg_parse(1, "readings: %s" % raw_str) parts = raw.split(' ') dbg_parse(3, "parts: %s (%s)" % (parts, len(parts))) if len(parts) < 2: raise ValueError("not enough parts in '%s'" % raw) return parts def parse_readings(self, raw, rain_per_tip): data = dict() if not raw: return data if not all(c in string.printable for c in raw): logerr("unprintable characters in readings: %s" % _fmt(raw)) return data try: data = self.parse_raw(raw, self.channels['iss'], self.channels['anemometer'], self.channels['leaf_soil'], self.channels['temp_hum_1'], self.channels['temp_hum_2'], rain_per_tip) except ValueError as e: logerr("parse failed for '%s': %s" % (raw, e)) return data @staticmethod def parse_raw(raw, iss_ch, wind_ch, ls_ch, th1_ch, th2_ch, rain_per_tip): data = dict() parts = Meteostick.get_parts(raw) n = len(parts) if parts[0] == 'B': # message example: # B 29530 338141 366 101094 60 37 data['channel'] = RAW_CHANNEL # rf_signal data will not be used data['rf_signal'] = 0 # not available data['rf_missed'] = 0 # not available if n >= 6: data['temp_in'] = float(parts[3]) / 10.0 # C data['pressure'] = float(parts[4]) / 100.0 # hPa if n > 7: # only with custom receiver data['humidity_in'] = float(parts[7]) else: logerr("B: not enough parts (%s) in '%s'" % (n, raw)) elif parts[0] == 'I': # raw Davis sensor message in 10 byte format incl header and # additional info # message example: # ---- raw message ---- rfs ts_last # I 102 51 0 DB FF 73 0 11 41 -65 5249944 202 raw_msg = [0] * 10 for i in range(0, 10): raw_msg[i] = parts[i + 2] pkt = bytearray([int(i, base=16) for i in raw_msg]) # perform crc-check raw_msg_crc = [0] * 8 if pkt[8] == 0xFF and pkt[9] == 0xFF: # message received from davis equipment # Calculate crc with bytes 0-7, result must be equal to 0 chksum = 0 for i in range(0, 8): raw_msg_crc[i] = chr(int(parts[i + 2], 16)) Meteostick._check_crc(raw_msg_crc, chksum) else: # message received via repeater # Calculate crc with bytes 0-5 and 8-9, result must be equal # to bytes 6-7 chksum = (pkt[6] << 8) + pkt[7] for i in range(0, 6): raw_msg_crc[i] = chr(int(parts[i + 2], 16)) for i in range(6, 8): raw_msg_crc[i] = chr(int(parts[i + 4], 16)) Meteostick._check_crc(raw_msg_crc, chksum) data['channel'] = (pkt[0] & 0x7) + 1 battery_low = (pkt[0] >> 3) & 0x1 data['rf_signal'] = int(parts[13]) time_since_last = int(parts[14]) # the cyclus time varies from 2.5 to 3 seconds for channels 1 to 8 # simplifiy calculation with max cyclus time of 3.0 seconds data['rf_missed'] = (time_since_last // 2500000) - 1 if data['rf_missed'] > 0: dbg_parse(3, "channel %s missed %s" % (data['channel'], data['rf_missed'])) if data['channel'] == iss_ch or data['channel'] == wind_ch \ or data['channel'] == th1_ch or data['channel'] == th2_ch: if data['channel'] == iss_ch: data['bat_iss'] = battery_low elif data['channel'] == wind_ch: data['bat_anemometer'] = battery_low elif data['channel'] == th1_ch: data['bat_th_1'] = battery_low else: data['bat_th_2'] = battery_low # Each data packet of iss or anemometer contains wind info, # but it is only valid when received from the channel with # the anemometer connected # message examples: # I 101 51 6 B2 FF 73 0 76 61 -69 2624964 59 # I 101 E0 0 0 4E 5 0 72 61 -68 2562440 68 (no sensor) wind_speed_raw = pkt[1] wind_dir_raw = pkt[2] if not(wind_speed_raw == 0 and wind_dir_raw == 0): """ The elder Vantage Pro and Pro2 stations measured the wind direction with a potentiometer. This type has a fairly big dead band around the North. The Vantage Vue station uses a hall effect device to measure the wind direction. This type has a much smaller dead band, so there are two different formulas for calculating the wind direction. To be able to select the right formula the Vantage type must be known. For now we use the traditional 'pro' formula for all wind directions. """ dbg_parse(3, "wind_speed_raw=%03x wind_dir_raw=0x%03x" % (wind_speed_raw, wind_dir_raw)) # Vantage Pro and Pro2 if wind_dir_raw == 0: wind_dir_pro = 5.0 elif wind_dir_raw == 255: wind_dir_pro = 355.0 else: wind_dir_pro = 9.0 + (wind_dir_raw - 1) * 342.0 / 253.0 # Vantage Vue wind_dir_vue = wind_dir_raw * 1.40625 + 0.3 # wind error correction is by raw byte values wind_speed_ec = round(Meteostick.calc_wind_speed_ec(wind_speed_raw, wind_dir_raw)) data['wind_speed_ec'] = wind_speed_ec data['wind_speed_raw'] = wind_speed_raw data['wind_dir'] = wind_dir_pro data['wind_speed'] = wind_speed_ec * MPH_TO_MPS dbg_parse(3, "WS=%s WD=%s WS_raw=%s WS_ec=%s WD_raw=%s WD_pro=%s WD_vue=%s" % (data['wind_speed'], data['wind_dir'], wind_speed_raw, wind_speed_ec, wind_dir_raw if wind_dir_raw <= 180 else 360 - wind_dir_raw, wind_dir_pro, wind_dir_vue)) # data from both iss sensors and extra sensors on # Anemometer Transport Kit message_type = (pkt[0] >> 4 & 0xF) if message_type == 2: # supercap voltage (Vue only) max: 0x3FF (1023) # message example: # I 103 20 4 C3 D4 C1 81 89 EE -77 2562520 -70 """When the raw values are divided by 300 the maximum voltage of the super capacitor will be about 2.8 V. This is close to its maximum operating voltage of 2.7 V """ supercap_volt_raw = ((pkt[3] << 2) + (pkt[4] >> 6)) & 0x3FF if supercap_volt_raw != 0x3FF: data['supercap_volt'] = supercap_volt_raw / 300.0 dbg_parse(3, "supercap_volt_raw=0x%03x value=%s" % (supercap_volt_raw, data['supercap_volt'])) elif message_type == 3: # unknown message type # message examples: # TODO # TODO (no sensor) dbg_parse(1, "unknown message with type=0x03; " "pkt[3]=0x%02x pkt[4]=0x%02x pkt[5]=0x%02x" % (pkt[3], pkt[4], pkt[5])) elif message_type == 4: # uv # message examples: # I 103 40 00 00 12 45 00 B5 2A -78 2562444 -24 # I 103 41 0 DE FF C3 0 A9 8D -65 2624976 -38 (no sensor) uv_raw = ((pkt[3] << 2) + (pkt[4] >> 6)) & 0x3FF if uv_raw != 0x3FF: data['uv'] = uv_raw / 50.0 dbg_parse(3, "uv_raw=%04x value=%s" % (uv_raw, data['uv'])) elif message_type == 5: # rain rate # message examples: # I 104 50 0 0 FF 75 0 48 5B -77 2562452 140 (no rain) # I 101 50 0 0 FE 75 0 7F 6B -66 2562464 68 (light_rain) # I 100 50 0 0 1B 15 0 3F 80 -67 2562448 -95 (heavy_rain) # I 102 51 0 DB FF 73 0 11 41 -65 5249944 202 (no sensor) """ The published rain_rate formulas differ from each other. For both light and heavy rain we like to know a 'time between tips' in s. The rain_rate then would be: 3600 [s/h] / time_between_tips [s] * 0.2 [mm] = xxx [mm/h] """ # typical time between tips: 64-1022 time_between_tips_raw = ((pkt[4] & 0x30) << 4) + pkt[3] dbg_parse(3, "time_between_tips_raw=%03x (%s)" % (time_between_tips_raw, time_between_tips_raw)) if data['channel'] == iss_ch: # rain sensor is present rain_rate = None if time_between_tips_raw == 0x3FF: # no rain rain_rate = 0 dbg_parse(3, "no_rain=%s mm/h" % rain_rate) elif pkt[4] & 0x40 == 0: # heavy rain. typical value: # 64/16 - 1020/16 = 4 - 63.8 (180.0 - 11.1 mm/h) time_between_tips = time_between_tips_raw / 16.0 rain_rate = 3600.0 / time_between_tips * rain_per_tip dbg_parse(3, "heavy_rain=%s mm/h, time_between_tips=%s s" % (rain_rate, time_between_tips)) else: # light rain. typical value: # 64 - 1022 (11.1 - 0.8 mm/h) time_between_tips = time_between_tips_raw rain_rate = 3600.0 / time_between_tips * rain_per_tip dbg_parse(3, "light_rain=%s mm/h, time_between_tips=%s s" % (rain_rate, time_between_tips)) data['rain_rate'] = rain_rate elif message_type == 6: # solar radiation # message examples # I 104 61 0 DB 0 43 0 F4 3B -66 2624972 121 # I 104 60 0 0 FF C5 0 79 DA -77 2562444 137 (no sensor) sr_raw = ((pkt[3] << 2) + (pkt[4] >> 6)) & 0x3FF if sr_raw < 0x3FE: data['solar_radiation'] = sr_raw * 1.757936 dbg_parse(3, "solar_radiation_raw=0x%04x value=%s" % (sr_raw, data['solar_radiation'])) elif message_type == 7: # solar cell output / solar power (Vue only) # message example: # I 102 70 1 F5 CE 43 86 58 E2 -77 2562532 173 """When the raw values are divided by 300 the voltage comes in the range of 2.8-3.3 V measured by the machine readable format """ solar_power_raw = ((pkt[3] << 2) + (pkt[4] >> 6)) & 0x3FF if solar_power_raw != 0x3FF: data['solar_power'] = solar_power_raw / 300.0 dbg_parse(3, "solar_power_raw=0x%03x solar_power=%s" % (solar_power_raw, data['solar_power'])) elif message_type == 8: # outside temperature # message examples: # I 103 80 0 0 33 8D 0 25 11 -78 2562444 -25 (digital temp) # I 100 81 0 0 59 45 0 A3 E6 -89 2624956 -42 (analog temp) # I 104 81 0 DB FF C3 0 AB F8 -66 2624980 125 (no digital sensor) # I 101 81 5 C9 FF 83 0 73 AC FF FF -68 2624988 161 (no analog sensor) temp_raw = (pkt[3] << 4) + (pkt[4] >> 4) # 12-bits temp value if temp_raw != 0xFFC and temp_raw != 0xFF8: if pkt[4] & 0x8: # digital temp sensor - value is twos-complement if pkt[3] & 0x80 != 0: temp_f = -(temp_raw ^ 0xFFF) / 10.0 else: temp_f = temp_raw / 10.0 temp_c = weewx.wxformulas.FtoC(temp_f) # C dbg_parse(3, "digital temp_raw=0x%03x temp_f=%s temp_c=%s" % (temp_raw, temp_f, temp_c)) else: # analog sensor (thermistor) temp_raw = temp_raw // 4 # 10-bits temp value temp_c = calculate_thermistor_temp(temp_raw) dbg_parse(3, "thermistor temp_raw=0x%03x temp_c=%s" % (temp_raw, temp_c)) if data['channel'] == th1_ch: data['temp_1'] = temp_c elif data['channel'] == th2_ch: data['temp_2'] = temp_c elif data['channel'] == wind_ch: data['temp_3'] = temp_c else: data['temperature'] = temp_c elif message_type == 9: # 10-min average wind gust # message examples: # I 102 91 0 DB 0 3 E 89 85 -66 2624972 204 # I 102 90 0 0 0 5 0 31 51 -75 2562456 223 (no sensor) gust_raw = pkt[3] # mph gust_index_raw = pkt[5] >> 4 if not(gust_raw == 0 and gust_index_raw == 0): dbg_parse(3, "W10=%s gust_index_raw=%s" % (gust_raw, gust_index_raw)) # don't store the 10-min gust data because there is no # field for it reserved in the standard wview schema elif message_type == 0xA: # outside humidity # message examples: # A0 00 00 C9 3D 00 2A 87 (digital sensor, variant a) # A0 01 3A 80 3B 00 ED 0E (digital sensor, variant b) # A0 01 41 7F 39 00 18 65 (digital sensor, variant c) # A0 00 00 22 85 00 ED E3 (analog sensor) # A1 00 DB 00 03 00 47 C7 (no sensor) humidity_raw = ((pkt[4] >> 4) << 8) + pkt[3] if humidity_raw != 0: if pkt[4] & 0x08 == 0x8: # digital sensor humidity = humidity_raw / 10.0 else: # analog sensor (pkt[4] & 0x0f == 0x5) humidity = humidity_raw * -0.301 + 710.23 if data['channel'] == th1_ch: data['humid_1'] = humidity elif data['channel'] == th2_ch: data['humid_2'] = humidity elif data['channel'] == wind_ch: loginf("Warning: humidity sensor of Anemometer Transmitter Kit not in sensor map: %s" % humidity) else: data['humidity'] = humidity dbg_parse(3, "humidity_raw=0x%03x value=%s" % (humidity_raw, humidity)) elif message_type == 0xC: # unknown message # message example: # I 101 C1 4 D0 0 1 0 E9 A4 -69 2624968 56 # As we have seen after one day of received data # pkt[3] and pkt[5] are always zero; # pckt[4] has values 0-3 (ATK) or 5 (temp/hum) dbg_parse(3, "unknown pkt[3]=0x%02x pkt[4]=0x%02x pkt[5]=0x%02x" % (pkt[3], pkt[4], pkt[5])) elif message_type == 0xE: # rain # message examples: # I 103 E0 0 0 5 5 0 9F 3D -78 2562416 -28 # I 101 E1 0 DB 80 3 0 16 8D -67 5249956 37 (no sensor) rain_count_raw = pkt[3] """We have seen rain counters wrap around at 127 and others wrap around at 255. When we filter the highest bit, both counter types will wrap at 127. """ if rain_count_raw != 0x80: rain_count = rain_count_raw & 0x7F # skip high bit data['rain_count'] = rain_count dbg_parse(3, "rain_count_raw=0x%02x value=%s" % (rain_count_raw, rain_count)) else: # unknown message type logerr("unknown message type 0x%01x" % message_type) elif data['channel'] == ls_ch: # leaf and soil station data['bat_leaf_soil'] = battery_low data_type = pkt[0] >> 4 if data_type == 0xF: data_subtype = pkt[1] & 0x3 sensor_num = ((pkt[1] & 0xe0) >> 5) + 1 temp_c = DEFAULT_SOIL_TEMP temp_raw = ((pkt[3] << 2) + (pkt[5] >> 6)) & 0x3FF potential_raw = ((pkt[2] << 2) + (pkt[4] >> 6)) & 0x3FF if data_subtype == 1: # soil moisture # message examples: # I 102 F2 9 1A 55 C0 0 62 E6 -51 2687524 207 # I 104 F2 29 FF FF C0 C0 F1 EC -52 2687408 124 (no sensor) if pkt[3] != 0xFF: # soil temperature temp_c = calculate_thermistor_temp(temp_raw) data['soil_temp_%s' % sensor_num] = temp_c dbg_parse(3, "soil_temp_%s=%s 0x%03x" % (sensor_num, temp_c, temp_raw)) if pkt[2] != 0xFF: # soil moisture potential # Lookup soil moisture potential in SM_MAP norm_fact = 0.009 # Normalize potential_raw soil_moisture = lookup_potential( "soil_moisture", norm_fact, potential_raw, temp_c, SM_MAP) data['soil_moisture_%s' % sensor_num] = soil_moisture dbg_parse(3, "soil_moisture_%s=%s 0x%03x" % (sensor_num, soil_moisture, potential_raw)) elif data_subtype == 2: # leaf wetness # message examples: # I 100 F2 A D4 55 80 0 90 6 -53 2687516 -121 # I 101 F2 2A 0 FF 40 C0 4F 5 -52 2687404 43 (no sensor) if pkt[3] != 0xFF: # leaf temperature temp_c = calculate_thermistor_temp(temp_raw) data['leaf_temp_%s' % sensor_num] = temp_c dbg_parse(3, "leaf_temp_%s=%s 0x%03x" % (sensor_num, temp_c, temp_raw)) if pkt[2] != 0: # leaf wetness potential # Lookup leaf wetness potential in LW_MAP norm_fact = 0.0 # Do not normalize potential_raw leaf_wetness = lookup_potential( "leaf_wetness", norm_fact, potential_raw, temp_c, LW_MAP) data['leaf_wetness_%s' % sensor_num] = leaf_wetness dbg_parse(3, "leaf_wetness_%s=%s 0x%03x" % (sensor_num, leaf_wetness, potential_raw)) else: logerr("unknown subtype '%s' in '%s'" % (data_subtype, raw)) else: logerr("unknown station with channel: %s, raw message: %s" % (data['channel'], raw)) elif parts[0] == '#': loginf("%s" % raw) else: logerr("unknown sensor identifier '%s' in %s" % (parts[0], raw)) return data # Normalize and interpolate raw wind values at raw angles @staticmethod def calc_wind_speed_ec(raw_mph, raw_angle): # some sanitization: no corrections needed under 3 and no values exist # above 150 mph if raw_mph < 3 or raw_mph > 150: return raw_mph # Error correction values for # [ 1..29 by 1, 30..150 by 5 raw mph ] # x # [ 1, 4, 8..124 by 4, 127, 128 raw degrees ] # # Extracted from a Davis Weather Envoy using a DIY transmitter to # transmit raw values and logging LOOP packets. # first row: raw angles; # first column: raw speed; # cells: values provided in response to raw data by the Envoy; # [0][0] is filler windtab = [ [0, 1, 4, 8, 12, 16, 20, 24, 28, 32, 36, 40, 44, 48, 52, 56, 60, 64, 68, 72, 76, 80, 84, 88, 92, 96, 100, 104, 108, 112, 116, 120, 124, 127, 128], [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0], [4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0], [5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0], [6, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 0, 0], [7, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 1, 0, 0], [8, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 1, 0, 0], [9, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 1, 0, 0], [10, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 1, 0, 0], [11, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 1, 0, 0], [12, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 1, 0, 0], [13, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 3, 3, 1, 0, 0], [14, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 3, 3, 1, 0, 0], [15, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 3, 3, 1, 0, 0], [16, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 3, 3, 1, 0, 0], [17, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 3, 3, 1, 0, 0], [18, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 3, 3, 1, 0, 0], [19, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 4, 4, 1, 0, 0], [20, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 3, 4, 4, 2, 0, 0], [21, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 3, 4, 4, 2, 0, 0], [22, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 3, 4, 4, 2, 0, 0], [23, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 3, 4, 4, 2, 0, 0], [24, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 2, 3, 4, 4, 2, 0, 0], [25, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 2, 3, 4, 4, 2, 0, 0], [26, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 2, 3, 5, 4, 2, 0, 0], [27, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 2, 3, 5, 5, 2, 0, 0], [28, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 2, 3, 5, 5, 2, 0, 0], [29, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 2, 3, 5, 5, 2, 0, 0], [30, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 2, 3, 5, 5, 2, 0, 0], [35, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 2, 4, 6, 5, 2, 0, -1], [40, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 2, 4, 6, 6, 2, 0, -1], [45, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 2, 4, 7, 6, 2, -1, -1], [50, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 2, 5, 7, 7, 2, -1, -2], [55, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 2, 5, 8, 7, 2, -1, -2], [60, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 2, 5, 8, 8, 2, -1, -2], [65, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 2, 5, 9, 8, 2, -2, -3], [70, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 0, 2, 5, 9, 9, 2, -2, -3], [75, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 0, 2, 6, 10, 9, 2, -2, -3], [80, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 0, 2, 6, 10, 10, 2, -2, -3], [85, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 0, 2, 7, 11, 11, 2, -3, -4], [90, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 2, 7, 12, 11, 2, -3, -4], [95, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 2, 2, 2, 2, 2, 3, 2, 2, 2, 1, 1, 1, 1, 2, 7, 12, 12, 3, -3, -4], [100, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 2, 2, 2, 2, 3, 3, 2, 2, 2, 1, 1, 1, 1, 2, 8, 13, 12, 3, -3, -4], [105, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 2, 2, 3, 3, 3, 3, 3, 2, 2, 2, 1, 1, 1, 2, 8, 13, 13, 3, -3, -4], [110, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 2, 2, 3, 3, 3, 3, 3, 2, 2, 2, 1, 1, 1, 2, 8, 14, 14, 3, -3, -5], [115, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 2, 2, 2, 3, 3, 3, 3, 3, 2, 2, 2, 1, 1, 1, 2, 9, 15, 14, 3, -3, -5], [120, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 2, 2, 2, 3, 3, 3, 3, 3, 2, 2, 2, 1, 1, 1, 3, 9, 15, 15, 3, -4, -5], [125, 1, 1, 2, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 3, 3, 3, 3, 3, 2, 2, 1, 1, 1, 3, 10, 16, 16, 3, -4, -5], [130, 1, 1, 2, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 3, 3, 3, 3, 3, 2, 2, 2, 1, 1, 3, 10, 17, 16, 3, -4, -6], [135, 1, 2, 2, 1, 1, 0, 0, 0, -1, 0, 0, 1, 1, 2, 2, 3, 3, 3, 3, 4, 3, 3, 2, 2, 2, 1, 1, 3, 10, 17, 17, 4, -4, -6], [140, 1, 2, 2, 1, 1, 0, 0, 0, -1, 0, 0, 1, 1, 2, 2, 3, 3, 3, 4, 4, 3, 3, 2, 2, 2, 1, 1, 3, 11, 18, 17, 4, -4, -6], [145, 2, 2, 2, 1, 1, 0, 0, 0, -1, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 4, 3, 3, 3, 2, 2, 1, 1, 3, 11, 19, 18, 4, -4, -6], [150, 2, 2, 2, 1, 1, 0, 0, -1, -1, 0, 0, 1, 1, 2, 3, 3, 4, 4, 4, 4, 4, 3, 3, 2, 2, 1, 1, 3, 12, 19, 19, 4, -4, -6] ] # EC is symmetric between W/E (90/270°) - probably a wrong assumption, # table needs to be redone for 0-360° if raw_angle > 128: raw_angle = 256 - raw_angle s0 = a0 = 1 while windtab[s0][0] < raw_mph: s0 += 1 while windtab[0][a0] < raw_angle: a0 += 1 if windtab[s0][0] == raw_mph: s1 = s0 else: if s0 > 1: s0 -= 1 s1 = len(windtab) - 1 if s0 == len(windtab) - 1 else s0 + 1 if windtab[0][a0] == raw_angle: a1 = a0 else: if a0 > 1: a0 -= 1 a1 = len(windtab[0]) - 2 if a0 == len(windtab) - 1 else a0 + 1 if s0 == s1 and a0 == a1: return raw_mph + windtab[s0][a0] else: return Meteostick.interpolate(windtab[0][a0], windtab[0][a1], windtab[s0][0], windtab[s1][0], windtab[s0][a0], windtab[s0][a1], windtab[s1][a0], windtab[s1][a1], raw_angle, raw_mph) # Simple bilinear interpolation # # a0 a1 <-- fixed raw angles # x0---------x1 s0 # | | # | | # | * <-|-- raw input angle, raw speed value (x, y) # | | # y0---------y1 s1 # ^ # \__ speed: measured raw / correction values # @staticmethod def interpolate(rx0, rx1, ry0, ry1, x0, x1, y0, y1, x, y): dbg_parse(3, "rx0=%s, rx1=%s, ry0=%s, ry1=%s, x0=%s, x1=%s, y0=%s, y1=%s, x=%s, y=%s" % (rx0, rx1, ry0, ry1, x0, x1, y0, y1, x, y)) if rx0 == rx1: return y + x0 + (y - ry0) / float(ry1 - ry0) * (y1 - y0) if ry0 == ry1: return y + y0 + (x - rx0) / float(rx1 - rx0) * (x1 - x0) dy0 = x0 + (y - ry0) / float(ry1 - ry0) * (y0 - x0) dy1 = x1 + (y - ry0) / float(ry1 - ry0) * (y1 - x1) return y + dy0 + (x - rx0) / float(rx1 - rx0) * (dy1 - dy0) class MeteostickConfEditor(weewx.drivers.AbstractConfEditor): @property def default_stanza(self): return """ [Meteostick] # This section is for the Meteostick USB receiver. # The serial port to which the meteostick is attached, e.g., /dev/ttyS0 port = /dev/ttyUSB0 # Radio frequency to use between USB transceiver and console: US, EU or AU # US uses 915 MHz # EU uses 868.3 MHz # AU uses 915 MHz but has different frequency hopping values than US transceiver_frequency = EU # A channel has value 0-8 where 0 indicates not present # The channel of the Vantage Vue, Pro, or Pro2 ISS iss_channel = 1 # Additional channels apply only to Vantage Pro or Pro2 anemometer_channel = 0 leaf_soil_channel = 0 temp_hum_1_channel = 0 temp_hum_2_channel = 0 # Rain bucket type: 0 is 0.01 inch per tip, 1 is 0.2 mm per tip rain_bucket_type = 1 # Print debug messages # 0=no logging; 1=minimum logging; 2=normal logging; 3=detailed logging debug_parse = 0 debug_serial = 0 debug_rain = 0 debug_rf_sensitivity = 1 # The driver to use driver = user.meteostick """ def prompt_for_settings(self): settings = dict() print("Specify the serial port on which the meteostick is connected,") print("for example /dev/ttyUSB0 or /dev/ttyS0") settings['port'] = self._prompt('port', Meteostick.DEFAULT_PORT) print("Specify the frequency between the station and the meteostick,") print("one of US (915 MHz), EU (868.3 MHz), or AU (915 MHz)") settings['transceiver_frequency'] = self._prompt('frequency', 'EU', ['US', 'EU', 'AU']) print("Specify the type of the rain bucket,") print("either 0 (0.01 inches per tip) or 1 (0.2 mm per tip)") settings['rain_bucket_type'] = self._prompt('rain_bucket_type', MeteostickDriver.DEFAULT_RAIN_BUCKET_TYPE) print("Specify the channel of the ISS (1-8)") settings['iss_channel'] = self._prompt('iss_channel', 1) print("Specify the channel of the Anemometer Transmitter Kit (0=none; 1-8)") settings['anemometer_channel'] = self._prompt('anemometer_channel', 0) print("Specify the channel of the Leaf & Soil station (0=none; 1-8)") settings['leaf_soil_channel'] = self._prompt('leaf_soil_channel', 0) print("Specify the channel of the first Temp/Humidity station (0=none; 1-8)") settings['temp_hum_1_channel'] = self._prompt('temp_hum_1_channel', 0) print("Specify the channel of the second Temp/Humidity station (0=none; 1-8)") settings['temp_hum_2_channel'] = self._prompt('temp_hum_2_channel', 0) return settings class MeteostickConfigurator(weewx.drivers.AbstractConfigurator): def add_options(self, parser): super(MeteostickConfigurator, self).add_options(parser) parser.add_option( "--info", dest="info", action="store_true", help="display meteostick configuration") parser.add_option( "--show-options", dest="opts", action="store_true", help="display meteostick command options") parser.add_option( "--set-verbose", dest="verbose", metavar="X", type=int, help="set verbose: 0=off, 1=on; default off") parser.add_option( "--set-debug", dest="debug", metavar="X", type=int, help="set debug: 0=off, 1=on; default off") # bug in meteostick: according to docs, 0=high, 1=low parser.add_option( "--set-ledmode", dest="led", metavar="X", type=int, help="set led mode: 1=high 0=low; default low") parser.add_option( "--set-bandwidth", dest="bandwidth", metavar="X", type=int, help="set bandwidth: " "0=narrow: best for Davis sensors, " "1=normal: for reading retransmitted packets, " "2=wide: for Ambient stations); default narrow") parser.add_option( "--set-probe", dest="probe", metavar="X", type=int, help="set probe: 0=off, 1=on; default off") parser.add_option( "--set-repeater", dest="repeater", metavar="X", type=int, help="set repeater: 0-255; default 255") parser.add_option( "--set-channel", dest="channel", metavar="X", type=int, help="set channel: 0-255; default 255") parser.add_option( "--set-format", dest="format", metavar="X", type=int, help="set format: 0=raw, 1=machine, 2=human") def do_options(self, options, parser, config_dict, prompt): driver = MeteostickDriver(None, config_dict) info = driver.station.reset() if options.info: print(info) cfg = { 'v': options.verbose, 'd': options.debug, 'l': options.led, 'b': options.bandwidth, 'p': options.probe, 'r': options.repeater, 'c': options.channel, 'o': options.format} for opt in cfg: if cfg[opt]: cmd = opt + cfg[opt] print("set station parameter %s" % cmd) driver.station.send_command(cmd) if options.opts: driver.station.send_command('?') print(driver.station.get()) driver.closePort() # define a main entry point for basic testing of the station without weewx # engine and service overhead. invoke this as follows from the weewx root dir: # # PYTHONPATH=bin python bin/user/meteostick.py if __name__ == '__main__': import optparse usage = """%prog [options] [--help]""" syslog.openlog('meteostick', syslog.LOG_PID | syslog.LOG_CONS) syslog.setlogmask(syslog.LOG_UPTO(syslog.LOG_DEBUG)) parser = optparse.OptionParser(usage=usage) parser.add_option('--version', dest='version', action='store_true', help='display driver version') parser.add_option('--port', dest='port', metavar='PORT', help='serial port to which the station is connected', default=Meteostick.DEFAULT_PORT) parser.add_option('--baud', dest='baud', metavar='BAUDRATE', help='serial port baud rate', default=Meteostick.DEFAULT_BAUDRATE) parser.add_option('--freq', dest='freq', metavar='FREQUENCY', help='comm frequency, either US (915MHz) or EU (868MHz)', default=Meteostick.DEFAULT_FREQUENCY) parser.add_option('--rfs', dest='rfs', metavar='RF_SENSITIVITY', help='RF sensitivity in dB', default=Meteostick.DEFAULT_RF_SENSITIVITY) parser.add_option('--iss-channel', dest='c_iss', metavar='ISS_CHANNEL', help='channel for ISS', default=1) parser.add_option('--anemometer-channel', dest='c_a', metavar='ANEMOMETER_CHANNEL', help='channel for anemometer', default=0) parser.add_option('--leaf-soil-channel', dest='c_ls', metavar='LEAF_SOIL_CHANNEL', help='channel for leaf-soil', default=0) parser.add_option('--th1-channel', dest='c_th1', metavar='TH1_CHANNEL', help='channel for T/H sensor 1', default=0) parser.add_option('--th2-channel', dest='c_th2', metavar='TH2_CHANNEL', help='channel for T/H sensor 2', default=0) (opts, args) = parser.parse_args() if opts.version: print("meteostick driver version %s" % DRIVER_VERSION) exit(0) with Meteostick(port=opts.port, baudrate=opts.baud, transceiver_frequency=opts.freq, iss_channel=int(opts.c_iss), anemometer_channel=int(opts.c_a), leaf_soil_channel=int(opts.c_ls), temp_hum_1_channel=int(opts.c_th1), temp_hum_2_channel=int(opts.c_th2), rf_sensitivity=int(opts.rfs)) as s: while True: print(time.time(), s.get_readings())