InfluxDB support and dockerisation

This commit is contained in:
2025-10-05 15:22:32 +01:00
parent 05407bb19e
commit 7e7889b157
5 changed files with 238 additions and 11 deletions

15
.gitignore vendored
View File

@@ -214,3 +214,18 @@ __marimo__/
# Streamlit # Streamlit
.streamlit/secrets.toml .streamlit/secrets.toml
# Environment files (contains sensitive tokens)
.env
.env.local
.env.*.local
# Docker
.dockerignore
# IDE
.vscode/
.idea/
*.swp
*.swo
*~

16
docker-compose.yaml Normal file
View File

@@ -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

17
docker/Dockerfile Normal file
View File

@@ -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"]

View File

@@ -6,6 +6,7 @@ Based on the meteostick.py weewx driver
This script opens a Meteostick device, initializes it, and continuously This script opens a Meteostick device, initializes it, and continuously
prints the raw messages received from Davis weather stations. prints the raw messages received from Davis weather stations.
Can also send data to InfluxDB for storage and visualization.
""" """
import time import time
@@ -13,6 +14,7 @@ import sys
import optparse import optparse
import math import math
import string import string
import os
try: try:
import serial import serial
@@ -20,6 +22,12 @@ except ImportError:
print("Error: pyserial module is required. Install with: pip install pyserial") print("Error: pyserial module is required. Install with: pip install pyserial")
sys.exit(1) sys.exit(1)
try:
from influxdb import InfluxDBClient
INFLUXDB_AVAILABLE = True
except ImportError:
INFLUXDB_AVAILABLE = False
# Simple CRC16 implementation for standalone use # Simple CRC16 implementation for standalone use
def crc16(data): def crc16(data):
"""Simple CRC16 implementation""" """Simple CRC16 implementation"""
@@ -510,36 +518,110 @@ class Meteostick(object):
logerr("unknown sensor identifier '%s' in %s" % (parts[0], raw)) logerr("unknown sensor identifier '%s' in %s" % (parts[0], raw))
return data 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(): def main():
usage = """%prog [options] [--help] usage = """%prog [options] [--help]
Read and display messages from a Meteostick device.""" 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 = optparse.OptionParser(usage=usage)
parser.add_option('--port', dest='port', metavar='PORT', parser.add_option('--port', dest='port', metavar='PORT',
help='serial port to which the meteostick is connected', 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, parser.add_option('--baud', dest='baud', metavar='BAUDRATE', type=int,
help='serial port baud rate', 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', parser.add_option('--freq', dest='freq', metavar='FREQUENCY',
help='comm frequency: US (915MHz), EU (868MHz), or AU (915MHz)', 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, parser.add_option('--rfs', dest='rfs', metavar='RF_SENSITIVITY', type=int,
help='RF sensitivity (0-125, default 90)', 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, 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, 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, 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, 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, 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, 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() (opts, args) = parser.parse_args()
@@ -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 1 Channel: {opts.c_th1}")
print(f"T/H 2 Channel: {opts.c_th2}") print(f"T/H 2 Channel: {opts.c_th2}")
print(f"Rain bucket type: {opts.bucket}") 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() print()
# Calculate rain per tip based on bucket type # Calculate rain per tip based on bucket type
@@ -584,10 +735,13 @@ Read and display messages from a Meteostick device."""
print("Configuring device...") print("Configuring device...")
station.configure() 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) print("=" * 60)
message_count = 0 message_count = 0
influx_sent_count = 0
while True: while True:
try: try:
@@ -604,6 +758,13 @@ Read and display messages from a Meteostick device."""
parsed_data = station.parse_readings(raw_reading, rain_per_tip) parsed_data = station.parse_readings(raw_reading, rain_per_tip)
if parsed_data: if parsed_data:
print(f" Parsed data: {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: except Exception as e:
print(f" Parse error: {e}") print(f" Parse error: {e}")
@@ -631,7 +792,11 @@ Read and display messages from a Meteostick device."""
pass pass
print(f"Total messages received: {message_count}") print(f"Total messages received: {message_count}")
if influx_client:
print(f"Total messages sent to InfluxDB: {influx_sent_count}")
return 0 return 0
if __name__ == '__main__': if __name__ == '__main__':
sys.exit(main()) sys.exit(main())
if __name__ == '__main__':
sys.exit(main())

14
requirements.txt Normal file
View File

@@ -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