def run_main(): logger.info("... Starting Raspberry Pi Power Monitor") logger.info("Press Ctrl-c to quit...") # The following empty dictionaries will hold the respective calculated values at the end of each polling cycle, which are then averaged prior to storing the value to the DB. solar_power_values = dict(power=[], pf=[], current=[]) home_load_values = dict(power=[], pf=[], current=[]) net_power_values = dict(power=[], current=[]) ct0_dict = dict(power=[], pf=[], current=[]) ct1_dict = dict(power=[], pf=[], current=[]) ct2_dict = dict(power=[], pf=[], current=[]) ct3_dict = dict(power=[], pf=[], current=[]) ct4_dict = dict(power=[], pf=[], current=[]) ct5_dict = dict(power=[], pf=[], current=[]) rms_voltages = [] i = 0 # Counter for aggregate function while True: try: board_voltage = get_board_voltage() samples = collect_data(2000) poll_time = samples['time'] ct0_samples = samples['ct0'] ct1_samples = samples['ct1'] ct2_samples = samples['ct2'] ct3_samples = samples['ct3'] ct4_samples = samples['ct4'] ct5_samples = samples['ct5'] v_samples = samples['voltage'] rebuilt_waves = rebuild_waves(samples, ct0_phasecal, ct1_phasecal, ct2_phasecal, ct3_phasecal, ct4_phasecal, ct5_phasecal) results = calculate_power(rebuilt_waves, board_voltage) # # RMS calculation for phase correction only - this is not needed after everything is tuned. The following code is used to compare the RMS power to the calculated real power. # # Ideally, you want the RMS power to equal the real power when you are measuring a purely resistive load. # rms_power_0 = round(results['ct0']['current'] * results['ct0']['voltage'], 2) # AKA apparent power # rms_power_1 = round(results['ct1']['current'] * results['ct1']['voltage'], 2) # AKA apparent power # rms_power_2 = round(results['ct2']['current'] * results['ct2']['voltage'], 2) # AKA apparent power # rms_power_3 = round(results['ct3']['current'] * results['ct3']['voltage'], 2) # AKA apparent power # rms_power_4 = round(results['ct4']['current'] * results['ct4']['voltage'], 2) # AKA apparent power # rms_power_5 = round(results['ct5']['current'] * results['ct5']['voltage'], 2) # AKA apparent power # Prepare values for database storage grid_0_power = results['ct0']['power'] # CT0 Real Power grid_1_power = results['ct1']['power'] # CT1 Real Power grid_2_power = results['ct2']['power'] # CT2 Real Power grid_3_power = results['ct3']['power'] # CT3 Real Power grid_4_power = results['ct4']['power'] # CT4 Real Power grid_5_power = results['ct5']['power'] # CT5 Real Power grid_0_current = results['ct0']['current'] # CT0 Current grid_1_current = results['ct1']['current'] # CT1 Current grid_2_current = results['ct2']['current'] # CT2 Current grid_3_current = results['ct3']['current'] # CT3 Current grid_4_current = results['ct4']['current'] # CT4 Current grid_5_current = results['ct5']['current'] # CT5 Current # If you are monitoring solar/generator inputs to your panel, specify which CT number(s) you are using, and uncomment the commented lines. solar_power = 0 solar_current = 0 solar_pf = 0 # solar_power = results['ct3']['power'] # solar_current = results['ct3']['current'] # solar_pf = results['ct3']['pf'] voltage = results['voltage'] # Set solar power and current to zero if the solar power is under 20W. if solar_power < 20: solar_power = 0 solar_current = 0 solar_pf = 0 # Determine if the system is net producing or net consuming right now by looking at the two panel mains. # Since the current measured is always positive, we need to add a negative sign to the amperage value if we're exporting power. if grid_0_power < 0: grid_0_current = grid_0_current * -1 if grid_1_power < 0: grid_1_current = grid_1_current * -1 if solar_power > 0: solar_current = solar_current * -1 # Unless your specific panel setup matches mine exactly, the following four lines will likely need to be re-written: home_consumption_power = grid_0_power + grid_1_power + grid_2_power + grid_3_power + grid_4_power + grid_5_power + solar_power net_power = home_consumption_power - solar_power home_consumption_current = grid_0_current + grid_1_current + grid_2_current + grid_3_current + grid_4_current + grid_5_current - solar_current net_current = grid_0_current + grid_1_current + grid_2_current + grid_3_current + grid_4_current + grid_5_current + solar_current if net_power < 0: current_status = "Producing" else: current_status = "Consuming" # Average 2 readings before sending to db if i < 2: solar_power_values['power'].append(solar_power) solar_power_values['current'].append(solar_current) solar_power_values['pf'].append(solar_pf) home_load_values['power'].append(home_consumption_power) home_load_values['current'].append(home_consumption_current) net_power_values['power'].append(net_power) net_power_values['current'].append(net_current) ct0_dict['power'].append(results['ct0']['power']) ct0_dict['current'].append(results['ct0']['current']) ct0_dict['pf'].append(results['ct0']['pf']) ct1_dict['power'].append(results['ct1']['power']) ct1_dict['current'].append(results['ct1']['current']) ct1_dict['pf'].append(results['ct1']['pf']) ct2_dict['power'].append(results['ct2']['power']) ct2_dict['current'].append(results['ct2']['current']) ct2_dict['pf'].append(results['ct2']['pf']) ct3_dict['power'].append(results['ct3']['power']) ct3_dict['current'].append(results['ct3']['current']) ct3_dict['pf'].append(results['ct3']['pf']) ct4_dict['power'].append(results['ct4']['power']) ct4_dict['current'].append(results['ct4']['current']) ct4_dict['pf'].append(results['ct4']['pf']) ct5_dict['power'].append(results['ct5']['power']) ct5_dict['current'].append(results['ct5']['current']) ct5_dict['pf'].append(results['ct5']['pf']) rms_voltages.append(voltage) i += 1 else: # Calculate the average, send the result to InfluxDB, and reset the dictionaries for the next 2 sets of data. infl.write_to_influx( solar_power_values, home_load_values, net_power_values, ct0_dict, ct1_dict, ct2_dict, ct3_dict, ct4_dict, ct5_dict, poll_time, i, rms_voltages, ) solar_power_values = dict(power=[], pf=[], current=[]) home_load_values = dict(power=[], pf=[], current=[]) net_power_values = dict(power=[], current=[]) ct0_dict = dict(power=[], pf=[], current=[]) ct1_dict = dict(power=[], pf=[], current=[]) ct2_dict = dict(power=[], pf=[], current=[]) ct3_dict = dict(power=[], pf=[], current=[]) ct4_dict = dict(power=[], pf=[], current=[]) ct5_dict = dict(power=[], pf=[], current=[]) rms_voltages = [] i = 0 if logger.handlers[0].level == 10: t = PrettyTable( ['', 'CT0', 'CT1', 'CT2', 'CT3', 'CT4', 'CT5']) t.add_row([ 'Watts', round(results['ct0']['power'], 3), round(results['ct1']['power'], 3), round(results['ct2']['power'], 3), round(results['ct3']['power'], 3), round(results['ct4']['power'], 3), round(results['ct5']['power'], 3) ]) t.add_row([ 'Current', round(results['ct0']['current'], 3), round(results['ct1']['current'], 3), round(results['ct2']['current'], 3), round(results['ct3']['current'], 3), round(results['ct4']['current'], 3), round(results['ct5']['current'], 3) ]) t.add_row([ 'P.F.', round(results['ct0']['pf'], 3), round(results['ct1']['pf'], 3), round(results['ct2']['pf'], 3), round(results['ct3']['pf'], 3), round(results['ct4']['pf'], 3), round(results['ct5']['pf'], 3) ]) t.add_row([ 'Voltage', round(results['voltage'], 3), '', '', '', '', '' ]) s = t.get_string() logger.debug('\n' + s) #sleep(0.1) except KeyboardInterrupt: infl.close_db() sys.exit()
def run_main(): logger.info("Press Ctrl-c to quit...") # The following empty dictionaries will hold the respective calculated values at the end of each polling cycle, which are then averaged prior to storing the value to the DB. solar_power_values = dict(power=[], pf=[], current=[]) home_load_values = dict(power=[], pf=[], current=[]) net_power_values = dict(power=[], current=[]) ct0_dict = dict(power=[], pf=[], current=[]) ct1_dict = dict(power=[], pf=[], current=[]) ct2_dict = dict(power=[], pf=[], current=[]) ct3_dict = dict(power=[], pf=[], current=[]) ct4_dict = dict(power=[], pf=[], current=[]) ct5_dict = dict(power=[], pf=[], current=[]) rms_voltage_values = [] i = 0 # Counter for aggregate function while True: try: board_voltage = get_board_voltage() samples = collect_data(2000) poll_time = samples['time'] ct0_samples = samples['ct0'] ct1_samples = samples['ct1'] ct2_samples = samples['ct2'] ct3_samples = samples['ct3'] ct4_samples = samples['ct4'] ct5_samples = samples['ct5'] v_samples = samples['voltage'] rebuilt_waves = rebuild_waves(samples, ct0_phasecal, ct1_phasecal, ct2_phasecal, ct3_phasecal, ct4_phasecal, ct5_phasecal) results = calculate_power(rebuilt_waves, board_voltage) # # RMS calculation for phase correction only - this is not needed after everything is tuned. The following code is used to compare the RMS power to the calculated real power. # # Ideally, you want the RMS power to equal the real power when you are measuring a purely resistive load. # rms_power_0 = round(results['ct0']['current'] * results['ct0']['voltage'], 2) # AKA apparent power # rms_power_1 = round(results['ct1']['current'] * results['ct1']['voltage'], 2) # AKA apparent power # rms_power_2 = round(results['ct2']['current'] * results['ct2']['voltage'], 2) # AKA apparent power # rms_power_3 = round(results['ct3']['current'] * results['ct3']['voltage'], 2) # AKA apparent power # rms_power_4 = round(results['ct4']['current'] * results['ct4']['voltage'], 2) # AKA apparent power # rms_power_5 = round(results['ct5']['current'] * results['ct5']['voltage'], 2) # AKA apparent power # phase_corrected_power_0 = results['ct0']['power'] # phase_corrected_power_1 = results['ct1']['power'] # phase_corrected_power_2 = results['ct2']['power'] # phase_corrected_power_3 = results['ct3']['power'] # phase_corrected_power_4 = results['ct4']['power'] # phase_corrected_power_5 = results['ct5']['power'] # # diff is the difference between the real_power (phase corrected) compared to the simple rms power calculation. # # This is used to calibrate for the "unknown" phase error in each CT. The phasecal value for each CT input should be adjusted so that diff comes as close to zero as possible. # diff_0 = phase_corrected_power_0 - rms_power_0 # diff_1 = phase_corrected_power_1 - rms_power_1 # diff_2 = phase_corrected_power_2 - rms_power_2 # diff_3 = phase_corrected_power_3 - rms_power_3 # diff_4 = phase_corrected_power_4 - rms_power_4 # diff_5 = phase_corrected_power_5 - rms_power_5 # Phase Corrected Results # logger.debug("\n") # logger.debug(f"CT0 Real Power: {round(results['ct0']['power'], 2):>10} W | Amps: {round(results['ct0']['current'], 2):<7} | RMS Power: {round(results['ct0']['current'] * results['ct0']['voltage'], 2):<6} W | PF: {round(results['ct0']['pf'], 5)}") # logger.debug(f"CT1 Real Power: {round(results['ct1']['power'], 2):>10} W | Amps: {round(results['ct1']['current'], 2):<7} | RMS Power: {round(results['ct1']['current'] * results['ct1']['voltage'], 2):<6} W | PF: {round(results['ct1']['pf'], 5)}") # logger.debug(f"CT2 Real Power: {round(results['ct2']['power'], 2):>10} W | Amps: {round(results['ct2']['current'], 2):<7} | RMS Power: {round(results['ct2']['current'] * results['ct2']['voltage'], 2):<6} W | PF: {round(results['ct2']['pf'], 5)}") # logger.debug(f"CT3 Real Power: {round(results['ct3']['power'], 2):>10} W | Amps: {round(results['ct3']['current'], 2):<7} | RMS Power: {round(results['ct3']['current'] * results['ct3']['voltage'], 2):<6} W | PF: {round(results['ct3']['pf'], 5)}") # logger.debug(f"CT4 Real Power: {round(results['ct4']['power'], 2):>10} W | Amps: {round(results['ct4']['current'], 2):<7} | RMS Power: {round(results['ct4']['current'] * results['ct4']['voltage'], 2):<6} W | PF: {round(results['ct4']['pf'], 5)}") # logger.debug(f"CT5 Real Power: {round(results['ct5']['power'], 2):>10} W | Amps: {round(results['ct5']['current'], 2):<7} | RMS Power: {round(results['ct5']['current'] * results['ct5']['voltage'], 2):<6} W | PF: {round(results['ct5']['pf'], 5)}") # logger.debug(f"Line Voltage: {round(results['voltage'], 2)} V") # Prepare values for database storage grid_0_power = results['ct0']['power'] # 200A Main (left) grid_1_power = results['ct1']['power'] # 200A Main (right) grid_2_power = results['ct2']['power'] # 100A Main (top) grid_4_power = results['ct4']['power'] # 100A Main (bottom) grid_5_power = results['ct5']['power'] # Unused grid_0_current = results['ct0']['current'] grid_1_current = results['ct1']['current'] grid_2_current = results['ct2']['current'] grid_4_current = results['ct4']['current'] grid_5_current = results['ct5']['current'] solar_power = results['ct3']['power'] solar_current = results['ct3']['current'] solar_pf = results['ct3']['pf'] # Set solar power and current to zero if the solar power is under 20W. if solar_power < 20: solar_power = 0 solar_current = 0 solar_pf = 0 # Determine if the system is net producing or net consuming right now by looking at the two panel mains. # Since the current measured is always positive, we need to add a negative sign to the amperage value if we're exporting power. if grid_0_power < 0: grid_0_current = grid_0_current * -1 if grid_1_power < 0: grid_1_current = grid_1_current * -1 if solar_power > 0: solar_current = solar_current * -1 # Unless your specific panel setup matches mine exactly, the following four lines will likely need to be re-written: home_consumption_power = grid_2_power + grid_4_power + grid_0_power + grid_1_power + solar_power net_power = home_consumption_power - solar_power home_consumption_current = grid_2_current + grid_4_current + grid_0_current + grid_1_current - solar_current net_current = grid_0_current + grid_1_current + grid_2_current + grid_4_current + solar_current if net_power < 0: current_status = "Producing" else: current_status = "Consuming" # Average 2 readings before sending to db if i < 2: solar_power_values['power'].append(solar_power) solar_power_values['current'].append(solar_current) solar_power_values['pf'].append(solar_pf) home_load_values['power'].append(home_consumption_power) home_load_values['current'].append(home_consumption_current) net_power_values['power'].append(net_power) net_power_values['current'].append(net_current) ct0_dict['power'].append(results['ct0']['power']) ct0_dict['current'].append(results['ct0']['current']) ct0_dict['pf'].append(results['ct0']['pf']) ct1_dict['power'].append(results['ct1']['power']) ct1_dict['current'].append(results['ct1']['current']) ct1_dict['pf'].append(results['ct1']['pf']) ct2_dict['power'].append(results['ct2']['power']) ct2_dict['current'].append(results['ct2']['current']) ct2_dict['pf'].append(results['ct2']['pf']) ct3_dict['power'].append(results['ct3']['power']) ct3_dict['current'].append(results['ct3']['current']) ct3_dict['pf'].append(results['ct3']['pf']) ct4_dict['power'].append(results['ct4']['power']) ct4_dict['current'].append(results['ct4']['current']) ct4_dict['pf'].append(results['ct4']['pf']) ct5_dict['power'].append(results['ct5']['power']) ct5_dict['current'].append(results['ct5']['current']) ct5_dict['pf'].append(results['ct5']['pf']) i += 1 else: # Calculate the average, send the result to InfluxDB, and reset the dictionaries for the next 2 sets of data. infl.write_to_influx(solar_power_values, home_load_values, net_power_values, ct0_dict, ct1_dict, ct2_dict, ct3_dict, ct4_dict, ct5_dict, poll_time, i) solar_power_values = dict(power=[], pf=[], current=[]) home_load_values = dict(power=[], pf=[], current=[]) net_power_values = dict(power=[], current=[]) ct0_dict = dict(power=[], pf=[], current=[]) ct1_dict = dict(power=[], pf=[], current=[]) ct2_dict = dict(power=[], pf=[], current=[]) ct3_dict = dict(power=[], pf=[], current=[]) ct4_dict = dict(power=[], pf=[], current=[]) ct5_dict = dict(power=[], pf=[], current=[]) i = 0 if logger.handlers[0].level == 10: t = PrettyTable( ['', 'CT0', 'CT1', 'CT2', 'CT3', 'CT4', 'CT5']) t.add_row([ 'Watts', round(results['ct0']['power'], 3), round(results['ct1']['power'], 3), round(results['ct2']['power'], 3), round(results['ct3']['power'], 3), round(results['ct4']['power'], 3), round(results['ct5']['power'], 3) ]) t.add_row([ 'Current', round(results['ct0']['current'], 3), round(results['ct1']['current'], 3), round(results['ct2']['current'], 3), round(results['ct3']['current'], 3), round(results['ct4']['current'], 3), round(results['ct5']['current'], 3) ]) t.add_row([ 'P.F.', round(results['ct0']['pf'], 3), round(results['ct1']['pf'], 3), round(results['ct2']['pf'], 3), round(results['ct3']['pf'], 3), round(results['ct4']['pf'], 3), round(results['ct5']['pf'], 3) ]) t.add_row([ 'Voltage', round(results['voltage'], 3), '', '', '', '', '' ]) s = t.get_string() logger.debug('\n' + s) #sleep(0.1) except KeyboardInterrupt: infl.close_db() sys.exit()
logger.info( dedent("""Usage: Start the program: python3 power-monitor.py Collect raw data and build an interactive plot: python3 power-monitor.py debug "chart title here" Launch interactive phase correction mode: python3 power-monitor.py phase Start the program like normal, but print all python3 power-monitor.py terminal readings to the terminal window """)) if MODE.lower() == 'debug': # This mode is intended to take a look at the raw CT sensor data. It will take 2000 samples from each CT sensor, plot them to a single chart, write the chart to an HTML file located in /var/www/html/, and then terminate. # It also stores the samples to a file located in ./data/samples/last-debug.pkl so that the sample data can be read when this program is started in 'phase' mode. samples = collect_data(2000) logger.debug("Finished Collecting Samples") ct0_samples = samples['ct0'] ct1_samples = samples['ct1'] ct2_samples = samples['ct2'] ct3_samples = samples['ct3'] ct4_samples = samples['ct4'] ct5_samples = samples['ct5'] v_samples = samples['voltage'] # Save samples to disk with open('data/samples/last-debug.pkl', 'wb') as f: pickle.dump(samples, f) if not title: title = input("Enter the title for this chart: ")
def find_phasecal(samples, ct_selection, accuracy_digits, board_voltage): # This controls how many times the calibration process is repeated for this particular CT. num_calibration_attempts = 20 # Get Initial PF rebuilt_wave = rebuild_wave(samples[ct_selection], samples['voltage'], 1.0) results = check_phasecal(rebuilt_wave['ct'], rebuilt_wave['new_v'], board_voltage) pf = results['pf'] logger.debug( f"Please wait while I read {ct_selection} and calculate the best PHASECAL value. This can take a few minutes, so please be patient." ) best_pfs = [] previous_phasecal = 1.0 previous_pf = pf trends = [] # Base Increment settings for changing phasecal increment = 1.005 decrement = 0.995 big_increment = 1.01 big_decrement = 0.98 for i, _ in enumerate(range(3), start=1): best_pf = { 'pf': 0, 'cal': 0, } for _ in range(75): if round(pf, 4) == 1.0: best_pf.update({ 'pf': pf, 'cal': new_phasecal, }) break if pf < 1.0: # If the PF isn't better than 0.995, we can increment the phasecal by an amount twice as large, referred to as big_increment, to help speed up the process. if round(pf, 2) != 1.0: new_phasecal = previous_phasecal * big_increment else: new_phasecal = previous_phasecal * increment action = 'incremented' else: if round(pf, 2) != 1.0: new_phasecal = previous_phasecal * big_decrement else: new_phasecal = previous_phasecal * decrement action = 'decremented' # Collect a live sample and calculate PF using new_phasecal samples = collect_data(2000) rebuilt_wave = rebuild_wave(samples[ct_selection], samples['voltage'], new_phasecal) results = check_phasecal(rebuilt_wave['ct'], rebuilt_wave['new_v'], board_voltage) pf = results['pf'] if pf > best_pf['pf']: best_pf.update({'pf': pf, 'cal': new_phasecal}) #logger.debug(f" PF: {pf} | Phasecal: {new_phasecal}") # Determine whether or not trend is moving away from 1.0 or towards 1.0. # Trend should be a moving average over two values. trends.append(pf - previous_pf) previous_phasecal = new_phasecal if len(trends) == 2: # Check to see if both values have the same sign to determine the actual trend, then empty the list if trends[0] < 0: if trends[1] < 0: trend = 'worse' # If the trend is getting worse, reject the previous phasecal, and reduce cut increment/decrement by half. increment = 1 + (abs(1 - increment) / 2) decrement = decrement + ((1 - decrement) / 2) # Apply the opposite action to the previous phasecal value to attempt to reverse the trend. If the previous phasecal # was incremented, then we will decrement using the newly adjusted decrement value. if action == 'increment': # Decrement instead new_phasecal = previous_phasecal * decrement else: # Increment instead new_phasecal = previous_phasecal * increment if trends[1] > 0: trend = 'better' trends = [] continue # Skip updating the previous phasecal and previous_pf since we want to attempt to reverse the trend. else: if action == 'increment': # Repeat same action new_phasecal = previous_phasecal * increment else: new_phasecal = previous_phasecal * decrement previous_pf = pf logger.debug(f"Wave {i}/3 results: ") logger.debug( f" Best PF: {best_pf['pf']} using phasecal: {best_pf['cal']}") best_pfs.append(best_pf) return best_pfs