Building Energy Boot Camp 2018 - Day 7
Today began with a TEDx talk. It was focused on climate change, and what you, as an individual, can do to help stop it. Afterwards, we continued to work on our projects for the rest of the day. As I am no longer away and have access to the school’s internet, that meant that I had to start working on using the buildingEnergyAPI to get live data from the various sensors, and do my calculations on that data. You can see my code for the day below:
# -*- coding: utf-8 -*-
from tkinter import *
import pandas as pd
import os
import threading
import time
pd.options.mode.chained_assignment = None # Stop chained assignment warnings - I know what I'm doing
SAVED_DATA_PATH = os.path.join('CSVs', 'ahs_air_data.csv')
HOSTNAME = '10.12.4.98'
PORT = '8000'
air_values = None
background_data_update = False
def save_data():
if air_values is not None:
air_values.to_csv(SAVED_DATA_PATH)
def stop():
import sys
save_data()
sys.exit()
def get_air_values_df(hostname, port, selected_floor, selected_wing, background_updater):
df_dictionary = {
'Date / Time': [],
'Room': [],
'Temperature': [],
'Temperature Units': [],
'CO2 Level': [],
'CO2 Units': [],
'Floor': [],
'Wing': []
}
try:
import argparse
from bacnet_gateway_requests import get_value_and_units
import datetime as dt
# Read spreadsheet into a DataFrame.
# Each row contains the following:
# - Location
# - Instance ID of CO2 sensor
# - Instance ID of temperature sensor
df = pd.read_csv(os.path.join('CSVs', 'ahs_air.csv'), na_filter=False, comment='#')
matching_floor = df['Floor'] == str(selected_floor)
matching_wing = df['Wing'] == selected_wing
filtered_rooms = df[matching_floor & matching_wing]
# Iterate over the rows of the DataFrame, getting temperature and CO2 values for each location
for row_index, row in filtered_rooms.iterrows():
while True:
if background_updater and not background_data_update:
time.sleep(5)
continue
else:
# Retrieve data
temp_value, temp_units = get_value_and_units(row['Facility'], row['Temperature'], hostname, port)
co2_value, co2_units = get_value_and_units(row['Facility'], row['CO2'], hostname, port)
# Prepare to print
temp_value = int(temp_value) if temp_value else ''
temp_units = temp_units.replace('deg ', '°') if temp_units else ''
co2_value = int(co2_value) if co2_value else ''
co2_units = co2_units if co2_units else ''
# Update dictionary
df_dictionary['Date / Time'].append(dt.datetime.now().strftime("%m/%d/%Y %H:%M:%S"))
df_dictionary['Room'].append(row['Label'])
df_dictionary['Temperature'].append(temp_value)
df_dictionary['Temperature Units'].append(temp_units)
df_dictionary['CO2 Level'].append(co2_value)
df_dictionary['CO2 Units'].append(co2_units)
df_dictionary['Floor'].append(row['Floor'])
df_dictionary['Wing'].append(row['Wing'])
break
return pd.DataFrame.from_dict(df_dictionary)
except KeyboardInterrupt:
stop()
def update_loaded_data(updated_df):
global air_values
air_values = updated_df
class BACnetThread(object):
"""
The run() method will be started and it will run in the background
until the application exits.
"""
def __init__(self, interval=10):
""" Constructor
:type interval: int
:param interval: Check interval, in seconds
"""
self.interval = interval
self.used_combos = None
self.updated_values = None
thread = threading.Thread(target=self.run, args=())
thread.daemon = True # Daemonize thread
thread.start() # Start the execution
def run(self):
""" Method that runs forever """
while True:
# Updates the already-requested rooms
if air_values is not None:
# Find which floor-wing combinations have been used so far
self.used_combos = air_values.groupby(
['Wing', 'Floor']).size().reset_index()
for row_index, row in self.used_combos.iterrows():
if row['Floor'] == '' and row['Wing'] == '':
self.used_combos.drop(row_index)
continue
df = get_air_values_df(HOSTNAME, PORT, row['Floor'], row['Wing'], True)
self.updated_values = pd.concat([self.updated_values, df],
ignore_index=True) if self.updated_values is not None else df
update_loaded_data(self.updated_values)
self.updated_values = None
time.sleep(self.interval)
# TODO: def request_data(self):
thread = BACnetThread()
def update_labels(avg_measure, max_measure, max_measure_room, unit, data_timestamp):
row_labels[0].config(
text="Data last updated at: {0}/{1}/{2} {3}:{4}".format(data_timestamp.month, data_timestamp.day,
data_timestamp.year, data_timestamp.hour,
"%02d" % data_timestamp.minute))
row_labels[1].config(text=(str(round(avg_measure, 2)) + ' ' + unit))
row_labels[2].config(text=(str(round(max_measure, 2)) + ' ' + unit))
row_labels[3].config(text=str(max_measure_room))
def fill_fields(selected_floor, selected_wing, selected_measurement):
enough_info = False
measurement_column = 'CO2 Level' if selected_measurement == 0 else 'Temperature'
unit_column = 'CO2 Units' if selected_measurement == 0 else 'Temperature Units'
if len(air_values[(air_values['Floor'] == selected_floor) & (air_values['Wing'] == selected_wing)]) >= 1 and len(
air_values[measurement_column]) != 0:
# The session cache has non-empty data for the wing
enough_info = True
# TODO: Check if the output file from the last session has data
# TODO: Fallback to an emergency file
if enough_info:
matching_floor = air_values['Floor'] == selected_floor
matching_wing = air_values['Wing'] == selected_wing
filtered_rooms = air_values[matching_floor & matching_wing]
unit = filtered_rooms[unit_column].iloc[len(filtered_rooms[unit_column]) - 1] # All are the same units
avg_measure = filtered_rooms[measurement_column].mean()
max_measure = 0
max_measure_room = 'None'
for row_index, row in filtered_rooms.iterrows():
if row[measurement_column] > max_measure:
max_measure = row[measurement_column]
max_measure_room = row['Room']
filtered_rooms['Date / Time'] = pd.to_datetime(filtered_rooms['Date / Time'], format='%m/%d/%Y %H:%M',
utc=True) + pd.Timedelta('5:00:00')
import time
utc_difference_in_hours = time.timezone / 3600.0 # Returns a value like 4.5 (hours)
utc_split = str(utc_difference_in_hours).split('.') # Turns 4.5 into ['4', '5']
decimal = int(utc_split[1]) / pow(10, len(utc_split[1])) # Turns the '5' into 0.5
tz_difference_string = "{0}:{1}:00".format(utc_split[0], "%02d" % (round(decimal * 60))) # Build the string
filtered_rooms['Date / Time'] -= pd.Timedelta(tz_difference_string)
data_timestamp = filtered_rooms['Date / Time'].get(0)
update_labels(avg_measure, max_measure, max_measure_room, unit, data_timestamp)
root = Tk()
root.title("AHS Air Data")
root.configure(background='white')
root.resizable(False, False)
wing = StringVar(root, value='A') # The selected wing
floor = IntVar(root, value=1) # The selected floor
measurement = IntVar(root, value=1) # The selected measurement
def set_wing():
fill_fields(floor.get(), str(wing.get()), measurement.get())
def set_floor():
fill_fields(floor.get(), str(wing.get()), measurement.get())
def set_measurement():
fill_fields(floor.get(), str(wing.get()), measurement.get())
# Setup table layout
COLUMN_TITLES = ['Floor', 'Wing', 'Measurement', 'Average', 'Maximum', 'Room Number With Maximum']
col_number = 0
row_labels = []
for col in COLUMN_TITLES:
label = Label(text=col, fg="Blue", bg="White", width="30")
label.grid(row=0, column=col_number, pady=(10, 0), sticky='we', ipady="2")
# Add floor options
if col_number == 0:
FLOOR_NAMES = ['1st', '2nd', '3rd']
current_row = 1
for floor_name in FLOOR_NAMES:
Radiobutton(text=floor_name, fg="Black", bg="White", variable=floor, value=int(floor_name[0]),
command=set_floor).grid(row=current_row, column=col_number, sticky='we')
current_row += 1
# Add wing options
elif col_number == 1:
WING_LETTERS = ['A', 'B', 'C', 'D']
current_row = 1
for index, wing_letter in enumerate(WING_LETTERS):
if index == len(WING_LETTERS) - 1:
Radiobutton(text=wing_letter, fg="Black", bg="White", variable=wing, value=wing_letter,
command=set_wing).grid(row=current_row, column=col_number, sticky='we',
pady=(0, 10))
else:
Radiobutton(text=wing_letter, fg="Black", bg="White", variable=wing, value=wing_letter,
command=set_wing).grid(row=current_row, column=col_number, sticky='we')
current_row += 1
row_label = Label(bg="White", fg="Blue", relief=FLAT, text="Data last updated at: 1/1/1970 00:00")
row_labels.append(row_label)
row_label.grid(row=current_row, column=0, columnspan=len(COLUMN_TITLES), sticky='we', ipady="2", padx=10,
pady=(0, 20))
# Add measurement options
elif col_number == 2:
MEASUREMENTS = ['CO2', 'Temperature']
current_row = 1
for index, measure in enumerate(MEASUREMENTS):
Radiobutton(text=measure, fg="Black", bg="White", variable=measurement, value=index,
command=set_measurement).grid(row=current_row, column=col_number, sticky='we')
current_row += 1
else:
# Create empty cell for value
row_label = Label(bg="White", fg="Black", relief=RIDGE, width="30")
row_labels.append(row_label)
row_label.grid(row=1, column=col_number, sticky='we', ipady="2", padx=5)
col_number += 1
root.grid_columnconfigure(0, weight=1)
fill_fields(floor.get(), str(wing.get()), measurement.get())
try:
root.mainloop()
except KeyboardInterrupt:
stop()
A major issue that I found was that getting all the data for every room at once was slow. In fact, getting data for any individual wing would take at least 10 seconds! And I realized that having to wait 10 seconds before the application even opens makes the application impractical. So, I began learning threading, and started with the updating thread BACnetThread
class. I shall explain its logic further on Day 9, but for now, all you really need to know is that it checks which wing-floor combinations have been loaded, and updates them one by one.