Commit f159cd8f authored by Alan Mitchell's avatar Alan Mitchell

Model Methods to support Alert Feature

And a bug fix when a building has no data in the normalized by dd and
ft2 report.
parent 9183d4c2
......@@ -7,5 +7,6 @@ settings.py
*.swf
*.gz
*.bak
*.ipynb
**/.ipynb_checkpoints
.idea
......@@ -57,6 +57,9 @@ BMSAPP_PROJ_NAME = 'bmon'
# further down in this settings file.
BMSAPP_STATIC_APP_NAME = 'bmon_static'
# The number of hours before a sensor is considered to be inactive (not posting data).
BMSAPP_SENSOR_INACTIVITY = 2.0 # Hours
# If you are using the Pushover notification service for alerts generated by the BMON
# application, you need to register an Application with Pushover and enter the API
# Token/Key below. It is a 30 character string. See 'https://pushover.net'.
......
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
import django.core.validators
class Migration(migrations.Migration):
dependencies = [
('bmsapp', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='AlertCondition',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('active', models.BooleanField(default=True, help_text=b'Uncheck the box to Disable the alert.')),
('condition', models.CharField(max_length=20, verbose_name=b'Notify when the Sensor value is', choices=[(b'>', b'>'), (b'>=', b'>='), (b'<', b'<'), (b'<=', b'<='), (b'==', b'equal to'), (b'!=', b'not equal to'), (b'inactive', b'inactive')])),
('test_value', models.FloatField(null=True, verbose_name=b'this value', blank=True)),
('alert_message', models.TextField(help_text=b'If left blank, a message will be created. Use the string "{val}" in the message to show the current sensor value.', max_length=200, blank=True)),
('priority', models.CharField(default=b'0', max_length=5, verbose_name=b'Priority of this Alert Situation', choices=[(b'-1', b'Low'), (b'0', b'Normal'), (b'1', b'High'), (b'2', b'Emergency')])),
('wait_before_next', models.FloatField(default=4.0, verbose_name=b'Hours to Wait before Notifying Again')),
('last_notified', models.FloatField(null=True, blank=True)),
('only_if_bldg', models.ForeignKey(verbose_name=b'But only if building', blank=True, to='bmsapp.Building', null=True)),
],
options={
},
bases=(models.Model,),
),
migrations.CreateModel(
name='AlertRecipient',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('active', models.BooleanField(default=True)),
('name', models.CharField(max_length=50)),
('notify_email', models.BooleanField(default=True, verbose_name=b'Send Email?')),
('email_address', models.EmailField(max_length=100, blank=True)),
('notify_cell', models.BooleanField(default=True, verbose_name=b'Send Text Message?')),
('cell_number', models.CharField(blank=True, max_length=10, verbose_name=b'10 digit Cell number', validators=[django.core.validators.RegexValidator(regex=b'^\\d{10}$', message=b'Phone number must be entered as a 10 digit number, including area code, no spaces, dashes or parens.')])),
('cell_sms_gateway', models.CharField(blank=True, max_length=60, verbose_name=b'Cell Phone Carrier', choices=[(b'msg.acsalaska.com', b'Alaska Communications (ACS)'), (b'txt.att.net', b'AT&T'), (b'mobile.gci.net', b'General Communications Inc. (GCI)'), (b'sms.mtawireless.com', b'MTA Wireless'), (b'vtext.com', b'Verizon Wireless')])),
('notify_pushover', models.BooleanField(default=True, verbose_name=b'Send Pushover Notification?')),
('pushover_id', models.CharField(blank=True, max_length=30, verbose_name=b'Pushover ID', validators=[django.core.validators.RegexValidator(regex=b'^\\w{30}$', message=b'Pushover ID should be exactly 30 characters long.')])),
],
options={
'ordering': ['name'],
},
bases=(models.Model,),
),
migrations.CreateModel(
name='BuildingMode',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('name', models.CharField(max_length=50, verbose_name=b'Mode Name')),
],
options={
'ordering': ['name'],
},
bases=(models.Model,),
),
migrations.AddField(
model_name='alertcondition',
name='only_if_bldg_mode',
field=models.ForeignKey(verbose_name=b'is in this mode', blank=True, to='bmsapp.BuildingMode', null=True),
preserve_default=True,
),
migrations.AddField(
model_name='alertcondition',
name='recipients',
field=models.ManyToManyField(to='bmsapp.AlertRecipient', verbose_name=b'Who should be notified?'),
preserve_default=True,
),
migrations.AddField(
model_name='alertcondition',
name='sensor',
field=models.ForeignKey(to='bmsapp.Sensor'),
preserve_default=True,
),
migrations.RemoveField(
model_name='dashboarditem',
name='generate_alert',
),
migrations.RemoveField(
model_name='dashboarditem',
name='no_alert_end_date',
),
migrations.RemoveField(
model_name='dashboarditem',
name='no_alert_start_date',
),
migrations.AddField(
model_name='building',
name='current_mode',
field=models.ForeignKey(verbose_name=b'Current Operating Mode', blank=True, to='bmsapp.BuildingMode', null=True),
preserve_default=True,
),
]
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('bmsapp', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='sensor',
name='tran_calc_function',
field=models.CharField(max_length=80, verbose_name=b'Transform or Calculated Field Function Name', blank=True),
preserve_default=True,
),
]
import time
from django.db import models
from django.core.validators import RegexValidator
from django.conf import settings
import bmsapp.data_util
import sms_gateways
......@@ -82,6 +85,28 @@ class Sensor(models.Model):
class Meta:
ordering = ['sensor_id']
def last_read(self, reading_db):
'''Returns the last reading from the sensor as a dictionary with 'ts' and 'val' keys.
Returns None if there have been no readings. 'reading_db' is a sensor reading database,
an instance of bmsapp.readingdb.bmsdata.BMSdata.
'''
return reading_db.last_read(self.sensor_id)
def is_active(self, reading_db):
'''Returns True if the sensor has last posted within the sensor
activity interval specified in the settings file. 'reading_db' is a sensor reading
database, an instance of bmsapp.readingdb.bmsdata.BMSdata.
'''
last_read = self.last_read(reading_db)
if last_read is not None:
# get inactivity setting from settings file
inactivity_hrs = getattr(settings, 'BMSAPP_SENSOR_INACTIVITY', 2.0)
last_post_hrs = (time.time() - last_read['ts']) / 3600.0
return (last_post_hrs <= inactivity_hrs)
else:
# no readings in database for this sensor
return False
class BuildingMode(models.Model):
'''A state or mode that the building could be in, such as Winter, Summer, Vacant.
......@@ -358,7 +383,7 @@ class AlertCondition(models.Model):
('inactive', 'inactive'),
)
# conditional type to evaluate for the sensor value
condition = models.CharField('Notify when the Sensor value is', max_length=20, choices=CONDITION_CHOICES)
condition = models.CharField('Notify when the Sensor value is', max_length=20, default='>', choices=CONDITION_CHOICES)
# the value to test the current sensor value against
test_value = models.FloatField(verbose_name='this value', blank=True, null=True)
......@@ -395,7 +420,82 @@ class AlertCondition(models.Model):
# when the last notification of this alert condition was sent out, Unix timestamp.
# This is filled out when alert conditions are evaluated and is not accessible in the Admin
# interface.
last_notified = models.FloatField(blank=True, null=True)
last_notified = models.FloatField(default=0.0)
def __unicode__(self):
return '%s %s %s' % (self.sensor.title, self.condition, self.test_value)
def check_condition(self, reading_db):
'''This method checks to see if the alert condition is in effect, and if so,
returns a message describing the alert. If the condition is not in effect,
None is returned. 'reading_db' is a sensor reading database, an instance of
bmsapp.readingdb.bmsdata.BMSdata. If the alert condition is not active, None
is returned.
'''
if not self.active:
return None # condition not active
# Make a description of the sensor that includes the building(s) it is
# associated with.
bldgs = [btos.building.title for btos in BldgToSensor.objects.filter(sensor__pk=self.sensor.pk)]
bldgs_str = ', '.join(bldgs)
sensor_desc = '%s sensor in %s' % (self.sensor.title, bldgs_str)
# get the most current reading for the sensor
last_read = self.sensor.last_read(reading_db)
# if the condition test is for an inactive sensor, do that test now.
# Do not consider the building mode test for this test.
if self.condition=='inactive' and not self.sensor.is_active(reading_db):
if last_read:
msg = 'The last reading from the %s was %.1f hours ago.' % \
( sensor_desc, (time.time() - last_read['ts'])/3600.0 )
else:
msg = 'The %s has never posted a reading.' % sensor_desc
return msg
# If there are no readings for this sensor return
if last_read is None:
return None
# Now look at the value test, considering the building mode if specified.
val_test = eval( '%s %s %s' % (last_read['val'], self.condition, self.test_value) )
if self.only_if_bldg is not None and self.only_if_bldg_mode is not None:
bldg_mode_test = (self.only_if_bldg.current_mode == self.only_if_bldg_mode)
else:
bldg_mode_test = True
if val_test and bldg_mode_test:
# Value test is in effect, return a message
if self.alert_message.strip():
msg = self.alert_message.strip()
if msg[-1] != '.':
msg += '.'
msg = '%s The current sensor reading is %s %s' % \
(msg, bmsapp.data_util.formatCurVal(last_read['val']), self.sensor.unit.label)
else:
# find the text for the condition
for val, text in AlertCondition.CONDITION_CHOICES:
if self.condition == val:
condition_text = text
msg = 'The %s has a current reading of %s %s, which is %s %s %s.' % \
(
sensor_desc,
bmsapp.data_util.formatCurVal(last_read['val']),
self.sensor.unit.label,
condition_text,
self.test_value,
self.sensor.unit.label
)
return msg
else:
return None
def wait_satisfied(self):
'''Returns True if there has been enough wait between the last notification
for this condition and now.
'''
return (time.time() >= self.last_notified + self.wait_before_next * 3600.0)
\ No newline at end of file
......@@ -75,6 +75,9 @@ class NormalizedByDDbyFt2(basechart.BaseChart):
# inner join, matching timestamps
df = df.join(df_temp, how='inner')
if len(df)==0:
continue
# make sure the data spans at least 80% of the requested interval.
# if not, skip this building.
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment