Commit 15f381c2 authored by Alan Mitchell's avatar Alan Mitchell

Added *.bak to .gitignore and Removed some .bak files

parent adc6fb28
......@@ -5,3 +5,5 @@ settings.py
*.pdf
*.swf
*.gz
*.bak
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 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)
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')
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