1353 lines
63 KiB
Python
1353 lines
63 KiB
Python
#!/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())
|