At a high level, weewx consists of an engine that is responsible for managing a set of services. A service consists of a Python class with a set of member functions. The engine arranges to have appropriate member functions called when specific events happen. For example, when a new LOOP packet arrives, member function processLoopPacket() of all services is called.
To customize, you can
This document describes how to do all three.
The default install of weewx includes the following services:
Service | Function |
weewx.wxengine.StdWunderground | Starts thread to manage WU connection; adds new data to a Queue to be posted to the WU by the thread. |
weewx.wxengine.StdCatchUp | Any data found on the weather station memory but not yet in the archive, is retrieved and put in the archive. |
weewx.wxengine.StdTimeSynch | Arranges to have the clock on the station synchronized at regular intervals. |
weewx.wxengine.StdPrint | Prints out new LOOP and archive packets on the console. |
weewx.wxengine.StdProcess | Launches a new thread to do processing after a new archive record arrives. The thread loads zero or more reports and processes them in order. Reports do things such as generate HTML files, generate images, or FTP files to a web server. New reports can be added easily by the user. |
The service weewx.wxengine.StdPrint prints out new LOOP and archive packets to the console when they arrive. By default, it prints out time, barometer, outside temperature, wind speed, and wind direction. Suppose you don't like this, and want to add humidity. This could be done by subclassing the default print service StdPrint and overriding member function processLoopPacket().
In file myprint.py:
from weewx.wxengine import StdPrint
from weeutil.weeutil import timestamp_to_string
class MyPrint(StdPrint):
# Override the default processLoopPacket:
def processLoopPacket(self, physicalPacket):
print "LOOP: ", timestamp_to_string(physicalPacket['dateTime']),\
physicalPacket['barometer'],\
physicalPacket['outTemp'],\
physicalPacket['outHumidity'],\
physicalPacket['windSpeed'],\
physicalPacket['windDir']
You then need to specify that your print service class should be loaded instead of the default StdPrint service. This is done by substituting your service name for the standard print service name in the option service_list, located in [Engines][[WxEngine]]:
[Engines]
[[WxEngine]]
service_list = weewx.wxengine.StdWunderground, weewx.wxengine.StdCatchUp,
weewx.wxengine.StdTimeSynch, myprint.MyPrint,
weewx.wxengine.StdProcess
(Note that this list is shown on several lines for clarity, but in actuality it must be all on one line. The parser ConfigObj does not allow options to be continued on to following lines.)
Suppose there is no service that can easily be customized for your needs. In this case, a new one can easily be created by subclassing off the abstract base class StdService, and then adding the functionality you need. Here's an example that implements an alarm. It is included in the standard distribution in subdirectory 'examples.'
File examples/alarm.py:
import time
import smtplib
from email.mime.text import MIMEText
import threading
import syslog
from weewx.wxengine import StdService
from weeutil.weeutil import timestamp_to_string
# Inherit from the base class StdService:
class MyAlarm(StdService):
"""Custom service that sounds an alarm if an expression
evaluates true"""
def __init__(self, engine):
# Pass the initialization information
on to my superclass:
StdService.__init__(self, engine)
# This will hold the time when the
last alarm message went out:
self.last_msg = None
self.expression = None
def setup(self):
try:
# Dig the
needed options out of the configuration dictionary.
# If a
critical option is missing, an exception will be thrown and
# the alarm
will not be set.
self.expression = self.engine.config_dict['Alarm']['expression']
self.time_wait = int(self.engine.config_dict['Alarm'].get('time_wait', '3600'))
self.smtp_host = self.engine.config_dict['Alarm']['smtp_host']
self.smtp_user = self.engine.config_dict['Alarm'].get('smtp_user')
self.smtp_password = self.engine.config_dict['Alarm'].get('smtp_password')
self.TO =
self.engine.config_dict['Alarm']['mailto']
syslog.syslog(syslog.LOG_INFO,
"alarm: Alarm set for expression %s" % self.expression)
except:
self.expression = None
self.time_wait = None
def postArchiveData(self, rec):
# Let the super class see the record
first:
StdService.postArchiveData(self, rec)
# See if the alarm has been set:
if self.expression:
# To avoid a
flood of nearly identical emails, this will do
# the check
only if we have never sent an email, or if we haven't
# sent one in
the last self.time_wait seconds:
if not
self.last_msg or abs(time.time() - self.last_msg) >= self.time_wait :
# Evaluate the expression in the context of 'rec'.
# Sound the alarm if it evaluates true:
if eval(self.expression, None, rec):
# Sound the alarm!
# Launch in a separate thread so it doesn't block the main LOOP thread:
t = threading.Thread(target = MyAlarm.soundTheAlarm, args=(self, rec))
t.start()
def soundTheAlarm(self, rec):
"""This function is called when the
given expression evaluates True."""
# Get the time and convert to a
string:
t_str = timestamp_to_string(rec['dateTime'])
# Form the message text:
msg_text = "Alarm expression %s
evaluated True at %s\nRecord:\n%s" % (self.expression, t_str, str(rec))
# Convert to MIME:
msg = MIMEText(msg_text)
# Fill in MIME headers:
msg['Subject'] = "Alarm message from
weewx"
msg['From'] = "weewx"
msg['To'] = self.TO
# Create an instance of class SMTP
for the given SMTP host:
s = smtplib.SMTP(self.smtp_host)
# If a username has been given,
assume that login is required for this host:
if self.smtp_user:
s.login(self.smtp_user,
self.smtp_password)
# Send the email:
s.sendmail(msg['From'], [self.TO],
msg.as_string())
# Log out of the server:
s.quit()
# Record when the message went out:
self.last_msg = time.time()
# Log it in the system log:
syslog.syslog(syslog.LOG_INFO,
"alarm: Alarm sounded for expression %s" % self.expression)
syslog.syslog(syslog.LOG_INFO, " ***
email sent to: %s" % self.TO)
This service expects all the information it needs to be in the configuration file weewx.conf in a new section called [Alarm]. So, add the following lines to your configuration file:
[Alarm]
expression = "outTemp < 40.0"
time_wait = 1800
smtp_host = smtp.mymailserver.com
smtp_user = myusername
smtp_password = mypassword
mailto = auser@adomain.com
These options specify that the alarm is to be sounded when "outTemp < 40.0" evaluates True, that is when the outside temperature is below 40.0 degrees. Any valid Python expression can be used, although the only variables available are those in the current archive record.
Another example expression could be:
expression = "outTemp < 32.0 and windSpeed > 10.0"
In this case, the alarm is sounded if the outside temperature drops below freezing and the wind speed is greater than 10.0.
Option time_wait is used to avoid a flood of nearly identical emails. The new service will wait this long before sending another email out.
Email will be sent through the SMTP host specified by option smtp_host. The recipient is specified in option mailto.
Many SMTP hosts require user login. If this is the case, the user and password are specified with options smtp_user and smtp_password, respectively.
To make this all work, you must tell the engine to load this new service. This is done by adding your service name to the list service_list, located in [Engines][[WxEngine]]:
[Engines]
[[WxEngine]]
service_list = weewx.wxengine.StdWunderground, weewx.wxengine.StdCatchUp,
weewx.wxengine.StdTimeSynch, weewx.wxengine.StdPrint,
weewx.wxengine.StdProcess, examples.alarm.MyAlarm
(Again, note that this list is shown on several lines for clarity, but in actuality it must be all on one line.)
In this section, we look at how to install a custom Engine. In general, this is the least desirable way to proceed, but in some cases it may be the only way to get what you want.
For example, suppose you want to define a new event for when the first archive of a day arrives. This can be done by extending the the standard engine.
This example is in file example/daily.py:
from weewx.wxengine import StdEngine, StdService
from weeutil.weeutil import startOfArchiveDay
class MyEngine(StdEngine):
"""A customized weewx engine."""
def __init__(self, *args, **vargs):
# Pass on the initialization data to
my superclass:
StdEngine.__init__(self, *args, **vargs)
# This will record the timestamp of
the old day
self.old_day = None
def postArchiveData(self, rec):
# First let my superclass process it:
StdEngine.postArchiveData(self, rec)
# Get the timestamp of the start of
the day using
# the utility function
startOfArchiveDay
dayStart_ts = startOfArchiveDay(rec['dateTime'])
# Call the function firstArchiveOfDay
if either this is
# the first archive since startup, or
if a new day has started
if not self.old_day or self.old_day
!= dayStart_ts:
self.old_day
= dayStart_ts
self.newDay(rec)
# Note 1
def newDay(self, rec):
"""Called when the first archive
record of a day arrives."""
# Go through the list of service
objects. This
# list is actually in my superclass
StdEngine.
for svc_obj in self.service_obj:
# Because
this is a new event, not all services will
# be prepared
to accept it. Check first to see if the
# service has
a member function "firstArchiveOfDay"
# before
calling it:
if hasattr(svc_obj,
"firstArchiveOfDay"): # Note 2
# The object does have the member function. Call it:
svc_obj.firstArchiveOfDay(rec)
This customized engine works by monitoring the arrival of archive records, and checking their time stamp (rec['dateTime']. It calculates the time stamp for the start of the day, and if it changes, calls member function newDay() (Note 1).
The member function newDay() then goes through the list of services (attribute self.service_obj). Because this engine is defining a new event (first archive of the day), the existing services may not be prepared to accept it. So, the engine checks each one to make sure it has a function firstArchiveOfDay before calling it (Note 2)
To use this engine, go into file weewxd.py and change the line
weewx.wxengine.main()
so that it uses your new engine:
from examples.daily import MyEngine
# Specify that my specialized engine should be used instead
# of the default:
weewx.wxengine.main(EngineClass = MyEngine)
We now have a new engine that defines a new event ("firstArchiveOfDay"), but there is no service to take advantage of it. We define a new service:
# Define a new service to take advantage of the new event
class DailyService(StdService):
"""This service can do something when the first archive
record of
a day arrives."""
def firstArchiveOfDay(self, rec):
"""Called when the first archive
record of a day arrives."""
print "The first archive of the day
has arrived!"
print rec
# You might want to do something here
like run a cron job
This service will simply print out a notice and then print out the new record. However, if there is some daily processing you want to do, perhaps a backup, or running utility wunderfixer, this would be the place to do it.
The final step is to go into your configuration file and specify that this new service be loaded, by adding its class name to option service_list:
[Engines]
[[WxEngine]]
# The list of services the main weewx engine should run:
service_list = weewx.wxengine.StdWunderground,
weewx.wxengine.StdCatchUp,
weewx.wxengine.StdTimeSynch, weewx.wxengine.StdPrint,
weewx.wxengine.StdProcess, examples.daily.DailyService
(Again, note that this list is shown on several lines for clarity, but in actuality it must be all on one line.)