From 7e7889b157a15422ebc02f48d89d505b43c37440 Mon Sep 17 00:00:00 2001 From: James Pattinson Date: Sun, 5 Oct 2025 15:22:32 +0100 Subject: [PATCH] InfluxDB support and dockerisation --- .gitignore | 15 ++++ docker-compose.yaml | 16 ++++ docker/Dockerfile | 17 ++++ meteostick_reader.py | 187 ++++++++++++++++++++++++++++++++++++++++--- requirements.txt | 14 ++++ 5 files changed, 238 insertions(+), 11 deletions(-) create mode 100644 docker-compose.yaml create mode 100644 docker/Dockerfile create mode 100644 requirements.txt diff --git a/.gitignore b/.gitignore index e15106e..915837d 100644 --- a/.gitignore +++ b/.gitignore @@ -214,3 +214,18 @@ __marimo__/ # Streamlit .streamlit/secrets.toml + +# Environment files (contains sensitive tokens) +.env +.env.local +.env.*.local + +# Docker +.dockerignore + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..586012a --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,16 @@ +version: "3.9" + +services: + metlogger: + build: + context: . + dockerfile: docker/Dockerfile + restart: unless-stopped + volumes: + - .:/app # optional: mount code for live editing + devices: + - "/dev/ttyACM0:/dev/ttyACM0" + env_file: + - .env + command: python -u meteostick_reader.py + diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000..12e0c88 --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,17 @@ +# Use a lightweight Python image for ARM (Pi) +FROM python:3.12-slim-bullseye + +# Set working directory +WORKDIR /app + +# Copy requirements first (better caching) +COPY ../requirements.txt . + +# Install dependencies +RUN pip install --no-cache-dir -r requirements.txt + +# Copy the rest of the code +COPY ../ ./ + +# Default command +CMD ["python", "main.py"] diff --git a/meteostick_reader.py b/meteostick_reader.py index 4b4353f..4f4a8b9 100644 --- a/meteostick_reader.py +++ b/meteostick_reader.py @@ -6,6 +6,7 @@ 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 @@ -13,6 +14,7 @@ import sys import optparse import math import string +import os try: import serial @@ -20,6 +22,12 @@ 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""" @@ -510,37 +518,111 @@ class Meteostick(object): 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=Meteostick.DEFAULT_PORT) + default=os.getenv('METEOSTICK_PORT', Meteostick.DEFAULT_PORT)) parser.add_option('--baud', dest='baud', metavar='BAUDRATE', type=int, help='serial port baud rate', - default=Meteostick.DEFAULT_BAUDRATE) + 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=Meteostick.DEFAULT_FREQUENCY) + 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=Meteostick.DEFAULT_RF_SENSITIVITY) + 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=1) + 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=0) + 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=0) + 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=0) + 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=0) + 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=1) + 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") @@ -555,6 +637,75 @@ Read and display messages from a Meteostick device.""" 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 @@ -584,10 +735,13 @@ Read and display messages from a Meteostick device.""" print("Configuring device...") station.configure() - print("\nListening for messages (Press Ctrl+C to exit)...") + 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: @@ -604,6 +758,13 @@ Read and display messages from a Meteostick device.""" 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}") @@ -631,7 +792,11 @@ Read and display messages from a Meteostick device.""" 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()) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..3f8eef5 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,14 @@ +certifi==2025.10.5 +charset-normalizer==3.4.3 +idna==3.10 +influxdb==5.3.2 +influxdb_client==1.49.0 +msgpack==1.1.1 +pyserial==3.5 +python-dateutil==2.9.0.post0 +pytz==2025.2 +reactivex==4.0.4 +requests==2.32.5 +six==1.17.0 +typing_extensions==4.15.0 +urllib3==2.5.0