mirror of
https://github.com/weewx/weewx.git
synced 2026-04-18 08:36:54 -04:00
make target `pypi-packages` copies them back into `wee_resources` before invoking `poetry build`
279 lines
10 KiB
Python
279 lines
10 KiB
Python
# Copyright (c) 2009-2023 Tom Keffer <tkeffer@gmail.com>
|
|
# See the file LICENSE.txt for your rights.
|
|
|
|
"""Example of how to implement a low battery alarm in WeeWX.
|
|
|
|
*******************************************************************************
|
|
|
|
To use this alarm, add the following somewhere in your configuration file
|
|
weewx.conf:
|
|
|
|
[Alarm]
|
|
time_wait = 3600
|
|
count_threshold = 10
|
|
smtp_host = smtp.example.com
|
|
smtp_user = myusername
|
|
smtp_password = mypassword
|
|
from = sally@example.com
|
|
mailto = jane@example.com, bob@example.com
|
|
subject = "Time to change the battery!"
|
|
|
|
An email will be sent to each address in the comma separated list of recipients
|
|
|
|
The example assumes an SMTP email server at smtp.example.com that requires
|
|
login. If the SMTP server does not require login, leave out the lines for
|
|
smtp_user and smtp_password.
|
|
|
|
Setting an email "from" is optional. If not supplied, one will be filled in,
|
|
but your SMTP server may or may not accept it.
|
|
|
|
Setting an email "subject" is optional. If not supplied, one will be filled in.
|
|
|
|
To avoid a flood of emails, one will only be sent every 3600 seconds (one
|
|
hour).
|
|
|
|
It will also not send an email unless the low battery indicator has been on
|
|
greater than or equal to count_threshold times in an archive period. This
|
|
avoids sending out an alarm if the battery is only occasionally being signaled
|
|
as bad.
|
|
|
|
*******************************************************************************
|
|
|
|
To enable this service:
|
|
|
|
1) Copy this file to your user directory. For pip install, this directory is
|
|
at ~/weewx-data/bin/user. For package installs, it's /usr/share/weewx/user.
|
|
|
|
2) Modify the weewx configuration file by adding this service to the option
|
|
"report_services", located in section [Engine][[Services]].
|
|
|
|
[Engine]
|
|
[[Services]]
|
|
...
|
|
report_services = weewx.engine.StdPrint, weewx.engine.StdReport, user.lowBattery.BatteryAlarm
|
|
|
|
*******************************************************************************
|
|
|
|
If you wish to use both this example and the alarm.py example, simply merge the
|
|
two configuration options together under [Alarm] and add both services to
|
|
report_services.
|
|
|
|
*******************************************************************************
|
|
"""
|
|
|
|
import logging
|
|
import time
|
|
import smtplib
|
|
from email.mime.text import MIMEText
|
|
import threading
|
|
|
|
import weewx
|
|
from weewx.engine import StdService
|
|
from weeutil.weeutil import timestamp_to_string, option_as_list
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
# Inherit from the base class StdService:
|
|
class BatteryAlarm(StdService):
|
|
"""Service that sends email if one of the batteries is low"""
|
|
|
|
battery_flags = ['txBatteryStatus', 'windBatteryStatus',
|
|
'rainBatteryStatus', 'inTempBatteryStatus',
|
|
'outTempBatteryStatus']
|
|
|
|
def __init__(self, engine, config_dict):
|
|
# Pass the initialization information on to my superclass:
|
|
super(BatteryAlarm, self).__init__(engine, config_dict)
|
|
|
|
# This will hold the time when the last alarm message went out:
|
|
self.last_msg_ts = 0
|
|
# This will hold the count of the number of times the VP2 has signaled
|
|
# a low battery alarm this archive period
|
|
self.alarm_count = 0
|
|
|
|
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.time_wait = int(config_dict['Alarm'].get('time_wait', 3600))
|
|
self.count_threshold = int(config_dict['Alarm'].get('count_threshold', 10))
|
|
self.smtp_host = config_dict['Alarm']['smtp_host']
|
|
self.smtp_user = config_dict['Alarm'].get('smtp_user')
|
|
self.smtp_password = config_dict['Alarm'].get('smtp_password')
|
|
self.SUBJECT = config_dict['Alarm'].get('subject', "Low battery alarm message from weewx")
|
|
self.FROM = config_dict['Alarm'].get('from', 'alarm@example.com')
|
|
self.TO = option_as_list(config_dict['Alarm']['mailto'])
|
|
except KeyError as e:
|
|
log.info("No alarm set. Missing parameter: %s", e)
|
|
else:
|
|
# If we got this far, it's ok to start intercepting events:
|
|
self.bind(weewx.NEW_LOOP_PACKET, self.new_loop_packet)
|
|
self.bind(weewx.NEW_ARCHIVE_RECORD, self.new_archive_record)
|
|
log.info("LowBattery alarm enabled. Count threshold is %d", self.count_threshold)
|
|
|
|
def new_loop_packet(self, event):
|
|
"""This function is called on each new LOOP packet."""
|
|
|
|
packet = event.packet
|
|
|
|
# If any battery status flag is non-zero, a battery is low. Use dictionary comprehension
|
|
# to build a new dictionary that just holds the non-zero values.
|
|
low_batteries = {k : packet[k] for k in BatteryAlarm.battery_flags
|
|
if k in packet and packet[k]}
|
|
|
|
# If there are any low batteries, see if we need to send an alarm
|
|
if low_batteries:
|
|
self.alarm_count += 1
|
|
|
|
# Don't panic on the first occurrence. We must see the alarm at
|
|
# least count_threshold times before sounding the alarm.
|
|
if self.alarm_count >= self.count_threshold:
|
|
# We've hit the threshold. However, to avoid a flood of nearly
|
|
# identical emails, send a new one only if it's been a long
|
|
# time since we sent the last one:
|
|
if abs(time.time() - self.last_msg_ts) >= self.time_wait :
|
|
# Sound the alarm!
|
|
timestamp = event.packet['dateTime']
|
|
# Launch in a separate thread so it does not block the
|
|
# main LOOP thread:
|
|
t = threading.Thread(target=BatteryAlarm.sound_the_alarm,
|
|
args=(self, timestamp,
|
|
low_batteries,
|
|
self.alarm_count))
|
|
t.start()
|
|
# Record when the message went out:
|
|
self.last_msg_ts = time.time()
|
|
|
|
def new_archive_record(self, event):
|
|
"""This function is called on each new archive record."""
|
|
|
|
# Reset the alarm counter
|
|
self.alarm_count = 0
|
|
|
|
def sound_the_alarm(self, timestamp, battery_flags, alarm_count):
|
|
"""This function is called when the alarm has been triggered."""
|
|
|
|
# Get the time and convert to a string:
|
|
t_str = timestamp_to_string(timestamp)
|
|
|
|
# Log it in the system log:
|
|
log.info("Low battery status sounded at %s: %s" % (t_str, battery_flags))
|
|
|
|
# Form the message text:
|
|
indicator_strings = []
|
|
for bat in battery_flags:
|
|
indicator_strings.append("%s: %04x" % (bat, int(battery_flags[bat])))
|
|
msg_text = """
|
|
The low battery indicator has been seen %d times since the last archive period.
|
|
|
|
Alarm sounded at %s
|
|
|
|
Low battery indicators:
|
|
%s
|
|
|
|
""" % (alarm_count, t_str, '\n'.join(indicator_strings))
|
|
# Convert to MIME:
|
|
msg = MIMEText(msg_text)
|
|
|
|
# Fill in MIME headers:
|
|
msg['Subject'] = self.SUBJECT
|
|
msg['From'] = self.FROM
|
|
msg['To'] = ','.join(self.TO)
|
|
|
|
try:
|
|
# First try end-to-end encryption
|
|
s=smtplib.SMTP_SSL(self.smtp_host)
|
|
log.debug("Using SMTP_SSL")
|
|
except AttributeError:
|
|
# If that doesn't work, try creating an insecure host, then upgrading
|
|
s = smtplib.SMTP(self.smtp_host)
|
|
try:
|
|
# Be prepared to catch an exception if the server
|
|
# does not support encrypted transport.
|
|
s.ehlo()
|
|
s.starttls()
|
|
s.ehlo()
|
|
log.debug("Using SMTP encrypted transport")
|
|
except smtplib.SMTPException:
|
|
log.debug("Using SMTP unencrypted transport")
|
|
|
|
try:
|
|
# 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)
|
|
log.debug("Logged in as %s", self.smtp_user)
|
|
|
|
# Send the email:
|
|
s.sendmail(msg['From'], self.TO, msg.as_string())
|
|
# Log out of the server:
|
|
s.quit()
|
|
except Exception as e:
|
|
log.error("Send email failed: %s", e)
|
|
raise
|
|
|
|
# Log sending the email:
|
|
log.info("Email sent to: %s", self.TO)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
"""This section is used to test lowBattery.py. It uses a record that is guaranteed to
|
|
sound a battery alert.
|
|
|
|
You will need a valid weewx.conf configuration file with an [Alarm]
|
|
section that has been set up as illustrated at the top of this file."""
|
|
|
|
from optparse import OptionParser
|
|
import weecfg
|
|
import weeutil.logger
|
|
|
|
usage = """Usage: python lowBattery.py --help
|
|
python lowBattery.py [CONFIG_FILE|--config=CONFIG_FILE]
|
|
|
|
Arguments:
|
|
|
|
CONFIG_PATH: Path to weewx.conf """
|
|
|
|
# Force debug:
|
|
weewx.debug = 1
|
|
|
|
# Create a command line parser:
|
|
parser = OptionParser(usage=usage)
|
|
parser.add_option("--config", dest="config_path", metavar="CONFIG_FILE",
|
|
help="Use configuration file CONFIG_FILE.")
|
|
# Parse the arguments and options
|
|
(options, args) = parser.parse_args()
|
|
|
|
try:
|
|
config_path, config_dict = weecfg.read_config(options.config_path, args)
|
|
except IOError as e:
|
|
exit("Unable to open configuration file: %s" % e)
|
|
|
|
print("Using configuration file %s" % config_path)
|
|
|
|
# Set logging configuration:
|
|
weeutil.logger.setup('lowBattery', config_dict)
|
|
|
|
if 'Alarm' not in config_dict:
|
|
exit("No [Alarm] section in the configuration file %s" % config_path)
|
|
|
|
# This is the fake packet that we'll use
|
|
pack = {'txBatteryStatus': 1.0,
|
|
'dateTime': int(time.time())}
|
|
|
|
# We need the main WeeWX engine in order to bind to the event, but we don't need
|
|
# for it to completely start up. So get rid of all services:
|
|
config_dict['Engine']['Services'] = {}
|
|
# Now we can instantiate our slim engine, using the DummyEngine class...
|
|
engine = weewx.engine.DummyEngine(config_dict)
|
|
# ... and set the alarm using it.
|
|
alarm = BatteryAlarm(engine, config_dict)
|
|
|
|
# Create a NEW_LOOP_PACKET event
|
|
event = weewx.Event(weewx.NEW_LOOP_PACKET, packet=pack)
|
|
|
|
# Trigger the alarm enough that we reach the threshold
|
|
for count in range(alarm.count_threshold):
|
|
alarm.new_loop_packet(event)
|