#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
bom_minmax.py - WeeWX service to record BoM-compliant min/max temperatures
This service records minimum and maximum temperatures between 6pm the previous
day and 9am the current day, storing them at 9am in a dedicated table.
"""
import datetime
import time
import weedb
import weewx
from weewx.engine import StdService
# Try to use new-style WeeWX V5 logging
try:
import logging
log = logging.getLogger(__name__)
def logdbg(msg):
log.debug(msg)
def loginf(msg):
log.info(msg)
def logerr(msg):
log.error(msg)
except (ImportError, AttributeError):
# Use old-style WeeWX V4 logging via syslog
import syslog
def logmsg(level, msg):
syslog.syslog(level, 'bom_minmax: %s' % msg)
def logdbg(msg):
logmsg(syslog.LOG_DEBUG, msg)
def loginf(msg):
logmsg(syslog.LOG_INFO, msg)
def logerr(msg):
logmsg(syslog.LOG_ERR, msg)
# BoM observation time configuration
BOM_MORNING_HOUR = 9 # 9am
BOM_EVENING_HOUR = 18 # 6pm
class BomMinMaxService(StdService):
"""Service to record min/max temperatures in BoM format."""
def __init__(self, engine, config_dict):
super(BomMinMaxService, self).__init__(engine, config_dict)
loginf("Starting BoM Min/Max Temperature Service")
# Get the database configuration
self.db_dict = config_dict.get('BomMinMax', {})
# Archive database binding - where to get temperature data from
self.archive_binding = self.db_dict.get('archive_binding', 'wx_binding')
# Output database binding - where to store BoM min/max data
self.output_binding = self.db_dict.get('output_binding', 'wx_binding')
# Table name for BoM min/max data
self.table_name = self.db_dict.get('table_name', 'archive_bom_minmax')
# Temperature field to use
self.temp_field = self.db_dict.get('temp_field', 'outTemp')
# Initialize the table if it doesn't exist
# Commented out for stability
#self.init_table()
# Bind to archive events
self.bind(weewx.NEW_ARCHIVE_RECORD, self.new_archive_record)
loginf("BoM Min/Max Temperature Service initialized")
def init_table(self):
"""Initialize the table if it doesn't exist."""
try:
with self.engine.db_binder.get_manager(self.output_binding) as dbm:
# Check if table exists
if self.table_name not in dbm.connection.tables():
loginf(f"Creating table {self.table_name}")
# Create the table
dbm.connection.execute(
f"CREATE TABLE {self.table_name} ("
f"dateTime INTEGER NOT NULL PRIMARY KEY, "
f"usUnits INTEGER NOT NULL, "
f"`interval` INTEGER NOT NULL, "
f"min_temp DOUBLE, "
f"min_temp_time INTEGER, "
f"max_temp DOUBLE, "
f"max_temp_time INTEGER)"
)
except Exception as e:
logerr(f"Error initializing table: {e}")
def new_archive_record(self, event):
"""Called when a new archive record is available.
Check if it's just after 9am and if so, record the min/max data
for the 6pm-9am period.
"""
timestamp = event.record['dateTime']
dt = datetime.datetime.fromtimestamp(timestamp)
# Only process records at or just after 9am
if dt.hour == BOM_MORNING_HOUR and dt.minute < 10:
self.process_bom_minmax(timestamp)
def process_bom_minmax(self, current_ts):
"""Process and record BoM min/max temperatures.
Args:
current_ts: Current timestamp (around 9am)
"""
# Convert timestamp to datetime
current_dt = datetime.datetime.fromtimestamp(current_ts)
# Calculate the timestamp for 6pm yesterday
yesterday = current_dt - datetime.timedelta(days=1)
yesterday_6pm = datetime.datetime(yesterday.year, yesterday.month,
yesterday.day, BOM_EVENING_HOUR, 0, 0)
start_ts = int(yesterday_6pm.timestamp())
# Calculate the timestamp for 9am today
today_9am = datetime.datetime(current_dt.year, current_dt.month,
current_dt.day, BOM_MORNING_HOUR, 0, 0)
end_ts = int(today_9am.timestamp())
loginf(f"Processing BoM min/max data for {yesterday_6pm.strftime('%Y-%m-%d %H:%M')} to {today_9am.strftime('%Y-%m-%d %H:%M')}")
try:
# Get the temperature data from the archive
with self.engine.db_binder.get_manager(self.archive_binding) as dbm_archive:
# Get min temperature
min_temp_data = dbm_archive.getSql(
f"SELECT MIN({self.temp_field}), dateTime, usUnits FROM archive "
f"WHERE dateTime >= {start_ts} AND dateTime <= {end_ts} "
f"AND {self.temp_field} IS NOT NULL")
# Get max temperature
max_temp_data = dbm_archive.getSql(
f"SELECT MAX({self.temp_field}), dateTime, usUnits FROM archive "
f"WHERE dateTime >= {start_ts} AND dateTime <= {end_ts} "
f"AND {self.temp_field} IS NOT NULL")
# Skip if no data available
if not min_temp_data or not max_temp_data:
loginf("Insufficient temperature data for period, skipping")
return
# Create the record
record = {
'dateTime': end_ts,
'usUnits': min_temp_data[2], # Use the same unit system from archive
'interval': 1440, # 24 hours in minutes
'min_temp': min_temp_data[0],
'min_temp_time': min_temp_data[1],
'max_temp': max_temp_data[0],
'max_temp_time': max_temp_data[1]
}
# Store in the output database
with self.engine.db_binder.get_manager(self.output_binding) as dbm_output:
dbm_output.addRecord(record)
# Log the min/max data
min_temp_time = datetime.datetime.fromtimestamp(min_temp_data[1])
max_temp_time = datetime.datetime.fromtimestamp(max_temp_data[1])
loginf(f"Recorded BoM min/max: Min {min_temp_data[0]:.1f}°C at {min_temp_time.strftime('%H:%M')}, "
f"Max {max_temp_data[0]:.1f}°C at {max_temp_time.strftime('%H:%M')}")
except Exception as e:
logerr(f"Error processing BoM min/max: {e}")
# To use this service, add to weewx.conf:
#
# [BomMinMax]
# # Database binding to read temperature data from
# archive_binding = wx_binding
# # Database binding to store BoM min/max data (can be same as archive_binding)
# output_binding = wx_binding
# # Table name for BoM min/max data
# table_name = archive_bom_minmax
# # Temperature field to monitor (default: outTemp)
# temp_field = outTemp
#
# [Engine]
# [[Services]]
# data_services = ..., user.bom_minmax.BomMinMaxService, ...