Customizing weewx v1.5

Overview

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.

Customizing a Service

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 print out humidity as well when a new LOOP packet arrives, but leave the printing of archive packets alone. 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.)

Adding a Service

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 that sends off an email when an arbitrary expression evaluates True. This example 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):        # NOTE 1
                    # 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. (The place in the code where the expression is evaluated is marked with "Note 1".)

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.)

Customizing the Engine

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.)