Files
meteostick-logger/meteostick_reader.py

803 lines
33 KiB
Python

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Standalone Meteostick reader script
Based on the meteostick.py weewx driver
This script opens a Meteostick device, initializes it, and continuously
prints the raw messages received from Davis weather stations.
Can also send data to InfluxDB for storage and visualization.
"""
import time
import sys
import optparse
import math
import string
import os
try:
import serial
except ImportError:
print("Error: pyserial module is required. Install with: pip install pyserial")
sys.exit(1)
try:
from influxdb import InfluxDBClient
INFLUXDB_AVAILABLE = True
except ImportError:
INFLUXDB_AVAILABLE = False
# Simple CRC16 implementation for standalone use
def crc16(data):
"""Simple CRC16 implementation"""
crc = 0
for byte in data:
crc ^= ord(byte) << 8
for _ in range(8):
if crc & 0x8000:
crc = (crc << 1) ^ 0x1021
else:
crc <<= 1
crc &= 0xFFFF
return crc
# Simple logging functions
def logdbg(msg):
print(f"DEBUG: {msg}")
def loginf(msg):
print(f"INFO: {msg}")
def logerr(msg):
print(f"ERROR: {msg}")
# Constants from meteostick.py
DEBUG_SERIAL = 0
DEBUG_RAIN = 0
DEBUG_PARSE = 0
MPH_TO_MPS = 1609.34 / 3600.0 # meter/mile * hour/second
DEFAULT_SOIL_TEMP = 24 # C
RAW = 0 # indices of table with raw values
POT = 1 # indices of table with potentials
# Lookup tables
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)}
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 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])
def calculate_thermistor_temp(temp_raw):
"""Calculate thermistor temperature using Davis formulas."""
# 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."""
# 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
# Simplified Fahrenheit to Celsius conversion
def FtoC(temp_f):
"""Convert Fahrenheit to Celsius"""
return (temp_f - 32.0) * 5.0 / 9.0
class Meteostick(object):
DEFAULT_PORT = '/dev/ttyUSB0'
DEFAULT_BAUDRATE = 115200
DEFAULT_FREQUENCY = 'EU'
DEFAULT_RF_SENSITIVITY = 90
MAX_RF_SENSITIVITY = 125
RAW_CHANNEL = 0 # unused channel for the receiver stats in raw format
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 = self.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 Exception(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 Exception("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'] = Meteostick.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':
# ...existing code...
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
# Wind data processing
wind_speed_raw = pkt[1]
wind_dir_raw = pkt[2]
if not(wind_speed_raw == 0 and wind_dir_raw == 0):
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
data['wind_speed_raw'] = wind_speed_raw
data['wind_dir'] = wind_dir_pro
data['wind_speed'] = wind_speed_raw * MPH_TO_MPS
dbg_parse(3, "WS=%s WD=%s WS_raw=%s WD_raw=%s WD_pro=%s WD_vue=%s" %
(data['wind_speed'], data['wind_dir'],
wind_speed_raw,
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)
# Process different message types (simplified for reader)
if message_type == 8:
# outside temperature
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 = FtoC(temp_f)
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 == 0xA:
# outside humidity
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 == 0xE:
# rain
rain_count_raw = pkt[3]
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))
elif parts[0] == '#':
loginf("%s" % raw)
else:
logerr("unknown sensor identifier '%s' in %s" % (parts[0], raw))
return data
def send_to_influxdb(client, measurement, data, channel=None):
"""Send data to InfluxDB"""
if not client:
return
try:
# Prepare the data point
fields = {}
tags = {}
# Add channel as tag if available
if channel is not None:
tags['channel'] = str(channel)
# Convert data to appropriate fields
for key, value in data.items():
if key in ['channel', 'rf_signal', 'rf_missed']:
continue # Skip these as they're handled separately or not needed
# Convert to float if possible, otherwise keep as string
try:
fields[key] = float(value)
except (ValueError, TypeError):
fields[key] = str(value)
if not fields:
return # No data to send
# Add RF signal info if available
if 'rf_signal' in data:
fields['rf_signal'] = int(data['rf_signal'])
if 'rf_missed' in data:
fields['rf_missed'] = int(data['rf_missed'])
json_body = [
{
"measurement": measurement,
"tags": tags,
"time": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
"fields": fields
}
]
client.write_points(json_body)
logdbg(f"Sent to InfluxDB: {len(fields)} fields")
except Exception as e:
logerr(f"Failed to send data to InfluxDB: {e}")
def main():
usage = """%prog [options] [--help]
Read and display messages from a Meteostick device."""
# Debug: Print environment variables
print("Environment Variables:")
for key in os.environ:
if key.startswith('METEOSTICK_'):
print(f" {key}={os.environ[key]}")
print()
parser = optparse.OptionParser(usage=usage)
parser.add_option('--port', dest='port', metavar='PORT',
help='serial port to which the meteostick is connected',
default=os.getenv('METEOSTICK_PORT', Meteostick.DEFAULT_PORT))
parser.add_option('--baud', dest='baud', metavar='BAUDRATE', type=int,
help='serial port baud rate',
default=int(os.getenv('METEOSTICK_BAUDRATE', Meteostick.DEFAULT_BAUDRATE)))
parser.add_option('--freq', dest='freq', metavar='FREQUENCY',
help='comm frequency: US (915MHz), EU (868MHz), or AU (915MHz)',
default=os.getenv('METEOSTICK_FREQUENCY', Meteostick.DEFAULT_FREQUENCY))
parser.add_option('--rfs', dest='rfs', metavar='RF_SENSITIVITY', type=int,
help='RF sensitivity (0-125, default 90)',
default=int(os.getenv('METEOSTICK_RF_SENSITIVITY', Meteostick.DEFAULT_RF_SENSITIVITY)))
parser.add_option('--iss-channel', dest='c_iss', metavar='ISS_CHANNEL', type=int,
help='channel for ISS (1-8)', default=int(os.getenv('METEOSTICK_ISS_CHANNEL', '1')))
parser.add_option('--anemometer-channel', dest='c_anem', metavar='ANEM_CHANNEL', type=int,
help='channel for anemometer (0=none, 1-8)', default=int(os.getenv('METEOSTICK_ANEMOMETER_CHANNEL', '0')))
parser.add_option('--leaf-soil-channel', dest='c_ls', metavar='LS_CHANNEL', type=int,
help='channel for leaf-soil station (0=none, 1-8)', default=int(os.getenv('METEOSTICK_LEAF_SOIL_CHANNEL', '0')))
parser.add_option('--th1-channel', dest='c_th1', metavar='TH1_CHANNEL', type=int,
help='channel for T/H sensor 1 (0=none, 1-8)', default=int(os.getenv('METEOSTICK_TH1_CHANNEL', '0')))
parser.add_option('--th2-channel', dest='c_th2', metavar='TH2_CHANNEL', type=int,
help='channel for T/H sensor 2 (0=none, 1-8)', default=int(os.getenv('METEOSTICK_TH2_CHANNEL', '0')))
parser.add_option('--rain-bucket', dest='bucket', metavar='BUCKET_TYPE', type=int,
help='rain bucket type: 0=0.01in, 1=0.2mm (default 1)', default=int(os.getenv('METEOSTICK_RAIN_BUCKET', '1')))
# InfluxDB options
parser.add_option('--influxdb-host', dest='influx_host', metavar='HOST',
help='InfluxDB host (default: localhost)', default=os.getenv('METEOSTICK_INFLUXDB_HOST', 'localhost'))
parser.add_option('--influxdb-port', dest='influx_port', metavar='PORT', type=int,
help='InfluxDB port (default: 8086)', default=int(os.getenv('METEOSTICK_INFLUXDB_PORT', '8086')))
parser.add_option('--influxdb-user', dest='influx_user', metavar='USER',
help='InfluxDB username (deprecated, use --influxdb-token)', default=os.getenv('METEOSTICK_INFLUXDB_USER', ''))
parser.add_option('--influxdb-pass', dest='influx_pass', metavar='PASSWORD',
help='InfluxDB password (deprecated, use --influxdb-token)', default=os.getenv('METEOSTICK_INFLUXDB_PASS', ''))
parser.add_option('--influxdb-token', dest='influx_token', metavar='TOKEN',
help='InfluxDB authentication token', default=os.getenv('METEOSTICK_INFLUXDB_TOKEN', ''))
parser.add_option('--influxdb-db', dest='influx_db', metavar='DATABASE',
help='InfluxDB database name (default: weather)', default=os.getenv('METEOSTICK_INFLUXDB_DB', 'weather'))
parser.add_option('--influxdb-measurement', dest='influx_measurement', metavar='MEASUREMENT',
help='InfluxDB measurement name (default: meteostick)', default=os.getenv('METEOSTICK_INFLUXDB_MEASUREMENT', 'meteostick'))
parser.add_option('--enable-influxdb', dest='enable_influx', action='store_true',
help='Enable sending data to InfluxDB', default=os.getenv('METEOSTICK_ENABLE_INFLUXDB', 'false').lower() in ('true', '1', 'yes', 'on'))
(opts, args) = parser.parse_args()
print("Meteostick Reader")
print("================")
print(f"Port: {opts.port}")
print(f"Baudrate: {opts.baud}")
print(f"Frequency: {opts.freq}")
print(f"RF Sensitivity: {opts.rfs}")
print(f"ISS Channel: {opts.c_iss}")
print(f"Anemometer Channel: {opts.c_anem}")
print(f"Leaf/Soil Channel: {opts.c_ls}")
print(f"T/H 1 Channel: {opts.c_th1}")
print(f"T/H 2 Channel: {opts.c_th2}")
print(f"Rain bucket type: {opts.bucket}")
# InfluxDB setup
influx_client = None
if opts.enable_influx:
if not INFLUXDB_AVAILABLE:
print("ERROR: InfluxDB client not available. Install with: pip install influxdb")
return 1
print(f"InfluxDB Host: {opts.influx_host}:{opts.influx_port}")
print(f"InfluxDB Database: {opts.influx_db}")
print(f"InfluxDB Measurement: {opts.influx_measurement}")
# Determine authentication method
if opts.influx_token:
print("Using token-based authentication")
auth_method = "token"
elif opts.influx_user or opts.influx_pass:
print("Using username/password authentication (deprecated)")
auth_method = "userpass"
else:
print("Using no authentication")
auth_method = "none"
try:
# Create client based on authentication method
if auth_method == "token":
# For token-based auth with influxdb library, pass token as password
influx_client = InfluxDBClient(
host=opts.influx_host,
port=opts.influx_port,
username='token', # Use 'token' as username
password=opts.influx_token, # Use token as password
database=opts.influx_db,
ssl=False, # Set to True if using HTTPS
verify_ssl=False
)
elif auth_method == "userpass":
influx_client = InfluxDBClient(
host=opts.influx_host,
port=opts.influx_port,
username=opts.influx_user if opts.influx_user else None,
password=opts.influx_pass if opts.influx_pass else None,
database=opts.influx_db
)
else:
influx_client = InfluxDBClient(
host=opts.influx_host,
port=opts.influx_port,
database=opts.influx_db
)
# Test connection
influx_client.ping()
# Create database if it doesn't exist
databases = influx_client.get_list_database()
if not any(db['name'] == opts.influx_db for db in databases):
influx_client.create_database(opts.influx_db)
print(f"Created InfluxDB database: {opts.influx_db}")
print("InfluxDB connection established")
except Exception as e:
print(f"Failed to connect to InfluxDB: {e}")
print("Continuing without InfluxDB support...")
influx_client = None
else:
print("InfluxDB support disabled")
print()
# Calculate rain per tip based on bucket type
rain_per_tip = 0.254 if opts.bucket == 0 else 0.2 # mm
try:
# Create and configure the Meteostick
station = Meteostick(
port=opts.port,
baudrate=opts.baud,
transceiver_frequency=opts.freq,
rf_sensitivity=opts.rfs,
iss_channel=opts.c_iss,
anemometer_channel=opts.c_anem,
leaf_soil_channel=opts.c_ls,
temp_hum_1_channel=opts.c_th1,
temp_hum_2_channel=opts.c_th2
)
print("Opening connection...")
station.open()
print("Resetting device...")
reset_info = station.reset()
print(f"Reset response: {reset_info.strip()}")
print("Configuring device...")
station.configure()
print(f"\nListening for messages (Press Ctrl+C to exit)...")
if influx_client:
print(f"Data will be sent to InfluxDB: {opts.influx_host}:{opts.influx_port}/{opts.influx_db}")
print("=" * 60)
message_count = 0
influx_sent_count = 0
while True:
try:
# Get raw reading
raw_reading = station.get_readings()
if raw_reading:
message_count += 1
timestamp = time.strftime("%Y-%m-%d %H:%M:%S")
print(f"[{timestamp}] Message #{message_count}: {raw_reading}")
# Parse the reading for additional info
try:
parsed_data = station.parse_readings(raw_reading, rain_per_tip)
if parsed_data:
print(f" Parsed data: {parsed_data}")
# Send to InfluxDB if enabled and we have meaningful data
if influx_client and len(parsed_data) > 2: # More than just channel and rf_signal
channel = parsed_data.get('channel')
send_to_influxdb(influx_client, opts.influx_measurement, parsed_data, channel)
influx_sent_count += 1
except Exception as e:
print(f" Parse error: {e}")
print()
else:
# No data received, small delay
time.sleep(0.1)
except KeyboardInterrupt:
print("\nShutting down...")
break
except Exception as e:
print(f"Error reading data: {e}")
time.sleep(1)
except Exception as e:
print(f"Error: {e}")
return 1
finally:
try:
station.close()
print("Connection closed.")
except:
pass
print(f"Total messages received: {message_count}")
if influx_client:
print(f"Total messages sent to InfluxDB: {influx_sent_count}")
return 0
if __name__ == '__main__':
sys.exit(main())
if __name__ == '__main__':
sys.exit(main())