Commit 68b079c6 authored by Alan Mitchell's avatar Alan Mitchell

Initial Commit

parents
settings.py
*.sqlite
*.pyc
*.log
*.pdf
*.swf
*.gz
import logging
import app_settings
logging.getLogger('bms').info('BMS Application first accessed.')
'''
This file configures the Admin interface, which allows for editing of the Models.
'''
from bmsapp.models import Building, Sensor, SensorGroup, BldgToSensor, Unit, BuildingChartType, BuildingChart
from bmsapp.models import MultiBuildingChartType, MultiBuildingChart, ChartBuildingInfo
from django.contrib import admin
from django.forms import TextInput, Textarea
from django.db import models
class BldgToSensorInline(admin.TabularInline):
model = BldgToSensor
extra = 1
class BuildingChartInline(admin.TabularInline):
model = BuildingChart
formfield_overrides = {
models.TextField: {'widget': Textarea(attrs={'rows':1, 'cols':60})},
}
extra = 1
class BuildingAdmin(admin.ModelAdmin):
inlines = (BuildingChartInline, BldgToSensorInline)
class SensorAdmin(admin.ModelAdmin):
inlines = (BldgToSensorInline,)
search_fields = ['title', 'tran_calc_function']
list_filter = ['is_calculated', 'tran_calc_function']
class SensorGroupAdmin(admin.ModelAdmin):
list_display = ('id', 'title', 'sort_order')
list_editable = ('title', 'sort_order')
class UnitAdmin(admin.ModelAdmin):
list_display = ('id', 'label', 'measure_type')
list_editable = ('label', 'measure_type')
class BuildingChartTypeAdmin(admin.ModelAdmin):
list_display = ('id', 'title', 'class_name', 'sort_order')
list_editable = ('title', 'class_name', 'sort_order')
class MultiBuildingChartTypeAdmin(admin.ModelAdmin):
list_display = ('id', 'title', 'class_name', 'sort_order')
list_editable = ('title', 'class_name', 'sort_order')
class ChartBuildingInfoInline(admin.TabularInline):
model = ChartBuildingInfo
formfield_overrides = {
models.TextField: {'widget': Textarea(attrs={'rows':1, 'cols':60})},
}
extra = 1
class MultiBuildingChartAdmin(admin.ModelAdmin):
inlines = (ChartBuildingInfoInline,)
formfield_overrides = {
models.TextField: {'widget': Textarea(attrs={'rows':1, 'cols':60})},
}
admin.site.register(Building, BuildingAdmin)
admin.site.register(Sensor, SensorAdmin)
admin.site.register(SensorGroup, SensorGroupAdmin)
admin.site.register(Unit, UnitAdmin)
admin.site.register(BuildingChartType, BuildingChartTypeAdmin)
admin.site.register(MultiBuildingChartType, MultiBuildingChartTypeAdmin)
admin.site.register(MultiBuildingChart, MultiBuildingChartAdmin)
from bmsapp.models import Building, Sensor, SensorGroup, BldgToSensor, Unit
from django.contrib import admin
class BldgToSensorInline(admin.TabularInline):
model = BldgToSensor
extra = 1
class BuildingAdmin(admin.ModelAdmin):
inlines = (BldgToSensorInline,)
class SensorAdmin(admin.ModelAdmin):
inlines = (BldgToSensorInline,)
search_fields = ['title', 'tran_calc_function']
list_filter = ['is_calculated', 'tran_calc_function']
class SensorGroupAdmin(admin.ModelAdmin):
list_display = ('id', 'title', 'sort_order')
list_editable = ('title', 'sort_order')
class UnitAdmin(admin.ModelAdmin):
list_display = ('id', 'label', 'measure_type')
list_editable = ('label', 'measure_type')
admin.site.register(Building, BuildingAdmin)
admin.site.register(Sensor, SensorAdmin)
admin.site.register(SensorGroup, SensorGroupAdmin)
admin.site.register(Unit, UnitAdmin)
'''
This file holds settings for the application
'''
from os.path import dirname, join, realpath
from glob import glob
APP_PATH = realpath(dirname(__file__))
# Full path to the database holding sensor readings
DATA_DB_FILENAME = join(APP_PATH, 'data', 'bms_data.sqlite')
# Full path to the Django database holding project model data
# (building lists, sensor lists, etc.). Assume it is the first sqlite database
# in the directory above
dbs = glob(join(APP_PATH, '..', '*.sqlite'))
PROJ_DB_FILENAME = realpath(dbs[0]) if dbs else ''
# ------- Set up logging for the application
import logging, logging.handlers
# Log file for the application
LOG_FILE = join(APP_PATH, 'logs', 'bms.log')
# *** This controls what messages will actually get logged
# Levels in order from least to greatest severity are: DEBUG, INFO, WARNING, ERROR, CRITICAL
# Methods to call that automatically set appropriate level are: debug(), info(), warning(), error(),
# exception(), and critical(). If 'exception()' is called, an exception traceback is automatically
# added to the log message (only call from within an exception handler).
LOG_LEVEL = logging.INFO
# ----
# create base logger for the application. Any other loggers named 'bms.XXXX'
# will inherit these settings.
logger = logging.getLogger('bms')
# set the log level
logger.setLevel(LOG_LEVEL)
# create a rotating file handler
fh = logging.handlers.RotatingFileHandler(LOG_FILE, maxBytes=200000, backupCount=5)
# create formatter and add it to the handler
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
fh.setFormatter(formatter)
# add the handler to the logger
logger.addHandler(fh)
# --------------------------
\ No newline at end of file
'''
Class to encapsulate a BMS database. Uses the SQLite database
'''
import os, sqlite3, time
import pandas as pd
class BMSdata:
def __init__(self, fname):
'''
fname: full path to SQLite database file. If the file is not present, it will be created with
the proper structure.
'''
# Open db file if it exists, or make the database if it doesn't exist. Store
# the connection object as an object variable.
if os.path.exists(fname):
self.conn = sqlite3.connect(fname)
else:
# no file, so make the necessary table and indexes.
self.conn = sqlite3.connect(fname)
self.conn.execute("CREATE TABLE reading(ts integer not null, id varchar(15) not null, val real, primary key (ts, id))")
self.conn.execute("CREATE INDEX id_ix on reading(id)")
self.conn.execute("CREATE INDEX ts_ix on reading(ts)")
self.conn.commit()
# use the SQLite Row row_factory for all Select queries
self.conn.row_factory = sqlite3.Row
# now create a cursor object
self.cursor = self.conn.cursor()
def close(self):
self.conn.close()
def insert_reading(self, ts, id, val):
'''
Inserts a record into the database and commits it.
'''
self.cursor.execute("INSERT INTO reading (ts, id, val) VALUES (?, ?, ?)", (ts, id, val))
self.conn.commit()
def last_read(self, id):
'''
Returns the last reading for a particular sensor. The reading is returned as a row dictionary.
'''
self.cursor.execute('SELECT * FROM reading WHERE id=? ORDER BY ts DESC LIMIT 1', (id,))
row = self.cursor.fetchone()
return dict(row) if row else {}
def rowsWhere(self, whereClause):
'''
Returns a list of row dictionaries matching the 'whereClause'. The where clause
can include ORDER BY and other clauses that can appear after WHERE.
'''
self.cursor.execute('SELECT * FROM reading WHERE ' + whereClause)
return [dict(r) for r in self.cursor.fetchall()]
def rowsForOneID(self, sensor_id, start_tm=None, end_tm=None):
'''
Returns a list of dictionaries, each dictionary having a 'ts' and 'val' key. The
rows are for a particular sensor ID, and can be further limited by a time range.
'start_tm' and 'end_tm' are UNIX timestamps. If either are not provided, no limit
is imposed. The rows are returned in timestamp order.
'''
sql = 'SELECT ts, val FROM reading WHERE id="%s"' % sensor_id
if start_tm is not None:
sql += ' AND ts>=%s' % int(start_tm)
if end_tm is not None:
sql += ' AND ts<=%s' % int(end_tm)
sql += ' ORDER BY ts'
self.cursor.execute(sql)
return [dict(r) for r in self.cursor.fetchall()]
def readingCount(self, startTime=0):
"""
Returns the number of readings in the reading table inserted after the specified
'startTime' (Unix seconds).
"""
self.cursor.execute('SELECT COUNT(*) FROM reading WHERE ts > ?', (startTime,))
return self.cursor.fetchone()[0]
\ No newline at end of file
This diff is collapsed.
This diff is collapsed.
'''
This module holds classes that create the HTML and supply the data for Charts and
Reports.
'''
import models, bmsdata, app_settings, transforms, data_util
from django.template import Context, loader
import time, pandas as pd, numpy as np, logging, xlwt
# Make a logger for this module
_logger = logging.getLogger('bms.' + __name__)
class BldgChart(object):
'''
Base class for all of the chart classes.
'''
def __init__(self, chart, get_params):
'''
'chart' is the models.BuildingChart object for the chart. 'get_params' are the parameters
passed in by the user through the Get http request.
'''
self.chart = chart
self.get_params = get_params
# the name of the class used to build and process this chart.
self.class_name = chart.chart_type.class_name
# for the chart object, take the keyword parameter string and convert it to a dictionary.
chart_params = transforms.makeKeywordArgs(chart.parameters)
# for any parameter name that starts with "id_", strip the "id_" from the name and substitute
# the models.Sensor object that corresponds to the id value.
for nm, val in chart_params.items():
if nm.startswith('id_'):
del chart_params[nm] # delete this item from the dictionary
# make a new item in the dictionary to hold the sensor identified by this id
chart_params[ nm[3:] ] = models.Sensor.objects.get(sensor_id=val)
self.chart_params = chart_params # store results in object
# Make a context variable for use by templates including useful template
# data.
self.context = Context( {} )
def html(self):
'''
Returns the HTML necessary to configure and display the chart.
'''
template = loader.get_template('bmsapp/%s.html' % self.class_name)
return template.render(self.context)
def data(self):
'''
This method should be overridden. Returns the series data and any other
data that is affected by user configuration of the chart.
'''
return [1,2,3]
def make_sensor_select_html(self, multi_select=False):
'''
Helper method that returns the HTML for a Select control that allows
selection of a sensor associated with this building. If 'multi_select'
is True, multi-select Select HTML is returned.
'''
grp = '' # tracks the sensor group
html = '<select id="select_sensor" name="select_sensor" '
html += 'multiple="multiple">' if multi_select else '>'
first_sensor = True
for b_to_sen in self.chart.building.bldgtosensor_set.all():
if b_to_sen.sensor_group != grp:
if first_sensor == False:
# Unless this is the first group, close the prior group
html += '</optgroup>'
html += '<optgroup label="%s">' % b_to_sen.sensor_group.title
grp = b_to_sen.sensor_group
html += '<option value="%s" %s>%s</option>' % \
(b_to_sen.sensor.sensor_id, 'selected' if first_sensor else '', b_to_sen.sensor.title)
first_sensor = False
html += '</optgroup></select>'
return html
def get_ts_range(self):
'''
Returns the start and stop timestamp as determined by the GET parameters that were posted
from the "time_period" Select control.
'''
tm_per = self.get_params['time_period']
if tm_per != "custom":
st_ts = int(time.time()) - int(tm_per) * 24 * 3600
end_ts = time.time() + 3600.0 # adding an hour to be sure all records are caught
else:
st_date = self.get_params['start_date']
st_ts = data_util.datestr_to_ts(st_date) if len(st_date) else 0
end_date = self.get_params['end_date']
end_ts = data_util.datestr_to_ts(end_date + " 23:59:59") if len(end_date) else time.time() + 3600.0
return st_ts, end_ts
class TimeSeries(BldgChart):
def html(self):
if 'sensor' in self.chart_params:
self.context['select_sensor'] = ''
else:
# provide sensor selection
self.context['select_sensor'] = self.make_sensor_select_html(True)
return super(TimeSeries, self).html()
def data(self):
'''
Returns the data for a Time Series chart. Return value is a dictionary
containing the dynamic data used to draw the chart.
'''
# open the database
db = bmsdata.BMSdata(app_settings.DATA_DB_FILENAME)
# determine the sensors to plot either from the chart configuration or from the
# sensor selected by the user. This creates a list of Sensor objects to plot
if 'sensor' in self.chart_params:
sensor_list = [ self.chart_params['sensor'] ] # the sensor to chart
else:
sensor_list = [ models.Sensor.objects.get(sensor_id=id) for id in self.get_params.getlist('select_sensor') ]
# determine the Y axes that will be needed to cover the the list of sensor, based on the labels
# of the units
y_axes_ids = list(set([sensor.unit.label for sensor in sensor_list]))
# get the requested averaging interval in hours
averaging_hours = float(self.get_params['averaging_time'])
# determine the start time for selecting records and loop through the selected
# records to get the needed dataset
st_ts, end_ts = self.get_ts_range()
# Create the series to plot
series = []
# determine suitable line width
line_width = 1 if len(sensor_list) > 1 else 2
for sensor in sensor_list:
db_recs = db.rowsForOneID(sensor.sensor_id, st_ts, end_ts)
# put timestamps and values into arrays
times = []
values = []
for rec in db_recs:
times.append(rec['ts'])
values.append(rec['val'])
# convert timestamps to a numpy array to be consistent with Pandas index below and
# to allow easy multiplication
times = np.array(times)
if averaging_hours:
# averaging is requested, so do it using a Pandas Series
ser = pd.Series(values, index=times).groupby(data_util.TsBin(averaging_hours).bin).mean()
values = ser.values
times = ser.index
# Highcharts uses milliseconds for timestamps, and convert to float because weirdly, integers have
# problems with JSON serialization.
times = times * 1000.0
# Create series data, each item being an [ts, val] pair.
# The 'yAxis' property indicates the id of the Y axis where the data should be plotted.
# Our convention is to use the unit label for the axis as the id.
series_data = [ [ts, data_util.round4(val)] for ts, val in zip(times, values) ]
series_opt = {'data': series_data,
'name': sensor.title,
'yAxis': sensor.unit.label,
'lineWidth': line_width}
# if the sensor has defined states, make the series a Step type series.
if sensor.unit.measure_type == 'state':
series_opt['step'] = 'left'
series.append( series_opt )
return {"series": series, "y_axes": y_axes_ids}
class HourlyProfile(BldgChart):
def html(self):
if 'sensor' in self.chart_params:
self.context['select_sensor'] = ''
else:
# provide sensor selection
self.context['select_sensor'] = self.make_sensor_select_html(False)
return super(HourlyProfile, self).html()
def data(self):
'''
Returns the data for an Hourly Profile chart. Return value is a dictionary
containing the dynamic data used to draw the chart.
'''
# open the database
db = bmsdata.BMSdata(app_settings.DATA_DB_FILENAME)
# determine the sensor to plot either from the chart configuration or from the
# sensor selected by the user.
if 'sensor' in self.chart_params:
the_sensor = self.chart_params['sensor'] # the sensor to chart
else:
the_sensor = models.Sensor.objects.get(sensor_id=self.get_params['select_sensor'])
# determine the start time for selecting records and loop through the selected
# records to get the needed dataset
st_ts, end_ts = self.get_ts_range()
db_recs = db.rowsForOneID(the_sensor.sensor_id, st_ts, end_ts)
recs = []
for rec in db_recs:
dt = data_util.ts_to_datetime(rec['ts'])
recs.append( {'da': dt.weekday(), 'hr': dt.hour, 'val': rec['val']} )
series = []
if len(recs):
# make a pandas DataFrame that has average values for each weekday / hour
# combination. Remove the multi-index so that it easier to select certain days
df = pd.DataFrame(recs).groupby(('da', 'hr')).mean().reset_index()
# Here are the groups of days we want to chart as separate series
da_groups = [ ('All Days', (0,1,2,3,4,5,6)),
('Mon', (0,)),
('Tue-Fri', (1,2,3,4)),
('Mon-Fri', (0,1,2,3,4)),
('Sat', (5,)),
('Sun', (6,)),
('Sat-Sun', (5,6)),
]
# Make a list of the series. create the series in a form directly useable by
# Highcharts.
for nm, da_tuple in da_groups:
a_series = {'name': nm}
df_gp = df[df.da.isin(da_tuple)].drop('da', axis=1).groupby('hr').mean()
a_series['data'] = [data_util.round4(df_gp.ix[hr, 'val']) if hr in df_gp.index else None for hr in range(24)]
series.append(a_series)
# if normalization was requested, scale values 0 - 100%, with 100% being the largest
# value across all the day groups.
if 'normalize' in self.get_params:
yTitle = "%"
# find maximum of each series
maxes = [max(ser['data']) for ser in series]
scaler = 100.0 / max(maxes) if max(maxes) else 1.0
# adjust the values
for ser in series:
for i in range(24):
if ser['data'][i]: # don't scale None values
ser['data'][i] = data_util.round4(ser['data'][i] * scaler)
else:
yTitle = the_sensor.unit.label
return {"series": series, 'y_label': yTitle}
class Histogram(BldgChart):
def html(self):
if 'sensor' in self.chart_params:
self.context['select_sensor'] = ''
else:
# provide sensor selection
self.context['select_sensor'] = self.make_sensor_select_html(False)
return super(Histogram, self).html()
def data(self):
'''
Returns the data for an Histogram chart. Return value is a dictionary
containing the dynamic data used to draw the chart.
'''
# open the database
db = bmsdata.BMSdata(app_settings.DATA_DB_FILENAME)
# determine the sensor to plot either from the chart configuration or from the
# sensor selected by the user.
if 'sensor' in self.chart_params:
the_sensor = self.chart_params['sensor'] # the sensor to chart
else:
the_sensor = models.Sensor.objects.get(sensor_id=self.get_params['select_sensor'])
# determine the start time for selecting records and loop through the selected
# records to get the needed dataset
st_ts, end_ts = self.get_ts_range()
db_recs = db.rowsForOneID(the_sensor.sensor_id, st_ts, end_ts)
# extract out the values and the timestamps into numpy arrays.
values = np.array([rec['val'] for rec in db_recs])
times = np.array([rec['ts'] for rec in db_recs])
if len(values):
# create a Pandas Time Series to allow easy time averaging
pd_ser = pd.Series(values, index=times)
series = [] # will hold all the series created
# info needed to create each series (time averaging function, series name)
series_info = ( (None, 'Raw'),
(data_util.TsBin(1).bin, '1 Hr Averages'),
(data_util.TsBin(2).bin, '2 Hr Avg'),
(data_util.TsBin(4).bin, '4 Hr Avg'),
(data_util.TsBin(8).bin, '8 Hr Avg'),
(data_util.TsBin(24).bin, '1 Day Avg') )
for avg_func, series_name in series_info:
if avg_func:
avg_series = pd_ser.groupby(avg_func).mean()
else:
avg_series = pd_ser
series.append( {'data': data_util.histogram_from_series(avg_series), 'name': series_name} )
else:
series = []
return {"series": series, "x_label": the_sensor.unit.label}
def formatCurVal(val):
'''
Helper function for formatting current values to 3 significant digits, but
avoiding the use of scientific notation for display
'''
if val >= 1000.0:
return '{:,}'.format( int(float('%.3g' % val)))
else:
return '%.3g' % val
class CurrentValues(BldgChart):
def html(self):
# open the database
db = bmsdata.BMSdata(app_settings.DATA_DB_FILENAME)
# make a list with the major items being a sensor group and the
# minor items being a list of sensor info:
# (sensor name, most recent value, units, how many minutes ago value occurred)
cur_group = ''
cur_group_sensor_list = []
sensor_list = []
cur_time = time.time() # needed for calculating how long ago reading occurred
for b_to_sen in self.chart.building.bldgtosensor_set.all():
if b_to_sen.sensor_group.title != cur_group:
if cur_group:
sensor_list.append( (cur_group, cur_group_sensor_list) )
cur_group = b_to_sen.sensor_group.title
cur_group_sensor_list = []
last_read = db.last_read(b_to_sen.sensor.sensor_id)
cur_value = formatCurVal(last_read['val']) if 'val' in last_read else ''
minutes_ago = '%.1f' % ((cur_time - last_read['ts'])/60.0) if 'ts' in last_read else ''
cur_group_sensor_list.append( {'title': b_to_sen.sensor.title,
'cur_value': cur_value,
'unit': b_to_sen.sensor.unit.label,
'minutes_ago': minutes_ago} )
# add the last group
if cur_group:
sensor_list.append( (cur_group, cur_group_sensor_list) )
# make this sensor list available to the template
self.context['sensor_list'] = sensor_list
# create a report title
self.context['report_title'] = 'Current Values: %s' % self.chart.building.title
return super(CurrentValues, self).html()
class ExportData(BldgChart):
def html(self):
# provide sensor selection multi-select box
self.context['select_sensor'] = self.make_sensor_select_html(True)
return super(ExportData, self).html()
def download_many(self, resp_object):
'''
Extracts the requested sensor data, averages it, and creates an Excel spreadsheet
which is then written to the returned HttpResponse object 'resp_object'.
'''
# determine a name for the spreadsheet and fill out the response object
# headers.
xls_name = 'sensors_%s.xls' % data_util.ts_to_datetime().strftime('%Y-%m-%d_%H%M%S')
resp_object['Content-Type']= 'application/vnd.ms-excel'
resp_object['Content-Disposition'] = 'attachment; filename=%s' % xls_name
resp_object['Content-Description'] = 'Sensor Data - readable in Excel'
# start the Excel workbook and format the first row and first column
wb = xlwt.Workbook()
ws = wb.add_sheet('Sensor Data')
# column title style
t1_style = xlwt.easyxf('font: bold on; borders: bottom thin; align: wrap on, vert bottom, horiz center')
# date formatting style
dt_style = xlwt.easyxf(num_format_str='M/D/yy h:mm AM/PM;@')
ws.write(0, 0, "Timestamp", t1_style)
ws.col(0).width = 4300
# make a timestamp binning object
binner = data_util.TsBin(float(self.get_params['averaging_time']))
# open the database
db = bmsdata.BMSdata(app_settings.DATA_DB_FILENAME)
# walk through sensors, setting column titles and building a Pandas DataFrame
# that aligns the averaged timestamps of the different sensors.
col = 1 # tracks spreadsheet column
df = pd.DataFrame()
for id in self.get_params.getlist('select_sensor'):
sensor = models.Sensor.objects.get(sensor_id=id)
# write column heading in spreadsheet
ws.write(0, col, '%s, %s' % (sensor.title, sensor.unit.label), t1_style)
ws.col(col).width = 3600
# determine the start time for selecting records and make a DataFrame from
# the records
st_ts, end_ts = self.get_ts_range()
db_recs = db.rowsForOneID(sensor.sensor_id, st_ts, end_ts)
df_new = pd.DataFrame(db_recs).set_index('ts')
df_new.columns = ['col%s' % col]
df_new = df_new.groupby(binner.bin).mean() # do requested averaging
# join this with the existing DataFrame, taking the union of all timestamps
df = df.join(df_new, how='outer')
col += 1
# put the data in the spreadsheet
row = 1
for ix, ser in df.iterrows():
ws.write(row, 0, data_util.ts_to_datetime(ix), dt_style)
col = 1
for v in ser.values:
if not np.isnan(v):
ws.write(row, col, float('%.4g' % v))
col += 1
row += 1
# flush the row data every 1000 rows to save memory.
if (row % 1000) == 0:
ws.flush_row_data()
# Write the spreadsheet to the HttpResponse object
wb.save(resp_object)
return resp_object
# ********************** Multi-Building Charts Below Here ***************************
class NormalizedFuel(BldgChart):
def data(self):
'''
Returns the data for a Normalized Fuel Use chart. Return value is a dictionary
containing the Highcharts series used to draw the chart.
'''
# open the database
db = bmsdata.BMSdata(app_settings.DATA_DB_FILENAME)
# make a timestamp binning object to bin the data into 1 hour intervals
binner = data_util.TsBin(1.0)
#!/bin/bash
# Script to backup the data in the 'reading' table of the BMS database.
# Backup files are gzipped comma-delimited and placed in the bak directory.
cd ~/webapps/django/rm/bmsapp/data
fname=bak/`date +%Y-%m-%d-%H%M%S`.csv
/usr/bin/sqlite3 bms_data.sqlite <<!
.mode csv
.output $fname
select * from reading;
!
gzip $fname
'''
Utilities used in the data analysis used to produce data for charts and reports.
'''
from datetime import datetime
import pytz, calendar, time
from dateutil import parser
import numpy as np
# Default timezone used when a datetime value needs to be created
default_tz = pytz.timezone('US/Alaska')
def ts_to_datetime(unix_ts=time.time(), tz=default_tz):
'''
Converts a UNIX timestamp (seconds) to a Python datetime object in a
particular timezone. The timezone info is stripped from the returned
datetime to make it naive, which works better with the Pandas library
'''
return datetime.fromtimestamp(unix_ts, tz).replace(tzinfo=None)
def datestr_to_ts(datestr, tz=default_tz):
'''
Converts a date/time string into a Unix timestamp, assuming the date/time is expressed
in the timezone 'tz'.
'''
dt = parser.parse(datestr)
dt_aware = tz.localize(dt)
return calendar.timegm(dt_aware.utctimetuple())
def round4(val):
'''
Rounds a number to a 4 significant digits.
'''
return float('%.4g' % val)
class TsBin:
'''
Class to determine a timestamp bin value (UNIX seconds) for purposes of time-averaging
data. Bins are aligned to the start of a Monday, standard time, in the requested timezone.
No accounting of daylight savings time occurs for establishment of the bins.
'''
def __init__(self, bin_width, tz=default_tz):
'''
'bin_width' is the bin width in hours.
'''
self.bin_wid_secs = bin_width * 3600.0 # bin width in seconds
# determine a reference timestamp that occurs at the start of a bin boundary for all
# binning widths. That would be a Monday at 0:00 am.
ref_dt = tz.localize(datetime(2013, 1, 7))
self.ref_ts = time.mktime(ref_dt.timetuple())
def bin(self, ts):
'''
Returns the bin midpoint for 'ts' in Unix seconds.
'''
bin_int = int((ts - self.ref_ts) / self.bin_wid_secs)
return bin_int * self.bin_wid_secs + self.bin_wid_secs * 0.5 + self.ref_ts
def histogram_from_series(pandas_series):
'''
Returns a list of histogram bins ( [bin center point, count] ) for the Pandas
Time Series 'pandas_series'. The values of the series (index not involved) are used
to create the histogram. The histogram has 30 bins.
'''
cts, bins = np.histogram(pandas_series.values, 20) # 20 bin histogram
avg_bins = (bins[:-1] + bins[1:]) / 2.0 # calculate midpoint of bins
# round these values for better display in Highcharts
avg_bins = [round4(x) for x in avg_bins]
# Convert count bins into % of total reading count
reading_ct = float(sum(cts))
cts = cts.astype('float64') / reading_ct * 100.0
cts = [round4(x) for x in cts]
# weirdly, some integer are "not JSON serializable". Had to
# convert counts to float to avoid the error. Also, round bin average
# to 4 significant figures
return zip(avg_bins, cts)
from django.db import models
# Models for the BMS App
class SensorGroup(models.Model):
'''
A functional group that the sensor belongs to, e.g. "Weather, Snowmelt System".
'''
# the diplay name of the Sensor Group.
title = models.CharField(max_length=40, unique=True)
# A number that determines the order that Sensor Groups will be presented to the user.
sort_order = models.IntegerField(default=999)
def __unicode__(self):
return self.title
class Meta:
ordering = ['sort_order']
class Unit(models.Model):
'''
A physical unit, e.g. "deg F", "gpm", for a sensor value.
'''
# the display name of the unit
label = models.CharField("Unit Label", max_length=20, unique=True)
# the type of physical quantity being measured, e.g. temperature, fluid flow, air flow, power
measure_type = models.CharField("Measurement Type", max_length=30)
def __unicode__(self):
return '%s: %s' % (self.measure_type, self.label)
class Meta:
ordering = ['measure_type', 'label']
class Sensor(models.Model):
'''
One sensor or a calculated field
'''
# sensor ID, either from Monnit network or a user-entered ID for a calculated value
sensor_id = models.CharField("Monnit Sensor ID, or Calculated Field ID", max_length=15, unique=True)
# descriptive title for the sensor, shown to users
title = models.CharField(max_length = 50)
# the units for the sensor values
unit = models.ForeignKey(Unit)
# if True, this field is a calculated field and is not directly created from a sensor.
is_calculated = models.BooleanField("Calculated Field")
# the name of the transform function to scale the value, if any, for a standard sensor field, or the
# calculation function (required) for a calculated field. transform functions must be located in the
# "transforms.py" module and calculated field functions must be located in "calculate_fields.py".
tran_calc_function = models.CharField("Transform or Calculated Field Function Name", max_length=35, blank=True)
# the function parameters, if any, for the transform or calculation function above. parameters are
# entered as one comma-separated string in keyword style, such as 'id_flow="124356", heat_capacity=40.2'
function_parameters = models.TextField("Function Parameters in Keyword form", blank=True)
# Calculation order. If this particular calculated field depends on the completion of other calculated fields
# first, make sure the calculation_order for this field is higher than the fields it depends on.
calculation_order = models.IntegerField(default=0)
def __unicode__(self):
return self.sensor_id + ": " + self.title
class Meta:
ordering = ['sensor_id']
class BuildingChartType(models.Model):
'''
A type of chart applicable to one building (not a group of buildings)
'''
# descriptive title of the Chart Type
title = models.CharField(max_length=50, unique=True)
# the name of the Javascript class used to render the chart. Also the
# name of the Django template name used to create the HTML for the chart.
class_name = models.CharField(max_length=30, unique=True)
# determines order of Chart Type displayed in Admin interface
sort_order = models.IntegerField(default=999)
def __unicode__(self):
return self.title
class Meta:
ordering = ['sort_order']
class MultiBuildingChartType(models.Model):
'''
A type of chart that uses data from multiple buildings
'''
# descriptive title of the Chart Type
title = models.CharField(max_length=50, unique=True)
# the name of the Javascript class used to render the chart. Also the
# name of the Django template name used to create the HTML for the chart.
class_name = models.CharField(max_length=30, unique=True)
# determines order of Chart Type displayed in Admin interface
sort_order = models.IntegerField(default=999)
def __unicode__(self):
return self.title
class Meta:
ordering = ['sort_order']
class Building(models.Model):
'''
A building that contains sensors.
'''
# name of the building displayed to users
title = models.CharField(max_length=50, unique=True)
# the sensors and calculated values associated with this building
sensors = models.ManyToManyField(Sensor, through='BldgToSensor', blank=True, null=True)
def __unicode__(self):
return self.title
class Meta:
ordering = ['title']
class BldgToSensor(models.Model):
'''
Links buildings to sensors and supplies additional information about what Sensor Group the sensor is
in and what order the sensor should be listed within the group. The Bldg-to-Sensor link is set up as
Many-to-Many beacause weather station sensors or calculated values can be used by multiple different
buildings.
'''
# The building and sensor that are linked
building = models.ForeignKey(Building)
sensor = models.ForeignKey(Sensor)
# For this building, the sensor group that the sensor should be classified in.
sensor_group = models.ForeignKey(SensorGroup)
# Within the sensor group, this field determines the sort order of this sensor.
sort_order = models.IntegerField(default=999)
def __unicode__(self):
return self.building.title + ": " + self.sensor.title
class Meta:
ordering = ('building__title', 'sensor_group__sort_order', 'sort_order')
class BuildingChart(models.Model):
'''
One particular chart for a building.
'''
# the building that is associated with this chart
building = models.ForeignKey(Building)
# descriptive title of the Chart
title = models.CharField(max_length=60)
# the type of chart
chart_type = models.ForeignKey(BuildingChartType)
# the parameters for this building needed to draw the chart, if any, parameters are
# entered as one comma-separated string in keyword style,
# such as 'id_flow="124356", heat_capacity=40.2'
parameters = models.TextField("Chart Parameters in Keyword Form", blank=True)
# determines order of Chart displayed in Admin interface
sort_order = models.IntegerField(default=999)
def __unicode__(self):
return self.title
class Meta:
ordering = ['sort_order']
class MultiBuildingChart(models.Model):
'''
One particular chart that utilizes data from a group of buildings
'''
# descriptive title of the Chart
title = models.CharField(max_length=60, unique=True)
# the type of chart
chart_type = models.ForeignKey(MultiBuildingChartType)
# the general parameters for this chart, if any. These are parameters that are
# *not* associated with a particular building. The parameters are
# entered as one comma-separated string in keyword style,
# such as 'id_flow="124356", heat_capacity=40.2'
parameters = models.TextField("General Chart Parameters in Keyword Form", blank=True)
# determines order of Chart displayed in Admin interface
sort_order = models.IntegerField(default=999)
def __unicode__(self):
return self.title
class Meta:
ordering = ['sort_order']
class ChartBuildingInfo(models.Model):
'''
Information about a building that is used in an MultiBuildingChart.
'''
# the associated chart
chart = models.ForeignKey(MultiBuildingChart)
# the building participating in the Chart
building = models.ForeignKey(Building)
# the parameters for this chart associated with this building, if any.
# The parameters are entered as one comma-separated string in keyword style,
# such as 'id_flow="124356", heat_capacity=40.2'
parameters = models.TextField("Chart Parameters in Keyword Form", blank=True)
# determines the order that this building appears in the chart
sort_order = models.IntegerField(default=999)
def __unicode__(self):
return self.chart.title + ": " + self.building.title
class Meta:
ordering = ['sort_order']
from django.db import models
# Models for the BMS App
class SensorGroup(models.Model):
'''
A functional group that the sensor belongs to, e.g. "Weather, Snowmelt System".
'''
# the diplay name of the Sensor Group.
title = models.CharField(max_length=40, unique=True)
# A number that determines the order that Sensor Groups will be presented to the user.
sort_order = models.IntegerField(default=999)
def __unicode__(self):
return self.title
class Meta:
ordering = ['sort_order']
class Unit(models.Model):
'''
A physical unit, e.g. "deg F", "gpm", for a sensor value.
'''
# the display name of the unit
label = models.CharField("Unit Label", max_length=20, unique=True)
# the type of physical quantity being measured, e.g. temperature, fluid flow, air flow, power
measure_type = models.CharField("Measurement Type", max_length=30)
def __unicode__(self):
return '%s: %s' % (self.measure_type, self.label)
class Meta:
ordering = ['measure_type', 'label']
class Sensor(models.Model):
'''
One sensor or a calculated field
'''
# sensor ID, either from Monnit network or a user-entered ID for a calculated value
sensor_id = models.CharField("Monnit Sensor ID, or Calculated Field ID", max_length=15, unique=True)
# descriptive title for the sensor, shown to users
title = models.CharField(max_length = 50)
# the units for the sensor values
unit = models.ForeignKey(Unit)
# if True, this field is a calculated field and is not directly created from a sensor.
is_calculated = models.BooleanField("Calculated Field")
# the name of the transform function to scale the value, if any, for a standard sensor field, or the
# calculation function (required) for a calculated field. transform functions must be located in the
# "transforms.py" module and calculated field functions must be located in "calculate_fields.py".
tran_calc_function = models.CharField("Transform or Calculated Field Function Name", max_length=35, blank=True)
# the function parameters, if any, for the transform or calculation function above. parameters are
# entered as one comma-separated string in keyword style, such as 'id_flow="124356", heat_capacity=40.2'
function_parameters = models.TextField("Function Parameters in Keyword form", blank=True)
# Calculation order. If this particular calculated field depends on the completion of other calculated fields
# first, make sure the calculation_order for this field is higher than the fields it depends on.
calculation_order = models.IntegerField(default=0)
def __unicode__(self):
return self.sensor_id + ": " + self.title
class Meta:
ordering = ['sensor_id']
class Building(models.Model):
'''
A building that contains sensors.
'''
# name of the building displayed to users
title = models.CharField(max_length=50, unique=True)
# the sensors and calculated values associated with this building
sensors = models.ManyToManyField(Sensor, through='BldgToSensor', blank=True, null=True)
# the sensors associated with key sensor values for this building.
# the Outdoor Temperature sensor
outdoor_temp_sensor = models.ForeignKey("BldgToSensor", blank=True, null=True, related_name='+')
# total electricity use, kW
total_elecricity_use = models.ForeignKey("BldgToSensor", blank=True, null=True, related_name='+')
# total fuel use, Btu/hour
total_fuel_use = models.ForeignKey("BldgToSensor", blank=True, null=True, related_name='+')
def __unicode__(self):
return self.title
class Meta:
ordering = ['title']
class BldgToSensor(models.Model):
'''
Links buildings to sensors and supplies additional information about what Sensor Group the sensor is
in and what order the sensor should be listed within the group. The Bldg-to-Sensor link is set up as
Many-to-Many beacause weather station sensors or calculated values can be used by multiple different
buildings.
'''
# The building and sensor that are linked
building = models.ForeignKey(Building)
sensor = models.ForeignKey(Sensor)
# For this building, the sensor group that the sensor should be classified in.
sensor_group = models.ForeignKey(SensorGroup)
# Within the sensor group, this field determines the sort order of this sensor.
sort_order = models.IntegerField(default=999)
def __unicode__(self):
return self.building.title + ": " + self.sensor.title
class Meta:
ordering = ('building__title', 'sensor_group__sort_order', 'sort_order')
#!/usr/local/bin/python2.7
import os, sys, sqlite3, logging, time
# change into this directory
os.chdir(os.path.dirname(sys.argv[0]))
sys.path.insert(0, '../') # add the parent directory to the Python path
import app_settings, bmsdata, calculated_readings
# make a logger object and set time zone so log readings are stamped with Alaska time.
# Did this because Django sets time to AK time.
os.environ['TZ'] = 'US/Alaska'
time.tzset()
logger = logging.getLogger('bms.calc_readings')
# get a BMSdata object for the sensor reading database and then make a Calculate
# Readings object. Only allow calculated readings within the last two hours (120 minutes).
reading_db = bmsdata.BMSdata(app_settings.DATA_DB_FILENAME)
calc = calculated_readings.CalculateReadings(reading_db, 120)
# get a database connection and cursor to the Django project database that has the sensor
# list.
conn = sqlite3.connect(app_settings.PROJ_DB_FILENAME)
cursor = conn.cursor()
# get all the calculated readings in calculation order
cursor.execute('SELECT sensor_id, tran_calc_function, function_parameters FROM bmsapp_sensor WHERE is_calculated = 1 ORDER BY calculation_order')
for row in cursor.fetchall():
try:
rec_count = calc.processCalc(row[0], row[1], row[2])
logger.debug( '%s %s readings calculated and inserted' % (rec_count, row[0]) )
except:
logger.exception('Error calculating %s readings' % row[0])
reading_db.close()
\ No newline at end of file
#!/usr/local/bin/python2.7
import os, sys, logging, time
# change into this directory
os.chdir(os.path.dirname(sys.argv[0]))
sys.path.insert(0, '../') # add the parent directory to the Python path
import app_settings, bmsdata
# make a logger object and set time zone so log readings are stamped with Alaska time.
# Did this because Django sets time to AK time.
os.environ['TZ'] = 'US/Alaska'
time.tzset()
logger = logging.getLogger('bms.daily_status')
# get a BMSdata object for the sensor reading database.
reading_db = bmsdata.BMSdata(app_settings.DATA_DB_FILENAME)
logger.info( '{:,} readings inserted in last day. {:,} total readings.'.format(reading_db.readingCount(time.time() - 3600*24), reading_db.readingCount()) )
reading_db.close()
body {
font-family: "Trebuchet MS", "Helvetica", "Arial", "Verdana", "sans-serif";
font-size: 90%;
}
html {
margin: 0;
padding: 0;
color: #000;
background: #ffffff;
}
a:link {
color: #0B6311;
text-decoration: none;
}
a:visited { color: #0B6311; }
a:hover {
color: #F5741D;
text-decoration: underline;
}
a:active { color: #aaa; }
a.selected-nav-link {
font-weight: bold;
color: #000;
}
a.selected-nav-link:hover {
color: #000;
text-decoration: none;
}
#wrap {
width: 950px;
margin: 0 auto;
background: #D4E7F7;
}
#header {
padding: 5px 10px;
background: #ffffff;
text-align: center;
font-size: 40px;
font-weight: bold;
}
#nav {
padding: 5px 10px;
background: #B2DBFF;
}
#nav ul {
margin: 0;
padding: 0;
list-style: none;
}
#nav li {
display: inline;
margin: 0;
padding: 10px;
}
#content {
margin-left: auto;
margin-right: auto;
padding: 10px;
}
#content h1 {
margin: 0px;
}
#time_period {
padding: 0px;
margin: 0 0 10px 0;
}
.ui-button {
font-size: 85%;
}
.but_lbl {
font-size: 75%;
}
#chart_container {
height: 550px;
}
.ui-multiselect {
font-size: 85%;
}
.ui-multiselect-menu {
font-size: 85%;
}
table {
background: #FFFFFF;
border-collapse: collapse;
margin-left: auto;
margin-right: auto;
}
thead {
background:#EEEEEE;
}
td, th {
padding-right: 5px;
padding-left: 5px;
border: 1px solid #999999;
}
th {
font-size: 110%;
}
td.number {
text-align: right;
}
td.indent {
padding-left: 20px;
}
tr.group_header {
font-weight: bold;
}
\ No newline at end of file
.ui-multiselect { padding:2px 0 2px 4px; text-align:left }
.ui-multiselect span.ui-icon { float:right }
.ui-multiselect-single .ui-multiselect-checkboxes input { position:absolute !important; top: auto !important; left:-9999px; }
.ui-multiselect-single .ui-multiselect-checkboxes label { padding:5px !important }
.ui-multiselect-header { margin-bottom:3px; padding:3px 0 3px 4px }
.ui-multiselect-header ul { font-size:0.9em }
.ui-multiselect-header ul li { float:left; padding:0 10px 0 0 }
.ui-multiselect-header a { text-decoration:none }
.ui-multiselect-header a:hover { text-decoration:underline }
.ui-multiselect-header span.ui-icon { float:left }
.ui-multiselect-header li.ui-multiselect-close { float:right; text-align:right; padding-right:0 }
.ui-multiselect-menu { display:none; padding:3px; position:absolute; z-index:10000; text-align: left }
.ui-multiselect-checkboxes { position:relative /* fixes bug in IE6/7 */; overflow-y:scroll }
.ui-multiselect-checkboxes label { cursor:default; display:block; border:1px solid transparent; padding:3px 1px }
.ui-multiselect-checkboxes label input { position:relative; top:1px }
.ui-multiselect-checkboxes li { clear:both; font-size:0.9em; padding-right:3px }
.ui-multiselect-checkboxes li.ui-multiselect-optgroup-label { text-align:center; font-weight:bold; border-bottom:1px solid }
.ui-multiselect-checkboxes li.ui-multiselect-optgroup-label a { display:block; padding:3px; margin:1px 0; text-decoration:none }
/* remove label borders in IE6 because IE6 does not support transparency */
* html .ui-multiselect-checkboxes label { border:none }
{"name":"ANTHC_Sites","type":"FeatureCollection"
,"crs":{"type":"name","properties":{"name":"urn:ogc:def:crs:OGC:1.3:CRS83"}}
,"features":[
{"type":"Feature","geometry":{"type":"Point","coordinates":[-161.327551740308,61.7848442712321]},"properties":{"Community":"Russian Mission","FacilityID":1,"Message":""}}
,{"type":"Feature","geometry":{"type":"Point","coordinates":[-170.483416499537,63.6895372306338]},"properties":{"Community":"Savoonga","FacilityID":1,"Message":""}}
,{"type":"Feature","geometry":{"type":"Point","coordinates":[-160.360372986981,61.5114755548242]},"properties":{"Community":"Lower Kalskag","FacilityID":1,"Message":""}}
,{"type":"Feature","geometry":{"type":"Point","coordinates":[-160.307518265371,61.5364705100918]},"properties":{"Community":"Upper Kalskag","FacilityID":1,"Message":""}}
,{"type":"Feature","geometry":{"type":"Point","coordinates":[-156.883828752217,66.9066134498121]},"properties":{"Community":"Kobuk","FacilityID":1,"Message":""}}
,{"type":"Feature","geometry":{"type":"Point","coordinates":[-157.854102987056,67.0854965620255]},"properties":{"Community":"Ambler","FacilityID":1,"Message":""}}
,{"type":"Feature","geometry":{"type":"Point","coordinates":[-160.425532255009,66.9743521845997]},"properties":{"Community":"Kiana","FacilityID":1,"Message":""}}
,{"type":"Feature","geometry":{"type":"Point","coordinates":[-161.035506894228,66.8376780084161]},"properties":{"Community":"Noorvik","FacilityID":1,"Message":""}}
,{"type":"Feature","geometry":{"type":"Point","coordinates":[-160.009586205537,66.6032568872637]},"properties":{"Community":"Selawik","FacilityID":1,"Message":"South Loop Circulation has Stopped."}}
,{"type":"Feature","geometry":{"type":"Point","coordinates":[-163.031635796445,64.5425428447876]},"properties":{"Community":"Golovin","FacilityID":1,"Message":""}}
,{"type":"Feature","geometry":{"type":"Point","coordinates":[-163.55566812836,63.0333814508606]},"properties":{"Community":"Kotlik","FacilityID":1,"Message":""}}
,{"type":"Feature","geometry":{"type":"Point","coordinates":[-165.588646344117,61.5269621345134]},"properties":{"Community":"Chevak","FacilityID":1,"Message":""}}
,{"type":"Feature","geometry":{"type":"Point","coordinates":[-159.773728804582,62.1987139052234]},"properties":{"Community":"Holy Cross","FacilityID":1,"Message":""}}
,{"type":"Feature","geometry":{"type":"Point","coordinates":[-157.314080763905,59.4520352025316]},"properties":{"Community":"New Stuyahok","FacilityID":1,"Message":""}}
,{"type":"Feature","geometry":{"type":"Point","coordinates":[-154.899391469714,59.7192860957181]},"properties":{"Community":"Newhalen","FacilityID":1,"Message":""}}
,{"type":"Feature","geometry":{"type":"Point","coordinates":[-161.590730678319,59.117810624579]},"properties":{"Community":"Goodnews Bay","FacilityID":1,"Message":""}}
,{"type":"Feature","geometry":{"type":"Point","coordinates":[-165.104714284754,60.5294835663577]},"properties":{"Community":"Toksook Bay","FacilityID":1,"Message":""}}
,{"type":"Feature","geometry":{"type":"Point","coordinates":[-158.52388085522,56.3528678359318]},"properties":{"Community":"Chignik Lagoon","FacilityID":1,"Message":""}}
,{"type":"Feature","geometry":{"type":"Point","coordinates":[-158.749316223161,56.2627328995011]},"properties":{"Community":"Chignik Lake","FacilityID":1,"Message":""}}
,{"type":"Feature","geometry":{"type":"Point","coordinates":[-149.330497302316,65.1557258590483]},"properties":{"Community":"Minto","FacilityID":1,"Message":""}}
,{"type":"Feature","geometry":{"type":"Point","coordinates":[-151.139079011188,61.0674945387515]},"properties":{"Community":"Tyonek","FacilityID":1,"Message":""}}
,{"type":"Feature","geometry":{"type":"Point","coordinates":[-162.041523036217,63.4772863172494]},"properties":{"Community":"Saint Michael","FacilityID":1,"Message":""}}
,{"type":"Feature","geometry":{"type":"Point","coordinates":[-163.290107839227,62.0320126724877]},"properties":{"Community":"Pitkas Point","FacilityID":1,"Message":""}}
,{"type":"Feature","geometry":{"type":"Point","coordinates":[-157.17204536555,61.7017953284101]},"properties":{"Community":"Sleetmute","FacilityID":1,"Message":""}}
,{"type":"Feature","geometry":{"type":"Point","coordinates":[-157.000187662519,58.7148154769625]},"properties":{"Community":"South Naknek","FacilityID":1,"Message":""}}
]}
// my object to contain all global variables and functions. Minimizes
// global namespace pollution.
var AN = {
chart: null, // the current chart object
chart_makers: {} // holds chart classes
};
// Returns the part of the URL that identifies the char type (multi-building
// or one building) and the chart ID.
AN.make_chart_id_url = function() {
// get building selected
var bldg = $("#select_bldg").val();
// get chart selected
var chart = $("#select_chart").val();
// create a string to identify the chart: "one/" + chart ID for a chart associated
// with one building. If the chart is associated with multiple
// buildings, the string is "multi/" + chart ID
if (bldg=="multi") {
return "multi/" + chart;
} else {
return "one/" + chart;
}
}
/*******************************************************************************
* Chart Classes and Helper Functions go here under the AN.chart_classes object *
********************************************************************************/
// Returns basic Highcharts options
AN.chart_makers.cht_options = function() {
return {
chart: {
renderTo: 'chart_container',
spacingTop: 20,
animation: true, // controls animation on redraws, not initial draw
zoomType: 'xy',
backgroundColor: '#EEEEEE',
borderWidth: 2,
plotBackgroundColor: '#FFFFFF',
plotBorderWidth: 1
},
title : {
style: {fontSize: '24px'},
},
subtitle : {
style: {fontSize: '22px'},
y : 40
},
yAxis : {
title: {
style: {fontSize: '16px'}
},
labels: {
style: {fontSize: '14px'}
}
},
xAxis: {
title: {
style: {fontSize: '16px'}
}
},
plotOptions: {
series: {
marker: {
enabled: false,
states: {
hover: {
enabled: true
}
}
}
}
},
exporting: {
sourceWidth: 930,
sourceHeight: 550,
scale: 1
},
credits: {
enabled: false
},
legend : {
enabled : true
},
series: [{}]
};
}
// Removes all the series on the 'the_chart' Highcharts chart and replaces them
// with 'new_series'. If 'only_show_first' is true, hide all series except the first
// (optional argument).
AN.chart_makers.replace_series = function(the_chart, new_series, only_show_first) {
// deal with optional 'only_show_first' argument
only_show_first = (typeof only_show_first === "undefined") ? false : only_show_first;
while (the_chart.series.length) { the_chart.series[0].remove() }
$.each(new_series, function(idx, a_series) {
// keeps colors starting from the beginning
a_series.color = the_chart.options.colors[idx];
the_chart.addSeries(a_series, false);
if (idx>0 & only_show_first) the_chart.series[idx].hide(); // only show first series initially
});
}
// Does some common configuration of the chart configuration UI and returns a
// basic chart object that contains properties and
AN.chart_makers.base_chart = function() {
// Configure many of the elements that commonly appear in chart configuration
// form.
$("#time_period").buttonset();
$("#averaging_time").buttonset();
// Related to selecting the range of dates to chart
$( "#start_date" ).datepicker({dateFormat: "mm/dd/yy"});
var d = new Date(); // current date to use for a default for Start Date
$( "#start_date" ).val((d.getMonth()+1) + "/" + d.getDate() + "/" + d.getFullYear());
$( "#end_date" ).datepicker({dateFormat: "mm/dd/yy"});
$("#custom_dates").hide(0); // hide custom dates element
// Show and Hide custom date range selector
$("#time_period").change(function() {
if ($('input:radio[name=time_period]:checked').val() != "custom") {
$("#custom_dates").hide();
} else {
$("#custom_dates").show();
}
});
// if the sensor select control is a multi-select, set it up
if($("#select_sensor[multiple]").length) {
$("#select_sensor").multiselect( {minWidth: 250} );
}
// Force Highcharts to display in the timezone of the Browser instead of UTC.
// this is a Global Highcharts option, not a chart specific option.
// Note, would be better to have Highcharts display in the timezone where the data
// was collected (US/Alaska), but that is not possible through configuration of
// Highcharts.
Highcharts.setOptions({
global: {
useUTC: false
}
});
// Retrieve the base set of chart options and customize somewhat
var ch_opt = AN.chart_makers.cht_options();
ch_opt.title.text = $("#select_chart option:selected").text() + ": " +
$("#select_bldg option:selected").text();
var cht_obj = {
chart_options: ch_opt,
data_url: "chart/" + AN.make_chart_id_url() + "/data/",
redraw: null // need to override this to configure and draw chart
};
// Because this function is used in an event callback, you cannot reference the
// object with the "this" keyword, since "this" is set to the DOM element that fired
// the event. So, instead, reference the object with the actual "cht_obj" variable
// which is maintained due to closure.
// NOTE: there is no "prototype" property for an object literal, so the "get_data" method
// is assigned directly to the cht_obj object.
cht_obj.get_data = function() {
if (cht_obj.chart) {
cht_obj.chart.showLoading("Loading Data");
}
$.getJSON(cht_obj.data_url, $("#config_chart").serialize(), function(the_data) {
cht_obj.server_data = the_data;
cht_obj.redraw();
cht_obj.chart.hideLoading();
});
};
// Make the Refresh button a jQuery UI button and wire it to the get_data() funcation.
// Have to remove any other handlers that were assigned to this button before
// (with the .off() method), since the button is part of the main page that is not
// replaced by AJAX queries.
$("#refresh").button().off('click').click(cht_obj.get_data);
return cht_obj;
}
// This creates a "chart" object for a text report. The only method it needs to expose
// is the 'get_data' method; this method does not need to do anything, since the
// report is created from the HTML returned by the server in the 'AN.update_chart_html'
// method near the bottom of this file.
AN.chart_makers.base_report = function() {
// Make the Refresh button a jQuery UI button and wire it
// to the method that gets the HTML.
// Have to remove any other handlers that were assigned to this button before
// (with the .off() method), since the button is part of the main page that is not
// replaced by AJAX queries.
$("#refresh").button().off('click').click(function(event) {
AN.update_chart_html();
});
return {
get_data: function() {}
};
}
/********************************************************************
Specific Chart Making functions follow
*********************************************************************/
// Hourly Profile chart creator function
AN.chart_makers.HourlyProfile = function() {
// Get the base chart and customize some options
var cht = AN.chart_makers.base_chart();
cht.chart_options.chart.type = "line";
cht.chart_options.xAxis.title.text = "Hour of the Day";
cht.chart_options.xAxis.categories =
['12a', '1a', '2a', '3a', '4a', '5a', '6a', '7a', '8a', '9a', '10a', '11a',
'12p', '1p', '2p', '3p', '4p', '5p', '6p', '7p', '8p', '9p', '10p', '11p'];
cht.redraw = function() {
// "this" won't refer to chart object if this function is
// called through an event callback.
// Set Y Axis title
cht.chart.yAxis[0].setTitle( {text: cht.server_data.y_label}, false );
// Set a Chart title indicating sensor, if there is a sensor selection involved
if ($("#select_sensor").length) {
var new_title = $("#select_sensor option:selected").text() + " Hourly Profile" + ": " + $("#select_bldg option:selected").text();
cht.chart.setTitle( {text: new_title,
style: {fontSize: '20px'}
});
}
// Remove all existing series and add new series
AN.chart_makers.replace_series(cht.chart, cht.server_data.series, true);
// if a normalized plot is occurring, set min and max of Y axis
if ($("#normalize").is(':checked')) {
cht.chart.yAxis[0].setExtremes(0, 100, false);
} else {
cht.chart.yAxis[0].setExtremes(null, null, false);
}
cht.chart.redraw();
};
$("#time_period").change(cht.get_data);
$("#select_sensor").change(cht.get_data);
$("#normalize").button();
$("#normalize").change(cht.get_data); // could be done locally, but easier at the server
cht.chart = new Highcharts.Chart(cht.chart_options);
return cht;
}
// Histogram chart object creator function
AN.chart_makers.Histogram = function() {
// Get the base chart and customize some options
var cht = AN.chart_makers.base_chart();
cht.chart_options.yAxis.title.text = "% of Readings";
cht.chart_options.yAxis.min = 0;
cht.chart_options.chart.type = "line";
cht.redraw = function() {
// "this" won't refer to chart object if this function is
// called through an event callback.
// Set Y Axis title
cht.chart.xAxis[0].setTitle( {text: cht.server_data.x_label}, false );
// Set a Chart title indicating sensor, if there is a sensor selection involved
if ($("#select_sensor").length) {
var new_title = $("#select_sensor option:selected").text() + " Histogram" + ": " + $("#select_bldg option:selected").text();
cht.chart.setTitle( {text: new_title,
style: {fontSize: '20px'}
});
}
// Remove all existing series and add new series
AN.chart_makers.replace_series(cht.chart, cht.server_data.series, true);
cht.chart.redraw();
};
$("#time_period").change(cht.get_data);
$("#select_sensor").change(cht.get_data);
cht.chart = new Highcharts.Chart(cht.chart_options);
return cht;
}
// Time Series Chart object creator function
AN.chart_makers.TimeSeries = function() {
// Get the base chart and customize some options
var cht = AN.chart_makers.base_chart();
cht.chart_options.title.text = "Time Series Plot: " + $("#select_bldg option:selected").text();
cht.chart_options.xAxis.title.text = "Date/Time (your computer's time zone)";
cht.chart_options.xAxis.type = "datetime";
cht.chart_options.chart.type = "line";
cht.redraw = function() {
// "this" won't refer to chart object if this function is
// called through an event callback.
// Create the needed Y Axes after removing existing axes
while (cht.chart.yAxis.length) { cht.chart.yAxis[0].remove() }
$.each(cht.server_data.y_axes, function(idx, axis_id) {
cht.chart.addAxis({
id: axis_id,
title: {
text: axis_id,
style: {fontSize: '16px'}
}
}, false, false);
});
// Remove all existing series and add new series
AN.chart_makers.replace_series(cht.chart, cht.server_data.series, false);
cht.chart.redraw();
};
$("#time_period").change(cht.get_data);
$("#select_sensor").bind('multiselectclose', cht.get_data);
$("#averaging_time").change(cht.get_data);
cht.chart = new Highcharts.Chart(cht.chart_options);
return cht;
}
// Current Values report
AN.chart_makers.CurrentValues = function() {
// Everything is done in the HTML returned by the server. Nothing to do here.
return AN.chart_makers.base_report();
}
// Export Data to Excel feature
AN.chart_makers.ExportData = function() {
// base chart provides a lot of needed UI setup, so use it.
var cht = AN.chart_makers.base_chart();
// don't need the get_data routine so replace it with a do nothing function
cht.get_data = function() {};
$("#download_many").button().click( function() {
window.location.href = "chart/" + AN.make_chart_id_url() + "/download_many/?" +
$("#config_chart").serialize();
});
return cht;
}
// base function for creating column charts that show a normalized value for a set of
// buildings.
AN.chart_makers.normalized_chart = function(norm_unit) {
// Get the base chart and customize some options
var cht = AN.chart_makers.base_chart();
cht.chart_options.title.text = $("#select_chart option:selected").text();
cht.chart_options.legend.enabled = false;
cht.chart_options.yAxis.min = 0;
cht.chart_options.chart.type = "column";
cht.chart_options.xAxis.labels = {
rotation: -45,
align: 'right',
style: {fontSize: '13px'}
};
cht.redraw = function() {
// "this" won't refer to chart object if this function is
// called through an event callback.
// set the building names to be the categories
cht.chart.xAxis[0].setCategories(cht.server_data.bldgs);
// set Y-axis title
cht.chart.yAxis[0].setTitle({text: cht.server_data.value_units + norm_unit});
// Remove all existing series and add new series
AN.chart_makers.replace_series(cht.chart, cht.server_data.series);
cht.chart.redraw();
};
$("#time_period").change(cht.get_data);
cht.chart = new Highcharts.Chart(cht.chart_options);
return cht;
}
// Normalized by Degree Day and Square foot chart object creator function
AN.chart_makers.NormalizedByDDbyFt2 = function() {
return AN.chart_makers.normalized_chart('/ft2/degree-day');
}
// Normalized by Square foot chart object creator function
AN.chart_makers.NormalizedByFt2 = function() {
return AN.chart_makers.normalized_chart('/ft2');
}
// ********************************* End of Chart Making Functions
// initializes the chart, including assigning event handlers and acquiring the chart data.
AN.init_chart = function() {
// Extract the chart function name from the html and Make a chart object.
var chart_func_name = $("#ChartFunc").text();
AN.chart = AN.chart_makers[chart_func_name]();
AN.chart.get_data();
}
// Updates the main content HTML based on the chart selected.
AN.update_chart_html = function() {
$.get("chart/" + AN.make_chart_id_url() + "/html/", function(chart_html) {
$("#chart_content").html(chart_html);
AN.init_chart();
});
}
// Updates the list of charts appropriate for the building selected.
AN.update_chart_list = function() {
// load the chart options from a AJAX query for the selected building
$("#select_chart").load("chart_list/" + $("#select_bldg").val() + "/");
// trigger the change event of the chart selector to get the
// selected option to process.
$("#select_chart").trigger("change");
}
// function that runs when the document is ready.
$(function() {
// Set up controls and functions to respond to events
$("#select_bldg").change(AN.update_chart_list);
$("#select_chart").change(AN.update_chart_html);
// Prep the chart and get the chart data.
AN.init_chart();
});
/*
* jQuery MultiSelect UI Widget 1.13
* Copyright (c) 2012 Eric Hynds
*
* http://www.erichynds.com/jquery/jquery-ui-multiselect-widget/
*
* Depends:
* - jQuery 1.4.2+
* - jQuery UI 1.8 widget factory
*
* Optional:
* - jQuery UI effects
* - jQuery UI position utility
*
* Dual licensed under the MIT and GPL licenses:
* http://www.opensource.org/licenses/mit-license.php
* http://www.gnu.org/licenses/gpl.html
*
*/
(function(d){var k=0;d.widget("ech.multiselect",{options:{header:!0,height:175,minWidth:225,classes:"",checkAllText:"Check all",uncheckAllText:"Uncheck all",noneSelectedText:"Select options",selectedText:"# selected",selectedList:0,show:null,hide:null,autoOpen:!1,multiple:!0,position:{}},_create:function(){var a=this.element.hide(),b=this.options;this.speed=d.fx.speeds._default;this._isOpen=!1;a=(this.button=d('<button type="button"><span class="ui-icon ui-icon-triangle-2-n-s"></span></button>')).addClass("ui-multiselect ui-widget ui-state-default ui-corner-all").addClass(b.classes).attr({title:a.attr("title"),"aria-haspopup":!0,tabIndex:a.attr("tabIndex")}).insertAfter(a);(this.buttonlabel=d("<span />")).html(b.noneSelectedText).appendTo(a);var a=(this.menu=d("<div />")).addClass("ui-multiselect-menu ui-widget ui-widget-content ui-corner-all").addClass(b.classes).appendTo(document.body),c=(this.header=d("<div />")).addClass("ui-widget-header ui-corner-all ui-multiselect-header ui-helper-clearfix").appendTo(a);(this.headerLinkContainer=d("<ul />")).addClass("ui-helper-reset").html(function(){return!0===b.header?'<li><a class="ui-multiselect-all" href="#"><span class="ui-icon ui-icon-check"></span><span>'+b.checkAllText+'</span></a></li><li><a class="ui-multiselect-none" href="#"><span class="ui-icon ui-icon-closethick"></span><span>'+b.uncheckAllText+"</span></a></li>":"string"===typeof b.header?"<li>"+b.header+"</li>":""}).append('<li class="ui-multiselect-close"><a href="#" class="ui-multiselect-close"><span class="ui-icon ui-icon-circle-close"></span></a></li>').appendTo(c);(this.checkboxContainer=d("<ul />")).addClass("ui-multiselect-checkboxes ui-helper-reset").appendTo(a);this._bindEvents();this.refresh(!0);b.multiple||a.addClass("ui-multiselect-single")},_init:function(){!1===this.options.header&&this.header.hide();this.options.multiple||this.headerLinkContainer.find(".ui-multiselect-all, .ui-multiselect-none").hide();this.options.autoOpen&&this.open();this.element.is(":disabled")&&this.disable()},refresh:function(a){var b=this.element,c=this.options,f=this.menu,h=this.checkboxContainer,g=[],e="",i=b.attr("id")||k++;b.find("option").each(function(b){d(this);var a=this.parentNode,f=this.innerHTML,h=this.title,k=this.value,b="ui-multiselect-"+(this.id||i+"-option-"+b),l=this.disabled,n=this.selected,m=["ui-corner-all"],o=(l?"ui-multiselect-disabled ":" ")+this.className,j;"OPTGROUP"===a.tagName&&(j=a.getAttribute("label"),-1===d.inArray(j,g)&&(e+='<li class="ui-multiselect-optgroup-label '+a.className+'"><a href="#">'+j+"</a></li>",g.push(j)));l&&m.push("ui-state-disabled");n&&!c.multiple&&m.push("ui-state-active");e+='<li class="'+o+'">';e+='<label for="'+b+'" title="'+h+'" class="'+m.join(" ")+'">';e+='<input id="'+b+'" name="multiselect_'+i+'" type="'+(c.multiple?"checkbox":"radio")+'" value="'+k+'" title="'+f+'"';n&&(e+=' checked="checked"',e+=' aria-selected="true"');l&&(e+=' disabled="disabled"',e+=' aria-disabled="true"');e+=" /><span>"+f+"</span></label></li>"});h.html(e);this.labels=f.find("label");this.inputs=this.labels.children("input");this._setButtonWidth();this._setMenuWidth();this.button[0].defaultValue=this.update();a||this._trigger("refresh")},update:function(){var a=this.options,b=this.inputs,c=b.filter(":checked"),f=c.length,a=0===f?a.noneSelectedText:d.isFunction(a.selectedText)?a.selectedText.call(this,f,b.length,c.get()):/\d/.test(a.selectedList)&&0<a.selectedList&&f<=a.selectedList?c.map(function(){return d(this).next().html()}).get().join(", "):a.selectedText.replace("#",f).replace("#",b.length);this.buttonlabel.html(a);return a},_bindEvents:function(){function a(){b[b._isOpen? "close":"open"]();return!1}var b=this,c=this.button;c.find("span").bind("click.multiselect",a);c.bind({click:a,keypress:function(a){switch(a.which){case 27:case 38:case 37:b.close();break;case 39:case 40:b.open()}},mouseenter:function(){c.hasClass("ui-state-disabled")||d(this).addClass("ui-state-hover")},mouseleave:function(){d(this).removeClass("ui-state-hover")},focus:function(){c.hasClass("ui-state-disabled")||d(this).addClass("ui-state-focus")},blur:function(){d(this).removeClass("ui-state-focus")}});this.header.delegate("a","click.multiselect",function(a){if(d(this).hasClass("ui-multiselect-close"))b.close();else b[d(this).hasClass("ui-multiselect-all")?"checkAll":"uncheckAll"]();a.preventDefault()});this.menu.delegate("li.ui-multiselect-optgroup-label a","click.multiselect",function(a){a.preventDefault();var c=d(this),g=c.parent().nextUntil("li.ui-multiselect-optgroup-label").find("input:visible:not(:disabled)"),e=g.get(),c=c.parent().text();!1!==b._trigger("beforeoptgrouptoggle",a,{inputs:e,label:c})&&(b._toggleChecked(g.filter(":checked").length!==g.length,g),b._trigger("optgrouptoggle",a,{inputs:e,label:c,checked:e[0].checked}))}).delegate("label","mouseenter.multiselect",function(){d(this).hasClass("ui-state-disabled")||(b.labels.removeClass("ui-state-hover"),d(this).addClass("ui-state-hover").find("input").focus())}).delegate("label","keydown.multiselect",function(a){a.preventDefault();switch(a.which){case 9:case 27:b.close();break;case 38:case 40:case 37:case 39:b._traverse(a.which,this);break;case 13:d(this).find("input")[0].click()}}).delegate('input[type="checkbox"], input[type="radio"]',"click.multiselect",function(a){var c=d(this),g=this.value,e=this.checked,i=b.element.find("option");this.disabled||!1===b._trigger("click",a,{value:g,text:this.title,checked:e})?a.preventDefault():(c.focus(),c.attr("aria-selected",e),i.each(function(){this.value===g?this.selected=e:b.options.multiple||(this.selected=!1)}),b.options.multiple||(b.labels.removeClass("ui-state-active"),c.closest("label").toggleClass("ui-state-active",e),b.close()),b.element.trigger("change"),setTimeout(d.proxy(b.update,b),10))});d(document).bind("mousedown.multiselect",function(a){b._isOpen&&(!d.contains(b.menu[0],a.target)&&!d.contains(b.button[0],a.target)&&a.target!==b.button[0])&&b.close()});d(this.element[0].form).bind("reset.multiselect",function(){setTimeout(d.proxy(b.refresh,b),10)})},_setButtonWidth:function(){var a=this.element.outerWidth(),b=this.options;/\d/.test(b.minWidth)&&a<b.minWidth&&(a=b.minWidth);this.button.width(a)},_setMenuWidth:function(){var a=this.menu,b=this.button.outerWidth()-parseInt(a.css("padding-left"),10)-parseInt(a.css("padding-right"),10)-parseInt(a.css("border-right-width"),10)-parseInt(a.css("border-left-width"),10);a.width(b||this.button.outerWidth())},_traverse:function(a,b){var c=d(b),f=38===a||37===a,c=c.parent()[f?"prevAll":"nextAll"]("li:not(.ui-multiselect-disabled, .ui-multiselect-optgroup-label)")[f?"last":"first"]();c.length?c.find("label").trigger("mouseover"):(c=this.menu.find("ul").last(),this.menu.find("label")[f? "last":"first"]().trigger("mouseover"),c.scrollTop(f?c.height():0))},_toggleState:function(a,b){return function(){this.disabled||(this[a]=b);b?this.setAttribute("aria-selected",!0):this.removeAttribute("aria-selected")}},_toggleChecked:function(a,b){var c=b&&b.length?b:this.inputs,f=this;c.each(this._toggleState("checked",a));c.eq(0).focus();this.update();var h=c.map(function(){return this.value}).get();this.element.find("option").each(function(){!this.disabled&&-1<d.inArray(this.value,h)&&f._toggleState("selected",a).call(this)});c.length&&this.element.trigger("change")},_toggleDisabled:function(a){this.button.attr({disabled:a,"aria-disabled":a})[a?"addClass":"removeClass"]("ui-state-disabled");var b=this.menu.find("input"),b=a?b.filter(":enabled").data("ech-multiselect-disabled",!0):b.filter(function(){return!0===d.data(this,"ech-multiselect-disabled")}).removeData("ech-multiselect-disabled");b.attr({disabled:a,"arial-disabled":a}).parent()[a?"addClass":"removeClass"]("ui-state-disabled");this.element.attr({disabled:a,"aria-disabled":a})},open:function(){var a=this.button,b=this.menu,c=this.speed,f=this.options,h=[];if(!(!1===this._trigger("beforeopen")||a.hasClass("ui-state-disabled")||this._isOpen)){var g=b.find("ul").last(),e=f.show,i=a.offset();d.isArray(f.show)&&(e=f.show[0],c=f.show[1]||this.speed);e&&(h=[e,c]);g.scrollTop(0).height(f.height);d.ui.position&&!d.isEmptyObject(f.position)?(f.position.of=f.position.of||a,b.show().position(f.position).hide()):b.css({top:i.top+a.outerHeight(),left:i.left});d.fn.show.apply(b,h);this.labels.eq(0).trigger("mouseover").trigger("mouseenter").find("input").trigger("focus");a.addClass("ui-state-active");this._isOpen=!0;this._trigger("open")}},close:function(){if(!1!==this._trigger("beforeclose")){var a=this.options,b=a.hide,c=this.speed,f=[];d.isArray(a.hide)&&(b=a.hide[0],c=a.hide[1]||this.speed);b&&(f=[b,c]);d.fn.hide.apply(this.menu,f);this.button.removeClass("ui-state-active").trigger("blur").trigger("mouseleave");this._isOpen=!1;this._trigger("close")}},enable:function(){this._toggleDisabled(!1)},disable:function(){this._toggleDisabled(!0)},checkAll:function(){this._toggleChecked(!0);this._trigger("checkAll")},uncheckAll:function(){this._toggleChecked(!1);this._trigger("uncheckAll")},getChecked:function(){return this.menu.find("input").filter(":checked")},destroy:function(){d.Widget.prototype.destroy.call(this);this.button.remove();this.menu.remove();this.element.show();return this},isOpen:function(){return this._isOpen},widget:function(){return this.menu},getButton:function(){return this.button},_setOption:function(a,b){var c=this.menu;switch(a){case "header":c.find("div.ui-multiselect-header")[b?"show":"hide"]();break;case "checkAllText":c.find("a.ui-multiselect-all span").eq(-1).text(b);break;case "uncheckAllText":c.find("a.ui-multiselect-none span").eq(-1).text(b);break;case "height":c.find("ul").last().height(parseInt(b,10));break;case "minWidth":this.options[a]=parseInt(b,10);this._setButtonWidth();this._setMenuWidth();break;case "selectedText":case "selectedList":case "noneSelectedText":this.options[a]=b;this.update();break;case "classes":c.add(this.button).removeClass(this.options.classes).addClass(b);break;case "multiple":c.toggleClass("ui-multiselect-single",!b),this.options.multiple=b,this.element[0].multiple=b,this.refresh()}d.Widget.prototype._setOption.apply(this,arguments)}})})(jQuery);
'''
Module used to store incoming sensor readings in the database.
'''
import dateutil.parser, calendar, re, time
from datetime import datetime
import bmsdata, app_settings, transforms
from os.path import join
def store(query_params, transform_func='', transform_params=''):
#print >> open(join(app_settings.APP_PATH, 'test.log'), 'a'), repr(transform_func), repr(transform_params)
# open the database
db = bmsdata.BMSdata(app_settings.DATA_DB_FILENAME)
try:
# parse the date into a datetime object and then into Unix seconds. Convert to
# integer.
if 'ts' in query_params:
ts = int( calendar.timegm(dateutil.parser.parse(query_params['ts']).timetuple()) )
else:
# no timestamp in query parameters, so assume the timestamp is now.
ts = int(time.time())
# get the sensor id
id = query_params['id']
# Determine the final value to store
val = query_params['val']
if ('True' in val) or ('Closed' in val) or (val.startswith('Motion') or (val.startswith('Light'))):
val = 1.0
elif ('False' in val) or ('Open' in val) or (val.startswith('No')):
val = 0.0
else:
# convert to float the first decimal number
parts = re.match(r'(-?\d+)\.?(\d+)?', val).groups('0')
val = float( parts[0] + '.' + parts[1] )
# if there is a transform function passed, use it to convert the reading values
if len(transform_func.strip()):
trans = transforms.Transformer(db)
ts, id, val = trans.transform_value(ts, id, val, transform_func, transform_params)
db.insert_reading(ts, id, val)
finally:
# close the database connection
db.close()
<h2 id="report_title">{{ report_title }}</h2>
<span id="ChartFunc" style="display:none">{{ chart_func }}</span>
<p style="color: red; text-align: center">Alarm Messages will display here.</p>
<table>
<thead><tr><th>Sensor</th><th>Value</th><th>Unit</th><th>When</th></tr></thead>
<tbody>
{% for group in sensor_list %}
<tr class="group_header"><td colspan="4">{{ group.0 }}</td></tr>
{% for sensor in group.1 %}
<tr><td class="indent"><a href="{% url 'reports-bldg-chart-sensor' bldg_id ts_chart_id sensor.sensor_id %}">{{ sensor.title }}</a></td>
<td class="number">{{ sensor.cur_value }}</td>
<td>{{ sensor.unit }}</td>
<td>{{ sensor.minutes_ago }} minutes ago</td>
</tr>
{% endfor %}
{% endfor %}
</tbody>
</table>
{% extends "bmsapp/chart_base.html" %}
{% block config %}
<h2 id="report_title">Download Sensor Data</h2>
<p>
Select Sensors: {{ select_sensor|safe }}&nbsp;&nbsp;
{% include "bmsapp/select_averaging_export.html" %}
</p>
{% include "bmsapp/select_time_period.html" %}
{% endblock config %}
{% block content %}
<p>
<button id="download_many">Download Excel Spreadsheet</button>
</p>
{% endblock %}
{% extends "bmsapp/chart_base.html" %}
{% block config %}
{% if select_sensor %}
<p>
Select Value to Plot: {{ select_sensor|safe }}
</p>
{% endif %}
{% include "bmsapp/select_time_period.html" %}
{% endblock config %}
{% extends "bmsapp/chart_base.html" %}
{% block config %}
<p>
{% if select_sensor %}
Select Value to Plot: {{ select_sensor|safe }}&nbsp;&nbsp;
{% endif %}
<input type="checkbox" id="normalize" name="normalize"/>
<label for="normalize" class="but_lbl">Scale Values to 0 - 100%</label></p>
{% include "bmsapp/select_time_period.html" %}
{% endblock config %}
{% extends "bmsapp/chart_base.html" %}
{% block config %}
{% include "bmsapp/select_time_period.html" %}
{% endblock config %}
{% extends "bmsapp/chart_base.html" %}
{% block config %}
{% include "bmsapp/select_time_period.html" %}
{% endblock config %}
{% extends "bmsapp/chart_base.html" %}
{% block config %}
<p>
{% if select_sensor %}
Select Values to Plot: {{ select_sensor|safe }}&nbsp;&nbsp;
{% endif %}
{% include "bmsapp/select_averaging.html" %}
</p>
{% include "bmsapp/select_time_period.html" %}
{% endblock config %}
{% load staticfiles %}
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html lang="en">
<head>
<title>ANTHC Remote Monitoring - {%block pagetitle %}{% endblock %}</title>
<link rel="stylesheet" type="text/css" href="http://code.jquery.com/ui/1.10.3/themes/smoothness/jquery-ui.min.css">
<link rel="stylesheet" type="text/css" href="{% static 'bmsapp/css/bmsapp.css' %}">
<script src="http://code.jquery.com/jquery-1.9.1.min.js"></script>
<script src="http://code.jquery.com/ui/1.10.3/jquery-ui.min.js"></script>
<script src="http://html5shiv.googlecode.com/svn/trunk/html5.js"></script>
<script>
$(function () {
$("#{% block this_nav_link %}{% endblock %}").addClass("selected-nav-link").removeAttr("href");
});
</script>
{% block head %}{% endblock %}
</head>
<body>
<div id="wrap">
<div id="header">ANTHC Remote Monitoring</div>
<div id="nav">
<ul>
<li><a href="{% url 'bmsapp.views.map' %}" id="link_map">Map</a></li>
<li><a href="{% url 'bmsapp.views.reports' %}" id="link_reports">Data Charts and Reports</a></li>
<li><a href="{% url 'bmsapp.views.training' %}" id="link_training">Training Videos and Project Reports</a></li>
</ul>
</div>
<div id="content">
<h1>{% block title %}{% endblock %}</h1>
{% block content %}{% endblock %}
<hr>
<p><img src="{% static 'bmsapp/images/ahfc_logo.png' %}" width="75" height="58" border="0" alt="AHFC Logo"
style="vertical-align:middle">&nbsp;&nbsp;
Thanks to the Alaska Housing Finance Corporation for providing a substantial portion of the source code for this website.</p>
</div>
</div>
</body>
</html>
{% for id, title in bldg_list %}
<option value="{{ id }}"{% if id == id_to_select %} selected{% endif %}>{{ title }}</option>
{% endfor %}
<span id="ChartFunc" style="display:none">{{ chart_func }}</span>
<form id="config_chart">
{% block config %}
{% endblock %}
</form>
{% block content %}
<div id="chart_container"></div>
{% endblock %}
\ No newline at end of file
{% for cht in cht_list %}
<option value="{{ cht.id }}"{% if cht.id == id_to_select %} selected{% endif %}>{{ cht.title }}</option>
{% endfor %}
{% extends "bmsapp/base.html" %}
{% load staticfiles %}
{% block pagetitle %}Map{% endblock %}
{% block head %}
<style>
#map_canvas {
width: 930px;
height: 600px;
margin-left:auto;
margin-right:auto;
}
#content {
background-color: #fff;
}
</style>
<script src="http://maps.googleapis.com/maps/api/js?sensor=false"></script>
<script>
function initialize() {
var map_canvas = document.getElementById('map_canvas');
var map_options = {
center: new google.maps.LatLng(65, -155),
zoom: 4,
mapTypeId: google.maps.MapTypeId.TERRAIN
}
var map = new google.maps.Map(map_canvas, map_options)
var xhr = new XMLHttpRequest();
xhr.open('GET', '{% static "bmsapp/map_data.json" %}', true);
xhr.onload = function() {loadSites(JSON.parse(this.responseText),map);};
xhr.send();
}
function loadSites(sitesJSON,map) {
for (var i = 0; i < sitesJSON.features.length; i++) {
var site = sitesJSON.features[i];
var coords = site.geometry.coordinates;
var latLng = new google.maps.LatLng(coords[1],coords[0]);
var markerText = site.properties.Community;
var markerColor = 'green';
if (site.properties.Message.length > 0) {
markerText = markerText + '\n' + site.properties.Message;
markerColor = 'red';
};
var marker = new google.maps.Marker({
position: latLng,
title: markerText,
icon: {
path: google.maps.SymbolPath.CIRCLE,
fillColor: markerColor,
fillOpacity: .6,
scale: 5,
strokeColor: 'black',
strokeWeight: .5
},
scale: .5,
map: map
});
google.maps.event.addListener(marker, 'click', function() {window.location.href = '{% url "reports" %}' + site.properties.FacilityID.toString() + '/'})
}
}
google.maps.event.addDomListener(window, 'load', initialize);
</script>
{% endblock %}
{% block this_nav_link %}link_map{% endblock %}
{% block content %}
<div id="map_canvas"></div>
{% endblock %}
{% extends "bmsapp/base.html" %}
{% load staticfiles %}
{% block pagetitle %}Reports/Charts{% endblock %}
{% block head %}
<link rel="stylesheet" type="text/css" href="{% static 'bmsapp/css/jquery.multiselect.css' %}">
<script src="http://code.highcharts.com/3/highcharts.js"></script>
<script src="http://code.highcharts.com/3/modules/exporting.js"></script>
<script src="{% static 'bmsapp/scripts/jquery.multiselect.min.js' %}"></script>
<script src="{% static 'bmsapp/scripts/bmsapp.js' %}"></script>
{% endblock %}
{% block this_nav_link %}link_reports{% endblock %}
{% block title %}Charts and Reports{% endblock %}
{% block content %}
<p>
Select Facility: <select id="select_bldg">
{{ bldgs_html }}
</select>
Select Chart/Report: <select id="select_chart">{{ chart_list_html }}</select>
&nbsp;&nbsp;&nbsp;<button id="refresh" type="button">Refresh Data</button>
</p>
<div id="chart_content">{{ chart_html }}</div>
{% endblock %}
<span id="averaging_time">
Data Averaging:
<input type="radio" id="avg_none" value="0" name="averaging_time" checked/><label for="avg_none" class="but_lbl">None</label>
<input type="radio" id="avg_half_hr" value="0.5" name="averaging_time"/><label for="avg_half_hr" class="but_lbl">1/2 hr</label>
<input type="radio" id="avg_1hr" value="1" name="averaging_time"/><label for="avg_1hr" class="but_lbl">1 hr</label>
<input type="radio" id="avg_2hr" value="2" name="averaging_time"/><label for="avg_2hr" class="but_lbl">2 hr</label>
<input type="radio" id="avg_4hr" value="4" name="averaging_time"/><label for="avg_4hr" class="but_lbl">4 hr</label>
<input type="radio" id="avg_8hr" value="8" name="averaging_time"/><label for="avg_8hr" class="but_lbl">8 hr</label>
<input type="radio" id="avg_1da" value="24" name="averaging_time"/><label for="avg_1da" class="but_lbl">1 da</label>
<input type="radio" id="avg_1wk" value="168" name="averaging_time"/><label for="avg_1wk" class="but_lbl">1 wk</label>
</span>
<span id="averaging_time">
Data Averaging:
<input type="radio" id="avg_15min" value="0.25" name="averaging_time"/><label for="avg_15min" class="but_lbl">15 min</label>
<input type="radio" id="avg_30min" value="0.5" name="averaging_time" checked/><label for="avg_30min" class="but_lbl">30 min</label>
<input type="radio" id="avg_1hr" value="1" name="averaging_time"/><label for="avg_1hr" class="but_lbl">1 hr</label>
<input type="radio" id="avg_2hr" value="2" name="averaging_time"/><label for="avg_2hr" class="but_lbl">2 hr</label>
<input type="radio" id="avg_4hr" value="4" name="averaging_time"/><label for="avg_4hr" class="but_lbl">4 hr</label>
<input type="radio" id="avg_8hr" value="8" name="averaging_time"/><label for="avg_8hr" class="but_lbl">8 hr</label>
<input type="radio" id="avg_1da" value="24" name="averaging_time"/><label for="avg_1da" class="but_lbl">1 da</label>
<input type="radio" id="avg_1wk" value="168" name="averaging_time"/><label for="avg_1wk" class="but_lbl">1 wk</label>
</span>
<div id="time_period">
Select Time Period:
<input type="radio" id="1d" value="1" name="time_period"/><label for="1d" class="but_lbl">1 da</label>
<input type="radio" id="3d" value="3" name="time_period"/><label for="3d" class="but_lbl">3 da</label>
<input type="radio" id="1w" value="7" name="time_period" checked="checked"/><label for="1w" class="but_lbl">1 wk</label>
<input type="radio" id="2w" value="14" name="time_period"/><label for="2w" class="but_lbl">2 wk</label>
<input type="radio" id="1m" value="31" name="time_period"/><label for="1m" class="but_lbl">1 mo</label>
<input type="radio" id="2m" value="61" name="time_period"/><label for="2m" class="but_lbl">2 mo</label>
<input type="radio" id="4m" value="122" name="time_period"/><label for="4m" class="but_lbl">4 mo</label>
<input type="radio" id="1y" value="365" name="time_period"/><label for="1y" class="but_lbl">1 yr</label>
<input type="radio" id="all" value="10000" name="time_period"/><label for="all" class="but_lbl">All</label>
<input type="radio" id="custom" value="custom" name="time_period"/><label for="custom" class="but_lbl">Custom</label>
<span id="custom_dates">
&nbsp;&nbsp;&nbsp;Start: <input id="start_date" type="text" name="start_date" size="10"/>
End: <input id="end_date" type="text" name="end_date" size="10"/>
</span>
</div>
{% extends "bmsapp/base.html" %}
{% load staticfiles %}
{% block pagetitle %}Training and Reports{% endblock %}
{% block this_nav_link %}link_training{% endblock %}
{% block title %}Training Material and Reports{% endblock %}
{% block content %}
<p>This page contains Reports, Presentations and a number of Videos that explain how to use the ANTHC Remote
Monitoring website. This website is based upon a website developed for the Alaska Housing Finance Corporation (AHFC)
Building Monitoring System project. Most of the reports and training material below comes from that project.</p>
<h2>Reports and Presentations</h2>
<p><a href="{% static 'bmsapp/reports/deliverable_2_6_7.pdf' %}"><b>Final Report for the Alaska Housing
Finance Corporation (AHFC) Building Monitoring System Project:</b></a> Design
of the Monitoring Systems, Benefits from Monitoring various System Types, Installation Tips,
Internet Access Issues.</p>
<h2>Training Videos</h2>
<p>Adobe Flash Player must be installed on your computer to view these videos. Click on the image
of the video's title page to start the video.</p>
<table width="800">
<tbody>
<tr>
<td><a href="{% url 'show-video' 'overview' 999 955 %}"><img src="{% static 'bmsapp/images/overview.png' %}"
width="320" height="240" border="0" alt="Overview Video"></a></td>
<td>(10 minutes) This video gives a brief overview the AHFC Building Monitoring System project. Much of
the content is also relevant to the ANTHC Remote Monitoring effort.</td>
</tr>
<tr>
<td><a href="{% url 'show-video' 'basic_features' 974 941 %}"><img src="{% static 'bmsapp/images/basic_features.png' %}"
width="320" height="240" border="0" alt="Basic Features Video"></a></td>
<td>(9 minutes) This video shows how to select various various reports and graphs for each facility.
The video also explains a number of graphing features, such as zooming, highlighting points, and hiding
data series on the graph. These features are common to all the graphs on the web site, and this
video is an important one to watch prior to watching the videos below.</td>
</tr>
<tr>
<td><a href="{% url 'show-video' 'multiple' 975 892 %}"><img src="{% static 'bmsapp/images/multiple.png' %}"
width="320" height="240" border="0" alt="Multiple Building Video"></a></td>
<td>(13 minutes) This video explains the graphs that are available to compare energy use across
multiple buildings, a feature not implemented for the ANTHC Remote Montioring project.
However, it also describes the "Current Values" report, which is implemented in this site. This
report shows the current sensor readings for any one facility.</td>
</tr>
<tr>
<td><a href="{% url 'show-video' 'time_series' 972 935 %}"><img src="{% static 'bmsapp/images/time_series.png' %}"
width="320" height="240" border="0" alt="Plot over Time Video"></a></td>
<td>(8 minutes) This video explains the "Plot over Time" graph, also known as the "Time Series" plot.
This graph lets you graph how one or more sensor values change over time. This is the
workhorse graph of the system, so this video is important.</td>
</tr>
<tr>
<td><a href="{% url 'show-video' 'hourly_profile' 967 931 %}"><img src="{% static 'bmsapp/images/hourly_profile.png' %}"
width="320" height="240" border="0" alt="Hourly Profile Video"></a></td>
<td>(8 minutes) This video explains the "Hourly Profile" chart.
This graph lets you graph how a sensor's value changes across the hours in the day. It also allows
you to select particular days to look at, such as Monday through Friday or just Saturday.</td>
</tr>
<tr>
<td><a href="{% url 'show-video' 'histogram' 970 932 %}"><img src="{% static 'bmsapp/images/histogram.png' %}"
width="320" height="240" border="0" alt="Histogram Video"></a></td>
<td>(6 minutes) This video explains the Histogram chart.
This graph is good for learning about the range of values measured by a sensor. It also shows you
the values are recorded most frequently and least frequently.</td>
</tr>
<tr>
<td><a href="{% url 'show-video' 'download' 971 896 %}"><img src="{% static 'bmsapp/images/download.png' %}"
width="320" height="240" border="0" alt="Download to Excel Video"></a></td>
<td>(5 minutes) This video explains how to download sensor data into an Excel spreadsheet.</td>
</tr>
<tr>
<td><a href="{% url 'show-video' 'new_sensor' 971 896 %}"><img src="{% static 'bmsapp/images/new_sensor.png' %}"
width="320" height="240" border="0" alt="Adding New Sensor Video"></a></td>
<td>(11 minutes) This video explains how to add a new Monnit Wireless Sensor to the monitoring
system. This video is only appropriate for system administrators and is not meant for general
users of the monitoring system.</td>
</tr>
</tbody>
</table>
{% endblock %}
{% load staticfiles %}
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8" />
<title>BMS Training Video</title>
<style type="text/css">
#movie
{
width: {{ width }}px;
height: {{ height }}px;
}
</style>
</head>
<body>
<p><a href="/training/">Back to Video List</a></p>
<div>
<object id="movie" type="application/x-shockwave-flash" data="{% static 'bmsapp/videos/' %}{{ filename }}.swf">
<param name="movie" value="{% static 'bmsapp/videos/' %}{{ filename }}.swf" />
<param name="quality" value="high" />
<param name="bgcolor" value="#ffffff" />
<p>You do not have the latest version of Flash installed. Please visit this link to download it: <a href="http://www.adobe.com/products/flashplayer/">http://www.adobe.com/products/flashplayer/</a></p>
</object>
</div>
</body>
</html>
'''
Transform functions for scaling and transforming sensor readings
'''
import sys
def makeKeywordArgs(keyword_str):
'''
Turns a string that looks like a set of keyword arguments into a dictionary of those
arguments. Numbers are converted to floats, except the 'id_' exception mentioned below.
Boolean are created if the text looks like a boolean. Otherwise a string is created as
the value. There is a special exception for keyword names that start with the string 'id_':
these are always converted to strings. This conveniently allows sensor ids to be entered
without quotes in parameter lists.
'''
result = {}
keyword_str = keyword_str.strip()
# need to exit if this is a blank string
if len(keyword_str)==0:
return result
for it in keyword_str.strip().split(','):
kw, val = it.split('=')
kw = kw.strip()
val = val.strip()
if kw.startswith('id_'):
# special case of keyword starting with 'id_'. Assume val is a string
# and strip any surrounding quotes of both types.
val = val.strip('"\'')
else:
try:
val = float(val)
except:
if val in ('True', 'true', 'Y', 'y', 'Yes', 'yes'):
val = True
elif val in ('False', 'false', 'N', 'n', 'No', 'no'):
val = False
else:
# must be a string.
# get rid of surrounding quotes of both types.
val = val.strip('"\'')
result[kw] = val
return result
class Transformer:
def __init__(self, db=None):
'''
Creates Transforms object. 'db' is the a bmsdata.BMSdata object that gives
gives access to the reading database, as some of the transform functions
need that access.
'''
# store database as an object variable
self.db = db
def transform_value(self, ts, id, val, trans_func, trans_params):
'''
Returns transformed sensor reading information, using a transformation
function identified by the string trans_func, and additional parameters to that function
given by the string 'trans_params'. That string is in keyword format, like "abc=23.3, xyz=True".
All three elements of the reading--ts, id, and val--can be transformed by the function.
'''
the_func = getattr(self, trans_func.strip())
params = makeKeywordArgs(trans_params)
return the_func(ts, id, val, **params)
# ******** Add Transform Functions below Here *********
#
# Transform functions must accept the ts, id, and val components of the sensor reading
# as the first three function parameters after self. The functions must return the same
# three values, transformed. Any of the three values can be transformed by the function.
def linear(self, ts, id, val, slope=1.0, offset=0.0):
'''Linear transformation of value'''
return ts, id, val * slope + offset
def count_rate(self, ts, id, val, typical_minutes=30.0, slope=1.0, offset=0.0, no_zero_after_link=True):
'''
Determines the rate/second of the count value 'val'. Assumes the 'val' count
occurred since the last reading in the database for this sensor (i.e. count readings are reset
after each read). After determining
the rate per second, 'slope' and 'offset' are used to linearly scale that value.
If there is no readings in the database for this sensor,
this function assumes the interval is 'typical_minutes' long and calulates a rate based on
that. If the interval between this reading and the last is different than typical_minutes by
more than 10%, the best interval to use is determined in the code below.
If 'no_zero_after_link' is True, then a zero count value is rejected if the sensor has not reported
for four or more 'typical_minutes'. This feature is present because Monnit sensors report a 0
after having been asleep in link mode. This routine substitutes the last reading value for the zero
value reported by the sensor.
'''
# for convenience, translate typical_minutes into seconds
typ_secs = typical_minutes * 60.0
# First, transform the timestamp to approximately the center of the read interval, since the
# count has occurred across the read interval and a mid-point timestamp is more appropriate.
# Use typ_secs to do this translation, not the actual interval, because we want to the use the
# same translation for every reading so that differences between readings are not distorted.
new_ts = ts - typ_secs * 0.5
# get the last reading for this sensor
last_read = self.db.last_read(id)
if last_read:
# calculate interval between current read and last. Override the interval if
# is more than 10% different from the typical interval.
interval = new_ts - float(last_read['ts']) # last read has a mid-point ts already
if no_zero_after_link and (interval >= 0.9 * 4.0 * typ_secs) and val==0.0: # 0.9 to account for timing inaccuracy
# sensor was probably in link mode; use the last reading
return new_ts, id, last_read['val']
elif interval / typ_secs < 0.9:
# interval is substantially shorter than standard one, probably a doubled up transmission.
# Just use the last reading value.
return new_ts, id, last_read['val']
elif interval / typ_secs > 1.1:
# interval is longer than standard interval. This could be a reboot, a missed prior
# transmission, etc. So, its not clear what the interval is, but it has to be a multiple
# of heartbeat. Use the typical seconds value as the heartbeat, and try multiples of the
# heartbeat up to 4. Use the one that yields the closest value to the last value
# track the minimum deviation from the last value.
# initialize to a large deviation.
min_dev = sys.float_info.max
for mult in range(1, 5):
test_val = slope * val / (mult * typ_secs) + offset
deviation = abs(test_val - last_read['val'])
if deviation < min_dev:
min_dev = deviation
best_val = test_val
return new_ts, id, best_val
else:
# no prior reading, so use the typical heartbeat value to calculate pulse rate.
interval = typ_secs
# calculate count/sec and apply slope and offset
return new_ts, id, (slope * val / interval + offset)
# ******** End of Transform Function Section **********
# ------------ Test functions --------------
def test_kw():
'''
Test function for makeKeywordArgs.
'''
print makeKeywordArgs('abc=True, xyz=23.3, jlk="Hello"')
print makeKeywordArgs("abc=Yes, xyz=23.3, jlk='Hello'")
print makeKeywordArgs("abc=Yes, xyz=23.3, jlk=Hello")
def test_transform():
import bmsdata, time
# t = Transformer()
# print t.transform_value(10, 'linear', 'slope=2.0, offset=5')
# print t.transform_value(10, 'linear', 'slope=2.0')
# print t.transform_value(10, 'linear', 'offset=5')
t = Transformer(bmsdata.BMSdata('data/bms_data.sqlite'), time.time(), '25236')
print t.transform_value(30, 'count_rate', 'typical_minutes=60.0, slope=60.0')
if __name__ == '__main__':
test_transform()
\ No newline at end of file
'''
URLs for the BMS Application
'''
from django.conf.urls import patterns, url
urlpatterns = patterns('bmsapp.views',
url(r'^$', 'index'),
url(r'^map/$', 'map'),
url(r'^reports/$', 'reports', name='reports'),
url(r'^reports/(multi|\d+)/$', 'reports'),
url(r'^reports/(multi|\d+)/(\d+)/$', 'reports'),
url(r'^reports/(multi|\d+)/(\d+)/(\w+)/$', 'reports', name='reports-bldg-chart-sensor'),
url(r'^stB3D82/', 'store_reading'), # toy security: obscure the URL
url(r'^store_test/$', 'store_test'),
url(r'^show_log/$', 'show_log'),
url(r'/chart_list/(multi)/$', 'chart_list'),
url(r'/chart_list/(\d+)/$', 'chart_list'),
url(r'/chart/(multi|one)/(\d+)/([a-zA-Z_]+)/', 'chart_info'),
url(r'^training/$', 'training'),
url(r'^training/video/(\w+)/(\d+)/(\d+)/$', 'show_video', name='show-video'),
)
'''
Helper functions for the views in this BMS application.
'''
from django.template import Context, loader
import models
def to_int(val):
'''
Trys to convert 'val' to an integer and returns the integer. If 'val' is not an integer,
just returns the original 'val'.
'''
try:
return int(val)
except:
return val
def chart_from_id(chart_type, chart_id):
'''
Returns a chart object from the application models based on 'chart_type' ('multi' for multi-building charts
or anything else for one-building charts) and a chart ID 'chart_id'.
'''
if chart_type=='multi':
return models.MultiBuildingChart.objects.get(id=int(chart_id))
else:
return models.BuildingChart.objects.get(id=int(chart_id))
def bldg_list_html(selected_bldg=None):
'''
Makes the html for the building list options and also returns the ID of the selected building.
(when 'selected_bldg" is passed in as None, the first building is selected and that ID is returned).
'selected_bldg' is the ID of the building to select in the option list. If None, then
the first building is selected.
'''
bldgs = []
# Determine whether a Multi building chart selection should be presented.
if models.MultiBuildingChart.objects.count() > 0:
bldgs.append( ('multi', 'Multiple Buildings') )
# Add the rest of buildings
for bldg in models.Building.objects.all():
bldgs.append( (bldg.id, bldg.title) )
if selected_bldg==None:
selected_bldg = bldgs[0][0] # select first ID
t = loader.get_template('bmsapp/bldg_list.html')
return t.render( Context({'bldg_list': bldgs, 'id_to_select': selected_bldg}) ), selected_bldg
def chart_list_html(bldg, selected_chart=None):
'''
Makes the html for the chart list options and also returns the ID of the selected chart.
(which is the passed in 'selected_chart' value, except a real chart ID is returned is
selected_chart=None.)
'bldg' is the ID of the of building to list charts for, or it is the string 'multi'
indicating that the multi-building charts should be listed.
'selected_chart' is the ID of the chart option to be selected. If None,
the first chart is selected.
'''
if bldg != 'multi':
cht_list = models.BuildingChart.objects.filter(building=bldg)
else:
cht_list = models.MultiBuildingChart.objects.all()
# Determine the chart ID to select
if selected_chart==None and len(cht_list)>0:
# select the first chart if selected_chart is None
id_to_select = cht_list[0].id
else:
id_to_select = selected_chart
t = loader.get_template('bmsapp/chart_list.html')
return t.render( Context({'cht_list': cht_list, 'id_to_select': id_to_select}) ), id_to_select
# Create your views here.
from django.http import HttpResponse
from django.shortcuts import render_to_response, redirect
from django.contrib.auth.decorators import login_required
from django.views.decorators.csrf import csrf_exempt
from django.core.urlresolvers import reverse
import models, storereads, app_settings, charts, view_util
import sys, logging, json
# Make a logger for this module
_logger = logging.getLogger('bms.' + __name__)
def index(request):
'''
The main home page for the site, which redirects to the desired page to show for the home page.
'''
#return redirect('/map/')
return redirect( reverse('bmsapp.views.map') )
def reports(request, selected_bldg=None, selected_chart=None, selected_sensor=None):
'''
The main graphs/reports page.
'''
# try to convert the selected building value to an integer (might be the string 'multi' or None) so that
# it will match the integer IDs in the database.
selected_bldg = view_util.to_int(selected_bldg)
# get the html for the list of buildings and the ID of the a selected building (converts a None
# into the ID of the first item).
bldgs_html, bldg_id_selected = view_util.bldg_list_html(selected_bldg)
# try to convert selected chart to an integer
selected_chart = view_util.to_int(selected_chart)
# get the html for the list of charts, selecting the requested one. Also returns the actual ID
# of the chart selected (a selected_chart=None is converted to an actual ID).
chart_list_html, chart_id_selected = view_util.chart_list_html(bldg_id_selected, selected_chart)
# get the html for configuring and displaying this particular chart
chart = view_util.chart_from_id(bldg_id_selected, chart_id_selected)
chart_class = getattr(charts, chart.chart_type.class_name) # get the class name to instantiate a chart object
chart_obj = chart_class(chart, request.GET) # Make the chart object from the class
chart_html = chart_obj.html(selected_sensor)
return render_to_response('bmsapp/reports.html', {'bldgs_html': bldgs_html, 'chart_list_html': chart_list_html, 'chart_html': chart_html})
def show_log(request):
'''
Returns the application's log file, without formatting.
'''
return HttpResponse('<pre>%s</pre>' % open(app_settings.LOG_FILE).read())
@csrf_exempt # needed to accept HTTP POST requests from the AHFC BAS system.
def store_reading(request):
'''
Stores a sensor reading in the sensor reading database. The sensor reading information is
provided in the GET parameters of the request.
'''
try:
# determine whether request was a GET or POST and extract data
data = request.GET.dict() if request.method == 'GET' else request.POST.dict()
# get the Sensor object, if available, to pass along a transform function
sensors = models.Sensor.objects.filter( sensor_id=data['id'] )
if len(sensors)>0:
# Take first sensor in list ( should be only one )
storereads.store(data, sensors[0].tran_calc_function, sensors[0].function_parameters)
else:
# no sensor with the requested ID was found. Therefore, no transform function and parameters
# to pass.
storereads.store(data)
return HttpResponse('OK')
except:
_logger.exception('Error Storing Reading: %s' % request.GET.dict())
return HttpResponse('Error Storing: %s' % request.GET.dict())
def store_test(request):
_logger.info('store_test GET Parameters: %s' % request.GET.dict())
return HttpResponse('%s' % request.GET.dict())
def chart_list(request, selected_bldg):
'''
Returns a JSON array of the charts that are available for the building with the pk ID
of 'bldg'. If 'bldg' is the string 'multi', the list of multi-building charts is returned.
The return value is an array of objects, each object having an 'id' property, giving the pk ID
of the chart, and a 'title' property giving the title of the chart.
'''
# try to convert the selected building value to an integer (might be the string 'multi') so that
# it will match the integer IDs in the database.
selected_bldg = view_util.to_int(selected_bldg)
charts_html, id_selected = view_util.chart_list_html(selected_bldg)
return HttpResponse(charts_html)
def chart_info(request, bldg_group, chart_id, info_type):
'''
Returns the HTML or data needed to display a chart
'bldg_group' is either 'one' or 'multi', indicating whether the chart is for one
building (one) or a group of buildings (multi). 'chart_id' is the pk ID of the chart requested.
'info_type' is the type of chart information requested: 'html' to request the HTML for
the chart page, 'data' to request the data for the chart, or the name of a method on the
created chart class to call to return data.
'''
try:
# get the chart object from the model
chart = view_util.chart_from_id(bldg_group, chart_id)
# get the appropriate class to instantiate a chart object
chart_class = getattr(charts, chart.chart_type.class_name)
# Make the chart object from the class
chart_obj = chart_class(chart, request.GET)
# Return the type of data indicated by 'info_type'
if info_type=='html':
return HttpResponse(chart_obj.html())
elif info_type=='data':
result = chart_obj.data()
return HttpResponse(json.dumps(result), content_type="application/json")
else:
# the 'info_type' indicates the method to call on the object
the_method = getattr(chart_obj, info_type)
# give this method an empty response object to fill out and return
return the_method(HttpResponse())
except:
_logger.exception('Error in chart_info')
return HttpResponse('Error in chart_info')
def map(request):
'''
The map page.
'''
return render_to_response('bmsapp/map.html', {})
def training(request):
'''
The training page.
'''
return render_to_response('bmsapp/training.html', {}) #, {'building_list': bldgs})
def show_video(request, filename, width, height):
'''
A Page to show a training video. 'filename' is the Flash file name of the video, without
the 'swf' extension, 'width' and 'height' are the width and height in pixels of the viewport.
'''
return render_to_response('bmsapp/video.html', {'filename': filename, 'width': width, 'height': height})
@login_required
def password_change_done(request):
return render_to_response('registration/password_change_done.html',
{'user': request.user})
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