class Publisher: def __init__(self, config_settings): # Obtains config file settings and starts Subscribers self.settings = config_settings # Initial Parameters self.bat_num = 0 self.grid_num = 0 self.bat_power = 0 self.grid = 0 self.sol_grid = 0 self.initial_time = 0 self.day_count = 0 self.savings = 0 self.sol_savings = 0 self.house_import = 0 # Non-Optimiser Control Settings self.grid_ref = self.settings.simulation["grid_ref"] self.grid_control_dir = self.settings.simulation["control_dir"] # Sets up Kalman Filters self.bat_cov = self.settings.control["bat_cov"] self.battery_filter = KalmanFilter(1, 0, 1, 0, 1, self.bat_cov, 1) # Creates Internal Data Store self.data_store = dict() self.data_store["bat_power"] = list() self.data_store["bat_plot"] = list() self.data_store["bat_time"] = list() self.data_store["grid_power"] = list() self.data_store["grid_plot"] = list() self.data_store["grid_time"] = list() # ZeroMQ Publishing pub_context = zmq.Context() self.pub_socket = pub_context.socket(zmq.PUB) self.pub_socket.bind("tcp://*:%s" % str(self.settings.ZeroMQ["battery_power_port"])) def set_power(self, bat_power): self.bat_power = bat_power def set_grid(self, solar, house): self.grid = self.bat_power + solar + house self.sol_grid = solar + house def update_data_store(self, device, power, house_time): # Power Value self.data_store[device + "_power"].append(power / 1000) # Time Value if self.settings.simulation["use_real_time"]: curr_time = (round(time.time() - self.initial_time, 2) / 3600) - (24 * self.day_count) else: curr_time = house_time self.data_store[device + "_time"].append(curr_time) # Plot Value if 24 - self.data_store[device + "_time"][-1] < 0.1: self.data_store[device + "_plot"].append(np.nan) else: self.data_store[device + "_plot"].append(power / 1000) # Increase total counters if device == "bat": self.bat_num += 1 else: self.grid_num += 1 def non_optimiser_control(self, soc): # Above, Below and Both Control above_control = self.grid_control_dir == "Above" and self.grid > self.grid_ref below_control = self.grid_control_dir == "Below" and self.grid < self.grid_ref # Sets new Battery Power if above_control or below_control or self.grid_control_dir == "Both": self.set_power(-self.grid + self.bat_power + self.grid_ref) # Accounting for SOC if (soc == 0 and self.bat_power < 0) or (soc == 100 and self.bat_power > 0): self.set_power(0) # Battery Power Filtering if self.settings.control["battery_filtering"]: self.battery_filter.step(0, self.bat_power) self.set_power(self.battery_filter.current_state()) def publish_power(self): self.pub_socket.send_string("%d %d" % (self.settings.ZeroMQ["battery_power_topic"], self.bat_power))
class ControlSystem: def __init__(self, config_settings): # Initialises Classes self.settings = config_settings self.sub = Subscriber(config_settings) self.pub = Publisher(config_settings) self.optimiser = Optimiser(config_settings) if self.settings.simulation["use_visualisation"]: self.plot = DataVisualisation(config_settings) # Sets Boolean Parameters self.control_mod = False self.opt_mod = False self.data_mod = False self.initial_connect = False self.connected = False # Total Energy self.solar_energy = 0 self.house_energy = 0 # Sets internal parameters self.mod_thresh = 0.002 self.optimiser_index = 0 self.data_skip = 0 self.covariance = 0.3 self.power_cov = 0.5 self.power_scale = self.settings.control["power_covariance"] self.prev_house_data = None self.prev_house_control = None self.prev_house_opt = None self.prev_house_day = None self.time_step = self.settings.control["data_time_step"] self.control_step = self.settings.control["control_time_step"] self.opt_step = self.settings.control["optimiser_time_step"] # Creates 24 hour data stores and filters self.power = None self.load = list(self.optimiser.load) self.pv = list(self.optimiser.pv) self.import_tariff = list(self.optimiser.import_tariff.values()) self.export_tariff = list(self.optimiser.export_tariff.values()) self.load_filter = None self.pv_filter = None self.power_filter = KalmanFilter(1, 0, 1, 0, 1, self.power_cov, 1) def current_time(self): # Obtains the current time if self.settings.simulation["use_real_time"]: curr_time = (round(time.time() - self.sub.initial_time, 2) / 3600) - (24 * self.sub.day_count) elif bool(self.sub.data_store["house_time"]) is False: curr_time = 0 else: curr_time = self.sub.data_store["house_time"][-1] return curr_time def update_day_counter(self): # Obtains the current time curr_time = self.current_time() # Obtains current house time if bool(self.sub.data_store["house_time"]) is False: curr_house_time = 0 else: curr_house_time = self.sub.data_store["house_time"][-1] if 1 < curr_time < 23: self.prev_house_day = None # Checks if it has been 24 hours if abs(curr_time - 24) < 0.1 and curr_house_time != self.prev_house_day: self.sub.day_count += 1 self.pub.day_count += 1 if self.settings.simulation["use_visualisation"]: self.plot.day_count += 1 self.prev_house_day = curr_house_time def calculate_savings(self): # Energy Totals self.solar_energy += (self.sub.solar_power / 1000) * (self.time_step / 60) self.house_energy += (self.sub.house_power / 1000) * (self.time_step / 60) # Total Savings if self.pub.grid < 0: self.pub.savings += (self.pub.grid / 1000) * ( self.time_step / 60) * self.export_tariff[0] else: self.pub.savings += (self.pub.grid / 1000) * ( self.time_step / 60) * self.import_tariff[0] # Battery Exclusive Savings if self.pub.sol_grid < 0: self.pub.sol_savings += (self.pub.sol_grid / 1000) * ( self.time_step / 60) * self.export_tariff[0] else: self.pub.sol_savings += (self.pub.sol_grid / 1000) * ( self.time_step / 60) * self.import_tariff[0] # No PV System Cost self.pub.house_import += (self.sub.house_power / 1000) * ( self.time_step / 60) * self.import_tariff[0] def update_24_data(self, data_skip): # Applies filters and removes last entries (solar and load) if len(self.load) == (60 / self.time_step) * 24: for i in reversed(range(0, data_skip + 1)): # Create Filters self.load_filter = KalmanFilter(1, 0, 1, self.load[0], 1, self.covariance, 1) self.pv_filter = KalmanFilter(1, 0, 1, self.pv[0], 1, self.covariance, 1) # Apply Filters self.load_filter.step( 0, self.sub.data_store["house_value"][-(i + 1)] / (60 / self.time_step)) self.pv_filter.step( 0, self.sub.data_store["solar_value"][-(i + 1)] / (60 / self.time_step)) # Remove First Value self.load.pop(0) self.pv.pop(0) # Append new values self.load.append(self.load_filter.current_state()) self.pv.append(self.pv_filter.current_state()) else: # Append new values self.load.append(self.sub.house_power / (1000 * (60 / self.time_step))) self.pv.append(self.sub.solar_power / (1000 * (60 / self.time_step))) # Update Tariffs for i in range(0, data_skip + 1): self.import_tariff.append(self.import_tariff.pop(0)) self.export_tariff.append(self.export_tariff.pop(0)) # Update profile classes and energy system self.optimiser.update_profiles(np.array(self.load), np.array(self.pv), np.array(self.import_tariff), np.array(self.export_tariff), self.sub.bat_SOC) self.optimiser.update_energy_system() def connection_loop(self): while self.connected is False: # Checks for Initial Connection Values bat_connect = self.sub.bat_SOC == b'bat_connect' solar_connect = self.sub.solar_power == b'solar_connect' house_connect = self.sub.house_power == b'house_connect' connecting = bat_connect and solar_connect and house_connect residual_connect = bat_connect or solar_connect or house_connect # Runs if Initial Connection is Established if connecting: self.pub.pub_socket.send_string( "%d %s" % (self.settings.ZeroMQ["battery_power_topic"], 'connected')) self.initial_connect = True # Runs if all Subscribers start returning intended values if self.initial_connect and residual_connect is False: self.connected = True self.sub.initial_time = round(time.time(), 2) self.pub.initial_time = self.sub.initial_time if self.settings.simulation["use_visualisation"]: self.plot.initial_time = self.sub.initial_time def main_loop(self): # Updates day counter self.update_day_counter() # Checks if all subscribers have new values all_read = self.sub.battery_read == 1 and self.sub.solar_read == 1 and self.sub.house_read == 1 # Obtains the current time curr_time = self.current_time() # True every time step curr_data_mod = curr_time % (self.time_step / 60) self.data_mod = curr_data_mod < self.mod_thresh or ( self.time_step / 60) - curr_data_mod < self.mod_thresh # True every control time step curr_control_mod = curr_time % (self.control_step / 60) self.control_mod = curr_control_mod < self.mod_thresh or ( self.control_step / 60) - curr_control_mod < self.mod_thresh # True on every optimiser time step curr_opt_mod = curr_time % (self.opt_step / 60) self.opt_mod = curr_opt_mod < self.mod_thresh or ( self.opt_step / 60) - curr_opt_mod < self.mod_thresh if all_read: # Applies Control if necessary self.apply_control() # Resets Subscriber Read Values self.sub.battery_read = 0 self.sub.solar_read = 0 self.sub.house_read = 0 def apply_control(self): # Obtains current house time if bool(self.sub.data_store["house_time"]) is False: curr_time = 0 else: curr_time = self.sub.data_store["house_time"][-1] # Calculates new grid value self.pub.set_grid(self.sub.solar_power, self.sub.house_power) # PV Self Consumption Control if self.settings.control["pv_self_cons"]: if self.control_mod and curr_time != self.prev_house_control: # Applies Control self.pub.non_optimiser_control(self.sub.bat_SOC) self.prev_house_control = curr_time # Optimiser Control if self.settings.control["optimiser"] and len( self.load) == (60 / self.time_step) * 24: if self.opt_mod and curr_time != self.prev_house_opt: # Applies Control self.data_skip = self.sub.house_num self.optimiser.optimise() self.power = list(self.optimiser.return_battery_power()) self.data_skip = self.sub.house_num - self.data_skip self.optimiser_index = 0 + self.data_skip self.prev_house_opt = curr_time # Data Time Step if self.data_mod and curr_time != self.prev_house_data: # Print Outputs print('Optimiser skipped ' + str(self.data_skip) + ' time steps') print('Total PV system money saved = $' + str(round(self.pub.house_import - self.pub.savings, 2))) print('Additional money saved by battery = $' + str(round(self.pub.sol_savings - self.pub.savings, 2))) print('Total cost of load import = $' + str(round(self.pub.house_import, 2))) print('Total load energy = ' + str(round(self.house_energy, 2)) + ' kWh') print('Total solar energy = ' + str(round(self.solar_energy, 2)) + ' kWh') print('Day counter = ' + str(self.sub.day_count)) # Sets new power value if self.settings.control["pv_self_cons"] and self.settings.control[ "optimiser"]: if bool(self.power): opt_power = self.power[self.optimiser_index] * 1000 * ( 60 / self.time_step) opt_power = self.power_scale * opt_power + ( 1 - self.power_scale) * self.pub.bat_power self.power_filter.step(0, opt_power) self.pub.set_power(self.power_filter.current_state()) else: self.pub.set_power(0) if self.settings.control["optimiser"] and self.settings.control[ "pv_self_cons"] is False: if bool(self.power): opt_power = self.power[self.optimiser_index] * 1000 * ( 60 / self.time_step) self.pub.set_power(opt_power) else: self.pub.set_power(0) # Updates optimiser index and 24 hour data if self.settings.control["optimiser"]: self.optimiser_index += 1 self.update_24_data(self.data_skip) self.prev_house_data = curr_time # Updates data stores and optimiser skips self.pub.update_data_store("grid", self.pub.grid, self.current_time()) self.pub.update_data_store("bat", self.pub.bat_power, self.current_time()) self.data_skip = 0 # Calculates Savings self.calculate_savings() # Publishes power self.pub.publish_power()
class Subscriber: def __init__(self, config_settings): # Obtains settings from config file self.settings = config_settings # Parameters and Initial Values self.battery_read = 0 self.solar_read = 0 self.house_read = 0 self.bat_SOC = self.settings.battery["initial_SOC"] self.solar_power = 0 self.house_power = 0 self.day_count = 0 self.soc_num = 0 self.solar_num = 0 self.house_num = 0 self.initial_time = 0 # Creates Thread Lock self.lock = threading.Lock() # Erases Previous Text File Contents open("control_power_values.txt", "w+").close() # Creates Internal Data Store self.data_store = dict() self.data_store["soc_time"] = list() self.data_store["soc_value"] = list() self.data_store["soc_plot"] = list() self.data_store["solar_time"] = list() self.data_store["solar_value"] = list() self.data_store["solar_plot"] = list() self.data_store["house_time"] = list() self.data_store["house_value"] = list() self.data_store["house_plot"] = list() # Sets up Kalman Filters self.solar_cov = self.settings.control["solar_cov"] self.solar_filter = KalmanFilter(1, 0, 1, 0, 1, self.solar_cov, 1) self.house_cov = self.settings.control["house_cov"] self.house_filter = KalmanFilter(1, 0, 1, 700, 1, self.house_cov, 1) # Defines Sockets and Threads self.bat_socket = None self.solar_socket = None self.house_socket = None self.bat_thread = None self.solar_thread = None self.house_thread = None # Starts Subscribers self.start_subscribers() def write_to_text(self, device, curr_time, value): self.lock.acquire() file = open("control_power_values.txt", "a+") file.write("\n"+device+" "+str(curr_time)+" "+str(value)) file.close() self.lock.release() def update_data_store(self, device, value): # New Value if device == "soc": self.data_store[device + "_value"].append(value) else: self.data_store[device + "_value"].append(value / 1000) # Time Value if self.settings.simulation["use_real_time"]: curr_time = (round(time.time() - self.initial_time, 2) / 3600) - (24 * self.day_count) elif bool(self.data_store[device + "_time"]) is False: curr_time = 0 else: curr_time = self.data_store[device + "_time"][-1] + self.settings.simulation["time_step"] / 60 if abs(curr_time - 24) < 0.02: curr_time = 0 self.data_store[device + "_time"].append(curr_time) # Plot Value if 24 - self.data_store[device + "_time"][-1] <= 0.1: self.data_store[device + "_plot"].append(np.nan) else: if device == "soc": self.data_store[device + "_plot"].append(value / (100 / 6)) else: self.data_store[device + "_plot"].append(value / 1000) def start_subscribers(self): # Connects Sockets sub_context = zmq.Context() self.bat_socket = sub_context.socket(zmq.SUB) self.bat_socket.connect("tcp://localhost:%s" % self.settings.ZeroMQ["battery_SOC_port"]) self.bat_socket.setsockopt_string(zmq.SUBSCRIBE, str(self.settings.ZeroMQ["battery_SOC_topic"])) self.solar_socket = sub_context.socket(zmq.SUB) self.solar_socket.connect("tcp://localhost:%s" % self.settings.ZeroMQ["solar_port"]) self.solar_socket.setsockopt_string(zmq.SUBSCRIBE, str(self.settings.ZeroMQ["solar_topic"])) self.house_socket = sub_context.socket(zmq.SUB) self.house_socket.connect("tcp://localhost:%s" % self.settings.ZeroMQ["house_port"]) self.house_socket.setsockopt_string(zmq.SUBSCRIBE, str(self.settings.ZeroMQ["house_topic"])) # Starts Battery Sub Thread print('starting battery SOC subscriber') self.bat_thread = threading.Thread(target=self.battery_subscriber) self.bat_thread.start() # Starts Solar Sub Thread print('starting solar subscriber') self.solar_thread = threading.Thread(target=self.solar_subscriber) self.solar_thread.start() # Starts House Sub Thread print('starting house subscriber') self.house_thread = threading.Thread(target=self.house_subscriber) self.house_thread.start() def battery_subscriber(self): while True: # Obtains Value from Topic bat_string = self.bat_socket.recv() b_topic, self.bat_SOC = bat_string.split() # Runs if not connecting if self.bat_SOC != b'bat_connect': # Converts to int self.bat_SOC = int(self.bat_SOC) # Updates Text File and Data Store curr_time = (round(time.time() - self.initial_time, 2) / 3600) - (24 * self.day_count) self.write_to_text("SOC", curr_time, self.bat_SOC) self.update_data_store("soc", self.bat_SOC) # Sets Read and Increases Counter self.soc_num += 1 self.battery_read = 1 def solar_subscriber(self): while True: # Obtains Value from Topic solar_string = self.solar_socket.recv() s_topic, self.solar_power = solar_string.split() # Runs if not connecting if self.solar_power != b'solar_connect': # Converts to int self.solar_power = int(self.solar_power) # Applies Filtering if self.settings.control["solar_filtering"]: self.solar_filter.step(0, self.solar_power) self.solar_power = self.solar_filter.current_state() # Updates Text File and Data Store curr_time = (round(time.time() - self.initial_time, 2) / 3600) - (24 * self.day_count) self.write_to_text("solar", curr_time, self.solar_power) self.update_data_store("solar", self.solar_power) # Sets Read and Increases Counter self.solar_num += 1 self.solar_read = 1 def house_subscriber(self): while True: # Obtains Value from Topic house_string = self.house_socket.recv() h_topic, self.house_power = house_string.split() # Runs if not connecting if self.house_power != b'house_connect': # Converts to int self.house_power = int(self.house_power) # Applies Filtering if self.settings.control["house_filtering"]: self.house_filter.step(0, self.house_power) self.house_power = self.house_filter.current_state() # Updates Text File and Data Store curr_time = (round(time.time() - self.initial_time, 2) / 3600) - (24 * self.day_count) self.write_to_text("house", curr_time, self.house_power) self.update_data_store("house", self.house_power) # Sets Read and Increases Counter self.house_num += 1 self.house_read = 1