InfluxDB support and dockerisation
This commit is contained in:
15
.gitignore
vendored
15
.gitignore
vendored
@@ -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
16
docker-compose.yaml
Normal 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
17
docker/Dockerfile
Normal 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"]
|
||||||
@@ -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
14
requirements.txt
Normal 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
|
||||||
Reference in New Issue
Block a user