Compare commits

...

2 Commits

Author SHA1 Message Date
9ad012f88c Adding README 2025-10-05 15:24:45 +01:00
7e7889b157 InfluxDB support and dockerisation 2025-10-05 15:22:32 +01:00
6 changed files with 429 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
*~

191
README.md Normal file
View File

@@ -0,0 +1,191 @@
# Meteostick Logger
A Python application for reading weather data from Davis Instruments weather stations via a Meteostick USB receiver. The application can display real-time weather data and optionally send it to InfluxDB for storage and visualization.
## Features
- Real-time weather data collection from Davis weather stations
- Support for multiple sensor types (ISS, anemometer, temperature/humidity sensors)
- InfluxDB integration for data storage
- Docker support for easy deployment
- Configurable via environment variables
- Token-based authentication for InfluxDB
## Supported Sensors
- **ISS (Integrated Sensor Suite)**: Temperature, humidity, wind speed/direction, rain
- **Anemometer Kit**: Additional wind measurements
- **Temperature/Humidity Sensors**: Up to 2 additional T/H sensors
- **Leaf/Soil Station**: Soil moisture and temperature sensors
## Requirements
- Python 3.7+
- Meteostick USB device
- Davis Instruments weather station transmitters
- InfluxDB (optional, for data storage)
## Installation
### Python Dependencies
```bash
pip install pyserial influxdb
```
### Docker (Recommended)
1. Clone this repository
2. Create a `.env` file (see configuration below)
3. Build and run with Docker Compose:
```bash
docker-compose up --build
```
## Configuration
### Environment Variables
The application is configured using environment variables. Create a `.env` file in the project root:
```env
# Meteostick Hardware Configuration
METEOSTICK_PORT=/dev/ttyACM0
METEOSTICK_BAUDRATE=115200
METEOSTICK_FREQUENCY=EU
METEOSTICK_RF_SENSITIVITY=90
# Weather Station Channels (1-8, or 0 to disable)
METEOSTICK_ISS_CHANNEL=1
METEOSTICK_ANEMOMETER_CHANNEL=0
METEOSTICK_LEAF_SOIL_CHANNEL=0
METEOSTICK_TH1_CHANNEL=0
METEOSTICK_TH2_CHANNEL=0
# Rain Bucket Type (0=0.01in, 1=0.2mm)
METEOSTICK_RAIN_BUCKET=1
# InfluxDB Configuration
METEOSTICK_ENABLE_INFLUXDB=true
METEOSTICK_INFLUXDB_HOST=localhost
METEOSTICK_INFLUXDB_PORT=8086
METEOSTICK_INFLUXDB_DB=weather
METEOSTICK_INFLUXDB_MEASUREMENT=meteostick
METEOSTICK_INFLUXDB_TOKEN=your_influxdb_token_here
```
### Configuration Options
| Variable | Default | Description |
|----------|---------|-------------|
| `METEOSTICK_PORT` | `/dev/ttyUSB0` | Serial port for Meteostick device |
| `METEOSTICK_BAUDRATE` | `115200` | Serial communication baud rate |
| `METEOSTICK_FREQUENCY` | `EU` | RF frequency: `US`, `EU`, or `AU` |
| `METEOSTICK_RF_SENSITIVITY` | `90` | RF sensitivity (0-125) |
| `METEOSTICK_ISS_CHANNEL` | `1` | ISS transmitter channel (1-8) |
| `METEOSTICK_ANEMOMETER_CHANNEL` | `0` | Anemometer channel (0=disabled, 1-8) |
| `METEOSTICK_LEAF_SOIL_CHANNEL` | `0` | Leaf/soil station channel (0=disabled, 1-8) |
| `METEOSTICK_TH1_CHANNEL` | `0` | T/H sensor 1 channel (0=disabled, 1-8) |
| `METEOSTICK_TH2_CHANNEL` | `0` | T/H sensor 2 channel (0=disabled, 1-8) |
| `METEOSTICK_RAIN_BUCKET` | `1` | Rain bucket type: 0=0.01in, 1=0.2mm |
| `METEOSTICK_ENABLE_INFLUXDB` | `false` | Enable InfluxDB data logging |
| `METEOSTICK_INFLUXDB_HOST` | `localhost` | InfluxDB server hostname |
| `METEOSTICK_INFLUXDB_PORT` | `8086` | InfluxDB server port |
| `METEOSTICK_INFLUXDB_DB` | `weather` | InfluxDB database name |
| `METEOSTICK_INFLUXDB_MEASUREMENT` | `meteostick` | InfluxDB measurement name |
| `METEOSTICK_INFLUXDB_TOKEN` | - | InfluxDB authentication token |
## Usage
### Direct Python Execution
```bash
# Basic usage with console output only
python meteostick_reader.py --port /dev/ttyACM0
# With InfluxDB logging
python meteostick_reader.py --port /dev/ttyACM0 --enable-influxdb --influxdb-token "your_token"
# All options via command line
python meteostick_reader.py \
--port /dev/ttyACM0 \
--freq EU \
--iss-channel 1 \
--enable-influxdb \
--influxdb-host influxdb.example.com \
--influxdb-token "your_token"
```
### Docker Compose
```bash
# Start the service
docker-compose up -d
# View logs
docker-compose logs -f
# Stop the service
docker-compose down
```
## Output Format
### Console Output
The application displays real-time weather data:
```
[2023-12-07 14:30:15] Message #123: I 1 0 48 89 8A 12 34 56 78 9A BC DE 95 1234567 0
Parsed data: {'channel': 1, 'temperature': 18.5, 'humidity': 65.0, 'wind_speed': 2.3, 'wind_dir': 225.0, 'rf_signal': 95}
```
### InfluxDB Data
Data is stored in InfluxDB with the following structure:
- **Measurement**: Configurable (default: `meteostick`)
- **Tags**: `channel` (transmitter channel)
- **Fields**: Weather data (temperature, humidity, wind_speed, wind_dir, pressure, etc.)
- **Timestamp**: UTC timestamp when data was received
## Troubleshooting
### Serial Port Issues
- Ensure the Meteostick device is connected and recognized by the system
- Check permissions: `sudo usermod -a -G dialout $USER`
- Verify the correct port with `ls /dev/tty*`
### InfluxDB Connection Issues
- Verify InfluxDB is running and accessible
- Check authentication token validity
- Ensure the database exists or the token has creation permissions
### No Weather Data
- Verify weather station transmitters are within range
- Check RF sensitivity settings
- Ensure correct channel configuration
- Verify transmitter battery levels
## License
This project is based on the WeewX meteostick driver and is released under the same license terms.
## Contributing
1. Fork the repository
2. Create a feature branch
3. Make your changes
4. Test thoroughly
5. Submit a pull request
## Support
For issues and questions:
1. Check the troubleshooting section
2. Review Davis Instruments documentation
3. Open an issue on the project repository

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