Commit acd3c59b authored by Alan Mitchell's avatar Alan Mitchell

Merge branch 'master' into bare-server

parents 2bbbda90 9bfd63a9
......@@ -90,13 +90,14 @@ class AlertAdminInline(admin.StackedInline):
'''Used in the Sensor Admin to enter alerts.
'''
model = AlertCondition
template = "admin/stacked_alerts.html"
extra = 0
filter_horizontal = ('recipients',)
fieldsets = (
(None, {'fields': ( ('active', 'sensor'),
('condition', 'test_value', 'read_count'),
('only_if_bldg', 'only_if_bldg_mode'),
('only_if_bldg', 'only_if_bldg_mode','only_if_bldg_status'),
('alert_message',),
('priority', 'wait_before_next'),
('recipients',)
......@@ -243,6 +244,7 @@ class MultiBuildingChartAdmin(admin.ModelAdmin):
@admin.register(AlertRecipient)
class AlertRecipientAdmin(admin.ModelAdmin):
change_form_template = 'admin/AlertRecipient_change_form.html'
list_display = ('name', 'active' )
list_editable= ('active',)
fields = (
......
......@@ -6,6 +6,7 @@ from datetime import datetime
import pytz, calendar, time, math
from dateutil import parser
import numpy as np
import pandas as pd
from django.conf import settings
......@@ -94,7 +95,7 @@ def histogram_from_series(pandas_series):
# to 4 significant figures
return list(zip(avg_bins, cts))
def resample_timeseries(pandas_dataframe, averaging_hours, drop_na=True):
def resample_timeseries(pandas_dataframe, averaging_hours, use_rolling_averaging=False, drop_na=True):
'''
Returns a new pandas dataframe that is resampled at the specified "averaging_hours"
interval. If the 'averaging_hours' parameter is fractional, the averaging time
......@@ -117,7 +118,20 @@ def resample_timeseries(pandas_dataframe, averaging_hours, drop_na=True):
}
params = interval_lookup.get(averaging_hours, {'rule':str(int(averaging_hours * 60)) + 'min', 'loffset':str(int(averaging_hours * 30)) + 'min'})
new_df = pandas_dataframe.resample(rule=params['rule'], loffset=params['loffset'],label='left').mean()
if not use_rolling_averaging:
new_df = pandas_dataframe.resample(rule=params['rule'], loffset=params['loffset'],label='left').mean()
else:
# resample to consistent interval
original_interval = pandas_dataframe.index.to_series().diff().quantile(.05)
new_df = pandas_dataframe.resample(rule=original_interval).median()
# apply the rolling averaging
new_df = new_df.rolling(int(pd.Timedelta(hours=averaging_hours) / original_interval),center=True,min_periods=1).mean()
# downsample the result if there are more than 1000 values
if len(new_df) > 1000:
new_df = new_df.resample(rule=(pandas_dataframe.index[-1] - pandas_dataframe.index[0]) / 1000).mean()
if drop_na:
new_df = new_df.dropna()
......
......@@ -116,7 +116,7 @@ def decode_boat_lt2(data: bytes) -> Dict[str, Any]:
# ------- Shore Power
shoreV = int16(0) / 1000. # voltage from wall wart in Volts
res['shorePower'] = 1 if shoreV > 4.3 and shoreV < 5.5 else 0
res['shorePower'] = 1 if shoreV > 4.3 and shoreV < 5.75 else 0
# ---- Battery voltage
batV = int16(2) / 1000.
......
# Generated by Django 2.2.4 on 2021-04-02 22:52
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('bmsapp', '0032_auto_20190926_1206'),
]
operations = [
migrations.AddField(
model_name='alertcondition',
name='only_if_bldg_status',
field=models.CharField(blank=True, choices=[('Occupied', 'Occupied'), ('Unoccupied', 'Unoccupied')], max_length=15, null=True, verbose_name='and this status'),
),
]
......@@ -8,6 +8,7 @@ from django.core.mail import send_mail
import requests
import bmsapp.data_util
import bmsapp.formatters
import bmsapp.schedule
from . import sms_gateways
import yaml
......@@ -409,6 +410,21 @@ class Building(models.Model):
return props
def current_status(self, ts = time.time()):
"""Returns the occupied/unoccupied status of a building at a given time
"""
if self.schedule:
if self.timezone:
scheduleObj = bmsapp.schedule.Schedule(self.schedule, self.timezone)
else:
scheduleObj = bmsapp.schedule.Schedule(self.schedule, 'US/Alaska')
if scheduleObj.is_occupied(ts):
return 'Occupied'
else:
return 'Unoccupied'
else:
return 'Occupied'
class Meta:
ordering = ['title']
......@@ -716,6 +732,7 @@ class AlertCondition(models.Model):
# fields to qualify the condition test according to building mode
only_if_bldg = models.ForeignKey(Building, models.SET_NULL, verbose_name='But only if building', blank=True, null=True)
only_if_bldg_mode = models.ForeignKey(BuildingMode, models.SET_NULL, verbose_name='is in this mode', blank=True, null=True)
only_if_bldg_status = models.CharField(verbose_name='and this status', max_length=15, blank=True, null=True, choices=(('Occupied','Occupied'),('Unoccupied','Unoccupied')))
# alert message. If left blank a message will be created from other field values.
alert_message = models.TextField(max_length=400, blank=True,
......@@ -751,12 +768,49 @@ class AlertCondition(models.Model):
return '%s %s %s, %s in %s mode' % \
(self.sensor.title, self.condition, self.test_value, self.only_if_bldg, self.only_if_bldg_mode)
def check_condition(self, reading_db):
def handle(self, reading_db, logger, testing = False):
'''This method handles an alert condition, notifying recipients if needed.
If 'testing' is set to True, a message will always be sent. The number of alerts (0 or 1)
is returned.
'''
alert_count = 0
try:
if testing:
subject_msg = (f"Test Alert: {self.sensor.title} Sensor","This is just a test.")
elif time.time() < (self.last_notified + self.wait_before_next * 3600.0):
# if the wait time has not been satisfied, don't notify
subject_msg = None
else:
subject_msg = self.check_condition(reading_db)
if subject_msg:
alert_count = 1
subject, msg = subject_msg
msg_count = 0 # tracks # of successful messages sent
for recip in self.recipients.all():
try:
msg_count += recip.notify(subject, msg, self.priority)
except:
logger.exception('Error notifying recipient %s of an alert.' % recip)
if msg_count and (not testing):
# at least one message was sent so update the field tracking the timestamp
# of the last notification for this condition.
self.last_notified = time.time()
self.save()
# store a log of the alert
reading_db.log_alert(self.sensor.sensor_id,msg)
except:
logger.exception('Error processing alert %s')
return alert_count
def check_condition(self, reading_db, override_value = None):
'''This method checks to see if the alert condition is in effect, and if so,
returns a (subject, message) tuple describing the alert. If the condition is
not in effect, None is returned. 'reading_db' is a sensor reading database, an
instance ofbmsapp.readingdb.bmsdata.BMSdata. If the alert condition is not active,
None is returned.
instance ofbmsapp.readingdb.bmsdata.BMSdata. If an 'override_value' is
specified, that value will be tested instead of the last values in the reading
database. If the alert condition is not active, None is returned.
'''
if not self.active:
......@@ -771,7 +825,10 @@ class AlertCondition(models.Model):
# get the most current reading for the sensor (last_read), and
# also fill out a list of all the recent readings that need to
# be evaluated to determine if the alert condition is true (last_reads).
if self.read_count==1:
if not override_value is None:
last_read = {'ts': int(time.time()), 'val': float(override_value)}
last_reads = [last_read]
elif self.read_count==1:
last_read = self.sensor.last_read(reading_db) # will be None if no readings
last_reads = [last_read] if last_read else []
else:
......@@ -814,10 +871,13 @@ class AlertCondition(models.Model):
# Loop through the requested number of last readings, testing whether
# the alert conditions are satisfied for all the readings.
# First see if there was a building mode test requested and test it.
if self.only_if_bldg is not None and self.only_if_bldg_mode is not None:
if self.only_if_bldg.current_mode != self.only_if_bldg_mode:
if self.only_if_bldg is not None:
if self.only_if_bldg_mode is not None and (self.only_if_bldg.current_mode != self.only_if_bldg_mode):
# Failed building mode test
return None
if self.only_if_bldg_status is not None and (self.only_if_bldg.current_status(last_reads[0]['ts']) != self.only_if_bldg_status):
# Failed building status test
return None
# Alert condition must be true for each of the requested readings
for read in last_reads:
......
......@@ -56,6 +56,15 @@ class BMSdata:
self.conn.commit()
self.sensor_ids.add('_last_raw')
# Check to see if the table that stores alert log records exists.
# If not, make it. Make the value field a
# real instead of integer in case this table is needed for non counter
# sensors in the future.
if '_alert_log' not in self.sensor_ids:
self.cursor.execute("CREATE TABLE [_alert_log] (id varchar(50), ts integer, message varchar(255))")
self.conn.commit()
self.sensor_ids.add('_alert_log')
# because SQLite has case insensitive table names, make a sensor ID set with lower-case names
self.sensor_ids_lower = {tbl.lower() for tbl in self.sensor_ids}
......@@ -335,6 +344,14 @@ class BMSdata:
id_list = [sens_id for sens_id in self.sensor_ids if sens_id[0]!='_']
return sorted(id_list)
def log_alert(self, sensor_id, message):
"""Stores a log of an alert nofification
"""
ts = int(time.time())
self.cursor.execute("INSERT INTO [_alert_log] (id, ts, message) VALUES (?, ?, ?)", (sensor_id, ts, message))
self.conn.commit()
def backup_db(self, days_to_retain):
"""Backs up the database and compresses the backup. Deletes old backup
files that were created more than 'days_to_retain' ago.
......
......@@ -10,7 +10,7 @@ class TimeSeries(basechart.BaseChart):
"""
# see BaseChart for definition of these constants
CTRLS = 'refresh, ctrl_sensor, ctrl_avg, ctrl_occupied, time_period_group, get_embed_link'
CTRLS = 'refresh, ctrl_sensor, ctrl_avg, ctrl_use_rolling_averaging, ctrl_occupied, time_period_group, get_embed_link'
MULTI_SENSOR = 1
def result(self):
......@@ -28,8 +28,15 @@ class TimeSeries(basechart.BaseChart):
# get the requested averaging interval in hours
if 'averaging_time' in self.request_params:
averaging_hours = float(self.request_params['averaging_time'])
if 'use_rolling_averaging' in self.request_params:
use_rolling_averaging = True
else:
use_rolling_averaging = False
else:
averaging_hours = 0
use_rolling_averaging = False
# determine the start time for selecting records and loop through the selected
# records to get the needed dataset
......@@ -51,7 +58,7 @@ class TimeSeries(basechart.BaseChart):
if not df.empty:
# perform average (if requested)
if averaging_hours:
df = bmsapp.data_util.resample_timeseries(df,averaging_hours)
df = bmsapp.data_util.resample_timeseries(df,averaging_hours,use_rolling_averaging)
# create lists for plotly
if np.absolute(df.val.values).max() < 10000:
......
......@@ -10,7 +10,7 @@ class Schedule:
""" This class represents an occupied/unoccupied schedule for a facility.
"""
def __init__(self, schedule_text, tz_name):
def __init__(self, schedule_text, tz_name = 'US/Alaska'):
""" Constructs a Schedule object.
'schedule_text' is a string that describes the occupied periods
......
......@@ -26,29 +26,6 @@ def run():
total_true_alerts = 0
for condx in AlertCondition.objects.all():
# if the wait time has not been satisfied, don't notify
if time.time() < (condx.last_notified + condx.wait_before_next * 3600.0):
continue
try:
subject_msg = condx.check_condition(reading_db)
if subject_msg:
total_true_alerts += 1
subject, msg = subject_msg
msg_count = 0 # tracks # of successful messages sent
for recip in condx.recipients.all():
try:
msg_count += recip.notify(subject, msg, condx.priority)
except:
logger.exception('Error notifying recipient %s of an alert.' % recip)
if msg_count:
# at least one message was sent so update the field tracking the timestamp
# of the last notification for this condition.
condx.last_notified = time.time()
condx.save()
except:
logger.exception('Error processing alert %s')
total_true_alerts += condx.handle(reading_db, logger)
return total_true_alerts
......@@ -108,7 +108,7 @@ process_chart_change = ->
# start by hiding all input controls
set_visibility(['refresh', 'ctrl_sensor', 'ctrl_avg', 'ctrl_avg_export',
'ctrl_normalize', 'ctrl_occupied', 'xy_controls', 'time_period_group',
'download_many', 'get_embed_link'], false)
'download_many', 'get_embed_link','ctrl_use_rolling_averaging'], false)
# get the chart option control that is selected. Then use the data
# attributes of that option element to configure the user interface.
......@@ -320,7 +320,7 @@ $ ->
$("#select_chart").change process_chart_change
# Set up change handlers for inputs.
ctrls = ['averaging_time', 'averaging_time_export', 'normalize', 'show_occupied',
ctrls = ['averaging_time', 'averaging_time_export', 'normalize', 'use_rolling_averaging', 'show_occupied',
'select_sensor', 'select_sensor_x', 'select_sensor_y', 'averaging_time_xy', 'div_date',
'start_date', 'end_date']
$("##{ctrl}").change inputs_changed for ctrl in ctrls
......
......@@ -101,7 +101,7 @@
process_chart_change = function () {
var multi, selected_chart_option, sensor_val, single, vis_ctrls;
set_visibility(['refresh', 'ctrl_sensor', 'ctrl_avg', 'ctrl_avg_export', 'ctrl_normalize', 'ctrl_occupied', 'xy_controls', 'time_period_group', 'download_many', 'get_embed_link'], false);
set_visibility(['refresh', 'ctrl_sensor', 'ctrl_avg', 'ctrl_avg_export', 'ctrl_normalize', 'ctrl_use_rolling_averaging', 'ctrl_occupied', 'xy_controls', 'time_period_group', 'download_many', 'get_embed_link'], false);
selected_chart_option = $("#select_chart").find("option:selected");
vis_ctrls = selected_chart_option.data("ctrls").split(",");
set_visibility(vis_ctrls, true);
......@@ -299,7 +299,7 @@
$("#select_group").change(update_bldg_list);
$("#select_bldg").change(update_chart_sensor_lists);
$("#select_chart").change(process_chart_change);
ctrls = ['averaging_time', 'averaging_time_export', 'normalize', 'show_occupied', 'select_sensor', 'select_sensor_x', 'select_sensor_y', 'averaging_time_xy', 'div_date', 'start_date', 'end_date'];
ctrls = ['averaging_time', 'averaging_time_export', 'normalize', 'use_rolling_averaging', 'show_occupied', 'select_sensor', 'select_sensor_x', 'select_sensor_y', 'averaging_time_xy', 'div_date', 'start_date', 'end_date'];
for (i = 0, len = ctrls.length; i < len; i++) {
ctrl = ctrls[i];
$("#" + ctrl).change(inputs_changed);
......
{% extends "admin/change_form.html" %}
{% block after_related_objects %}
<fieldset class="module aligned">
<div class="form-row">
<div>
<label for="btn_trigger_alert">Testing:</label>
<button type="button" id="btn_trigger_alert" onclick="testAlert({{ original.id }});">Send A Test Notification</button>
<div class="help">You should 'Save' any changes first.</div>
</div>
</div>
</fieldset>
<script
src="https://code.jquery.com/jquery-3.4.0.min.js"
integrity="sha256-BJeo0qm959uMBGb65z40ejJYGSgR7REI4+CW1fNKwOg="
crossorigin="anonymous"></script>
<script>
var cookies = document.cookie
.split(';')
.map(v => v.split('='))
.reduce((acc, v) => {
acc[decodeURIComponent(v[0].trim())] = decodeURIComponent(v[1].trim());
return acc;
}, {});
function testAlert(recipientID) {
$.post( {
url: "/test-alert-notifications/",
data: {'recipient': recipientID},
headers: {'X-CSRFToken': cookies['csrftoken']}
})
.done(function( data ) {
alert( data )})
.fail(function (jqXHR, textStatus, errorThrown) {
alert( jqXHR.responseText )});
}
</script>
{% endblock %}
\ No newline at end of file
{% load i18n admin_urls static %}
<div class="js-inline-admin-formset inline-group"
id="{{ inline_admin_formset.formset.prefix }}-group"
data-inline-type="stacked"
data-inline-formset="{{ inline_admin_formset.inline_formset_data }}">
<fieldset class="module {{ inline_admin_formset.classes }}">
<h2>{{ inline_admin_formset.opts.verbose_name_plural|capfirst }}</h2>
{{ inline_admin_formset.formset.management_form }}
{{ inline_admin_formset.formset.non_form_errors }}
{% for inline_admin_form in inline_admin_formset %}<div class="inline-related{% if inline_admin_form.original or inline_admin_form.show_url %} has_original{% endif %}{% if forloop.last and inline_admin_formset.has_add_permission %} empty-form last-related{% endif %}" id="{{ inline_admin_formset.formset.prefix }}-{% if not forloop.last %}{{ forloop.counter0 }}{% else %}empty{% endif %}">
<h3><b>{{ inline_admin_formset.opts.verbose_name|capfirst }}:</b>&nbsp;<span class="inline_label">{% if inline_admin_form.original %}{{ inline_admin_form.original }}{% if inline_admin_form.model_admin.show_change_link and inline_admin_form.model_admin.has_registered_model %} <a href="{% url inline_admin_form.model_admin.opts|admin_urlname:'change' inline_admin_form.original.pk|admin_urlquote %}" class="{% if inline_admin_formset.has_change_permission %}inlinechangelink{% else %}inlineviewlink{% endif %}">{% if inline_admin_formset.has_change_permission %}{% trans "Change" %}{% else %}{% trans "View" %}{% endif %}</a>{% endif %}
{% else %}#{{ forloop.counter }}{% endif %}</span>
{% if inline_admin_form.show_url %}<a href="{{ inline_admin_form.absolute_url }}">{% trans "View on site" %}</a>{% endif %}
{% if inline_admin_formset.formset.can_delete and inline_admin_form.original %}<span class="delete">{{ inline_admin_form.deletion_field.field }} {{ inline_admin_form.deletion_field.label_tag }}</span>{% endif %}
</h3>
{% if inline_admin_form.form.non_field_errors %}{{ inline_admin_form.form.non_field_errors }}{% endif %}
{% for fieldset in inline_admin_form %}
{% include "admin/includes/fieldset.html" %}
{% endfor %}
{% if inline_admin_form.needs_explicit_pk_field %}{{ inline_admin_form.pk_field.field }}{% endif %}
{% if inline_admin_form.fk_field %}{{ inline_admin_form.fk_field.field }}{% endif %}
{% comment "Alternate Implementation - Replace the above with this include statement" %}
{% include "admin\edit_inline\stacked.html" %}
{% endcomment %}
<fieldset class="module aligned">
<div class="form-row">
<div>
<label for="btn_test_alert_value">Test a Value:</label>
<input type="number" name="alertcondition_test_value" value="0" step="any" id="alertcondition_test_value">
<button type="button" id="btn_test_alert_value" onclick="testAlertValue(this.form);">Check Value</button>
<div class="help">You should 'Save' any changes first.</div>
</div>
</div>
</fieldset>
</div>{% endfor %}
</fieldset>
</div>
<script
src="https://code.jquery.com/jquery-3.4.0.min.js"
integrity="sha256-BJeo0qm959uMBGb65z40ejJYGSgR7REI4+CW1fNKwOg="
crossorigin="anonymous"></script>
<script>
var cookies = document.cookie
.split(';')
.map(v => v.split('='))
.reduce((acc, v) => {
acc[decodeURIComponent(v[0].trim())] = decodeURIComponent(v[1].trim());
return acc;
}, {});
function testAlertValue(currentForm) {
var formData = new FormData(currentForm);
$.post( {
url: "/test-alert-value/",
data: {'sensor': formData.get('alertcondition_set-0-sensor'),
'test_value': formData.get('alertcondition_test_value')},
headers: {'X-CSRFToken': cookies['csrftoken']}
})
.done(function( data ) {
alert( data )})
.fail(function (jqXHR, textStatus, errorThrown) {
alert( jqXHR.responseText )});
}
</script>
......@@ -32,6 +32,7 @@
<li><a href="{{ bmsapp_nav_link_base_url }}admin/">System Administration: Configure BMON System</a></li>
<li><a href="{{ bmsapp_nav_link_base_url }}unassigned-sensors/">Unassigned Sensors: View and Delete</a></li>
<li><a href="{{ bmsapp_nav_link_base_url }}sensor-data-utilities/">Sensor Data Utilities (Eliminate Bad Data, Merge Sensors)</a></li>
<li><a href="{{ bmsapp_nav_link_base_url }}alert-log/">Alert Logs: View and Export</a></li>
<li><a href="{{ bmsapp_nav_link_base_url }}ecobee-auth/">Authorize Access to an Ecobee Thermostat Account</a></li>
</ul>
......
{% extends "bmsapp/base.html" %}
{% block pagetitle %}Alert Logs{% endblock %}
{% block title %}Alert notifications that have been sent{% endblock %}
{% block content %}
<link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/v/bs4/jszip-2.5.0/dt-1.10.24/b-1.7.0/b-html5-1.7.0/sl-1.3.3/datatables.min.css"/>
<script type="text/javascript" src="https://cdn.datatables.net/v/bs4/jszip-2.5.0/dt-1.10.24/b-1.7.0/b-html5-1.7.0/sl-1.3.3/datatables.min.js"></script>
<script>
var cookies = document.cookie
.split(';')
.map(v => v.split('='))
.reduce((acc, v) => {
acc[decodeURIComponent(v[0].trim())] = decodeURIComponent(v[1].trim());
return acc;
}, {});
$(function () {
$.fn.dataTable.Buttons.defaults.dom.button.className = 'btn';
var alert_table = $('#Alerts').DataTable({
paging: false,
scrollY: '80vh',
scrollCollapse: true,
columnDefs: [ {
orderable: false,
className: 'select-checkbox',
targets: 0
} ],
dom: 'Bfrtip',
buttons: [
{
text: 'Select all',
className: 'btn-secondary',
action: function (e, dt, node, config) {
dt.rows().deselect();
dt.rows( { search: 'applied' } ).select();
}
},
'selectNone',
'excel'
],
select: {
style: 'multi',
selector: 'td:first-child'
},
order: [[ 1, 'asc' ]]
});
});
</script>
<div style="background-color: white; width: fit-content; padding: 5px;">
<table id="Alerts" class="table table-sm table-striped">
<thead><tr>
<th>&nbsp;&nbsp;&nbsp;&nbsp;</th>
<th scope="col" class="text-center">Sensor ID</th>
<th scope="col" class="text-center">Timestamp</th>
<th scope="col" class="text-center">Message</th>
</tr></thead>
<tbody style="background-color: white">
{% for alert in alert_list %}
<tr id="{{ alert.id }}">
<td></td>
<td class="bmon-sensor-id">{{ alert.id }}</td>
<td>{{ alert.when }}</td>
<td>{{ alert.message }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}
......@@ -91,6 +91,13 @@
Shade Occupied Periods
</label>
</div>
<div id="ctrl_use_rolling_averaging" class="form-check mb-4 ml-3">
<input class="form-check-input" type="checkbox" value="" id="use_rolling_averaging"
name="use_rolling_averaging">
<label class="form-check-label" for="use_rolling_averaging">
Use Rolling Averaging
</label>
</div>
<div id="ctrl_normalize" class="form-check mb-4 ml-3">
<input class="form-check-input" type="checkbox" value="" id="normalize"
name="normalize">
......
......@@ -40,6 +40,7 @@ urlpatterns = [
views.show_video, name='show-video'),
re_path(r'^make-store-key/$', views.make_store_key),
re_path(r'^ecobee-auth/$', views.ecobee_auth),
re_path(r'^unassigned-sensors/$', views.unassigned_sensors),
re_path(r'^unassigned-sensors/delete_ids/$',
views.delete_unassigned_sensor_ids),
......@@ -48,6 +49,10 @@ urlpatterns = [
re_path(r'^merge-sensors/$', views.merge_sensors),
re_path(r'^delete-sensor-values/$', views.delete_sensor_values),
re_path(r'^test-alert-notifications/$', views.test_alert_notifications),
re_path(r'^test-alert-value/$', views.test_alert_value),
re_path(r'^alert-log/$', views.alert_log),
# Views related to the API, version 1
re_path(r'^api/v1/version/$', views_api_v1.api_version),
re_path(r'^api/v1/readings/(.+)/$', views_api_v1.sensor_readings),
......
......@@ -782,6 +782,20 @@ def delete_unassigned_sensor_ids(request):
return HttpResponse(f'Deleted {len(row_ids)} unassigned sensors')
@login_required(login_url='../admin/login/')
def alert_log(request):
"""Shows a log of alerts that have been issued.
"""
db = bmsdata.BMSdata()
db.cursor.execute('SELECT * FROM [_alert_log]')
alert_list = [{**x,'when':time.strftime('%Y-%m-%d %M:%S',time.localtime(x['ts']))} for x in [dict(r) for r in db.cursor.fetchall()]]
# time.strftime('%Y-%m-%dT%M:%S',time.localtime(alert_list[0]['ts']))
ctx = base_context()
ctx.update({'alert_list': alert_list})
return render_to_response('bmsapp/alert-log.html', ctx)
@login_required(login_url='../admin/login/')
def backup_reading_db(request):
......@@ -790,6 +804,32 @@ def backup_reading_db(request):
bmsapp.scripts.backup_readingdb.run()
return HttpResponse('Sensor Reading Backup Complete!')
def test_alert_notifications(request):
"""Send test notifications
"""
try:
recipient = request.POST.get('recipient')
recip = models.AlertRecipient.objects.filter(id=recipient)[0]
result = recip.notify(subject='BMON Test Alert', message='This is a test of a BMON notification that was sent manually by an administrator.', pushover_priority='0')
if result:
return HttpResponse(f'The test notifications have been sent.')
else:
return HttpResponse(f'Failed to send any notifications!',status=500)
except Exception as e:
return HttpResponse(e,status=500)
def test_alert_value(request):
"""Test a value to see if it triggers an alert
"""
sensor_id = request.POST.get('sensor')
test_value = request.POST.get('test_value')
alert = models.AlertCondition.objects.filter(sensor=sensor_id)[0]
result = alert.check_condition(reading_db=None, override_value=test_value)
if result:
subject, msg = result
return HttpResponse(f'The value triggered an Alert:\n{subject}\n{msg}')
else:
return HttpResponse(f'The value did not trigger an Alert')
def wildcard(request, template_name):
'''
......
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