#!/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())