├── images ├── HVAC_cycle.mp4 ├── Total_Energy.mp4 ├── cycle_times.jpg ├── dehumidifier.mp4 ├── wiring_to_pi.png ├── tasmota outlet.jpg ├── Node-Red_dashboard.png ├── power - rice cooker.png ├── Power - Washer zoom in.png ├── calibration_setup_01.jpg ├── hourly item - AC only.png ├── Node-Red_flow_dashboard.png ├── hour item - query details.png ├── Grafana_config_discrete_panel.jpg ├── xcel_energy_bill_compare_crop.png ├── Daily Energy - Cost july xcel date.png ├── compare_AC_to_dehumidifier_humidity.jpg └── xcel_energy_saver switch_activation.jpg ├── circuitsetup_mqtt ├── emon_home.service ├── error_handling.py ├── atm90_e32_registers.py ├── monitor.py └── atm90_e32_pi.py ├── LICENSE └── README.md /images/HVAC_cycle.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsaitsai/circuitsetup_energy_to_mqtt/HEAD/images/HVAC_cycle.mp4 -------------------------------------------------------------------------------- /images/Total_Energy.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsaitsai/circuitsetup_energy_to_mqtt/HEAD/images/Total_Energy.mp4 -------------------------------------------------------------------------------- /images/cycle_times.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsaitsai/circuitsetup_energy_to_mqtt/HEAD/images/cycle_times.jpg -------------------------------------------------------------------------------- /images/dehumidifier.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsaitsai/circuitsetup_energy_to_mqtt/HEAD/images/dehumidifier.mp4 -------------------------------------------------------------------------------- /images/wiring_to_pi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsaitsai/circuitsetup_energy_to_mqtt/HEAD/images/wiring_to_pi.png -------------------------------------------------------------------------------- /images/tasmota outlet.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsaitsai/circuitsetup_energy_to_mqtt/HEAD/images/tasmota outlet.jpg -------------------------------------------------------------------------------- /images/Node-Red_dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsaitsai/circuitsetup_energy_to_mqtt/HEAD/images/Node-Red_dashboard.png -------------------------------------------------------------------------------- /images/power - rice cooker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsaitsai/circuitsetup_energy_to_mqtt/HEAD/images/power - rice cooker.png -------------------------------------------------------------------------------- /images/Power - Washer zoom in.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsaitsai/circuitsetup_energy_to_mqtt/HEAD/images/Power - Washer zoom in.png -------------------------------------------------------------------------------- /images/calibration_setup_01.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsaitsai/circuitsetup_energy_to_mqtt/HEAD/images/calibration_setup_01.jpg -------------------------------------------------------------------------------- /images/hourly item - AC only.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsaitsai/circuitsetup_energy_to_mqtt/HEAD/images/hourly item - AC only.png -------------------------------------------------------------------------------- /images/Node-Red_flow_dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsaitsai/circuitsetup_energy_to_mqtt/HEAD/images/Node-Red_flow_dashboard.png -------------------------------------------------------------------------------- /images/hour item - query details.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsaitsai/circuitsetup_energy_to_mqtt/HEAD/images/hour item - query details.png -------------------------------------------------------------------------------- /images/Grafana_config_discrete_panel.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsaitsai/circuitsetup_energy_to_mqtt/HEAD/images/Grafana_config_discrete_panel.jpg -------------------------------------------------------------------------------- /images/xcel_energy_bill_compare_crop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsaitsai/circuitsetup_energy_to_mqtt/HEAD/images/xcel_energy_bill_compare_crop.png -------------------------------------------------------------------------------- /images/Daily Energy - Cost july xcel date.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsaitsai/circuitsetup_energy_to_mqtt/HEAD/images/Daily Energy - Cost july xcel date.png -------------------------------------------------------------------------------- /images/compare_AC_to_dehumidifier_humidity.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsaitsai/circuitsetup_energy_to_mqtt/HEAD/images/compare_AC_to_dehumidifier_humidity.jpg -------------------------------------------------------------------------------- /images/xcel_energy_saver switch_activation.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsaitsai/circuitsetup_energy_to_mqtt/HEAD/images/xcel_energy_saver switch_activation.jpg -------------------------------------------------------------------------------- /circuitsetup_mqtt/emon_home.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | # put .service file in /lib/systemd/system 3 | Description=Electrical Monitoring 4 | 5 | [Install] 6 | WantedBy=default.target 7 | 8 | [Service] 9 | # Command to execute when the service is started 10 | WorkingDirectory=/home/pi/emon 11 | User=pi 12 | ExecStart=/usr/bin/python3 /home/pi/emon/monitor.py 13 | Restart=on-failure -------------------------------------------------------------------------------- /circuitsetup_mqtt/error_handling.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import logging 3 | logger = logging.getLogger(__name__) 4 | 5 | 6 | class Error(Exception): 7 | """ Base class for the exceptions in this module. """ 8 | pass 9 | 10 | 11 | class RegistryWriteError(Error): 12 | """Exception raised when write to atm90e32 register 13 | cannot be validated. 14 | 15 | Attributes: 16 | message -- register where write verificaiton failed. 17 | 18 | """ 19 | 20 | def __init__(self, message): 21 | self.message = message 22 | 23 | 24 | def handle_exception(e): 25 | """Function all modules share to handled exceptions. 26 | Currently error strings (e) are put into the log file as 27 | an exception. 28 | 29 | :param e: Error message. 30 | 31 | """ 32 | 33 | logger.exception(f'Exception...{e}') 34 | sys.exit() 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Eric Tsai 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This repo has code and notes for whole home electrical power monitoring using a Raspberry Pi and [CircuitSetup modules](https://circuitsetup.us). Beyond just collecting data, it also uses Grafana to visualize electricity consumption. 2 | 3 |

4 | CircuitSetup module wiring to Raspberry Pi 5 | CircuitSetup 6 |
7 | Node-Red dashboard 8 | CircuitSetup 9 |
10 | Grafana Dashboard 11 | CircuitSetup 12 |

13 | CircuitSetup 14 | CircuitSetup 15 | CircuitSetup 16 | 17 | 18 | -------------------------------------------------------------------------------- /circuitsetup_mqtt/atm90_e32_registers.py: -------------------------------------------------------------------------------- 1 | # REGISTERS * 2 | """Mappings of the atm90e32 to user readable strings. 3 | """ 4 | # STATUS REGISTERS *# 5 | MeterEn = 0x00 # Metering Enable 6 | ChannelMapI = 0x01 # Current Channel Mapping Configuration 7 | ChannelMapU = 0x02 # Voltage Channel Mapping Configuration 8 | SagPeakDetCfg = 0x05 # Sag and Peak Detector Period Configuration 9 | OVth = 0x06 # Over Voltage Threshold 10 | ZXConfig = 0x07 # Zero-Crossing Config 11 | SagTh = 0x08 # Voltage Sag Th 12 | PhaseLossTh = 0x09 # Voltage Phase Losing Th 13 | INWarnTh = 0x0A # Neutral Current (Calculated) Warning Threshold 14 | OIth = 0x0B # Over Current Threshold 15 | FreqLoTh = 0x0C # Low Threshold for Frequency Detection 16 | FreqHiTh = 0x0D # High Threshold for Frequency Detection 17 | PMPwrCtrl = 0x0E # Partial Measurement Mode Power Control 18 | IRQ0MergeCfg = 0x0F # IRQ0 Merge Configuration 19 | ENStatus0 = 0x95 # Tells us if readings are above the threshold and being read 20 | 21 | 22 | # EMM STATUS REGISTERS *# 23 | SoftReset = 0x70 # Software Reset 24 | EMMState0 = 0x71 # EMM State 0 25 | EMMState1 = 0x72 # EMM State 1 26 | EMMIntState0 = 0x73 # EMM Interrupt Status 0 27 | EMMIntState1 = 0x74 # EMM Interrupt Status 1 28 | EMMIntEn0 = 0x75 # EMM Interrupt Enable 0 29 | EMMIntEn1 = 0x76 # EMM Interrupt Enable 1 30 | LastSPIData = 0x78 # Last Read/Write SPI Value 31 | CRCErrStatus = 0x79 # CRC Error Status 32 | CRCDigest = 0x7A # CRC Digest 33 | CfgRegAccEn = 0x7F # Configure Register Access Enable 34 | 35 | # LOW POWER MODE REGISTERS - NOT USED *# 36 | DetectCtrl = 0x10 37 | DetectTh1 = 0x11 38 | DetectTh2 = 0x12 39 | DetectTh3 = 0x13 40 | PMOffsetA = 0x14 41 | PMOffsetB = 0x15 42 | PMOffsetC = 0x16 43 | PMPGA = 0x17 44 | PMIrmsA = 0x18 45 | PMIrmsB = 0x19 46 | PMIrmsC = 0x1A 47 | PMConfig = 0x10B 48 | PMAvgSamples = 0x1C 49 | PMIrmsLSB = 0x1D 50 | 51 | # CONFIGURATION REGISTERS *# 52 | PLconstH = 0x31 # High Word of PL_Constant 53 | PLconstL = 0x32 # Low Word of PL_Constant 54 | MMode0 = 0x33 # Metering Mode Config 55 | MMode1 = 0x34 # PGA Gain Configuration for Current Channels 56 | PStartTh = 0x35 # Startup Power Th (P) 57 | QStartTh = 0x36 # Startup Power Th (Q) 58 | SStartTh = 0x37 # Startup Power Th (S) 59 | PPhaseTh = 0x38 # Startup Power Accum Th (P) 60 | QPhaseTh = 0x39 # Startup Power Accum Th (Q) 61 | SPhaseTh = 0x3A # Startup Power Accum Th (S) 62 | 63 | # CALIBRATION REGISTERS *# 64 | PoffsetA = 0x41 # A Line Power Offset (P) 65 | QoffsetA = 0x42 # A Line Power Offset (Q) 66 | PoffsetB = 0x43 # B Line Power Offset (P) 67 | QoffsetB = 0x44 # B Line Power Offset (Q) 68 | PoffsetC = 0x45 # C Line Power Offset (P) 69 | QoffsetC = 0x46 # C Line Power Offset (Q) 70 | PQGainA = 0x47 # A Line Calibration Gain 71 | PhiA = 0x48 # A Line Calibration Angle 72 | PQGainB = 0x49 # B Line Calibration Gain 73 | PhiB = 0x4A # B Line Calibration Angle 74 | PQGainC = 0x4B # C Line Calibration Gain 75 | PhiC = 0x4C # C Line Calibration Angle 76 | 77 | # FUNDAMENTAL#HARMONIC ENERGY CALIBRATION REGISTERS *# 78 | POffsetAF = 0x51 # A Fund Power Offset (P) 79 | POffsetBF = 0x52 # B Fund Power Offset (P) 80 | POffsetCF = 0x53 # C Fund Power Offset (P) 81 | PGainAF = 0x54 # A Fund Power Gain (P) 82 | PGainBF = 0x55 # B Fund Power Gain (P) 83 | PGainCF = 0x56 # C Fund Power Gain (P) 84 | 85 | # MEASUREMENT CALIBRATION REGISTERS *# 86 | UgainA = 0x61 # A Voltage RMS Gain 87 | IgainA = 0x62 # A Current RMS Gain 88 | UoffsetA = 0x63 # A Voltage Offset 89 | IoffsetA = 0x64 # A Current Offset 90 | UgainB = 0x65 # B Voltage RMS Gain 91 | IgainB = 0x66 # B Current RMS Gain 92 | UoffsetB = 0x67 # B Voltage Offset 93 | IoffsetB = 0x68 # B Current Offset 94 | UgainC = 0x69 # C Voltage RMS Gain 95 | IgainC = 0x6A # C Current RMS Gain 96 | UoffsetC = 0x6B # C Voltage Offset 97 | IoffsetC = 0x6C # C Current Offset 98 | IoffsetN = 0x6E # N Current Offset 99 | 100 | # ENERGY REGISTERS *# 101 | APenergyT = 0x80 # Total Forward Active 102 | APenergyA = 0x81 # A Forward Active 103 | APenergyB = 0x82 # B Forward Active 104 | APenergyC = 0x83 # C Forward Active 105 | ANenergyT = 0x84 # Total Reverse Active 106 | ANenergyA = 0x85 # A Reverse Active 107 | ANenergyB = 0x86 # B Reverse Active 108 | ANenergyC = 0x87 # C Reverse Active 109 | RPenergyT = 0x88 # Total Forward Reactive 110 | RPenergyA = 0x89 # A Forward Reactive 111 | RPenergyB = 0x8A # B Forward Reactive 112 | RPenergyC = 0x8B # C Forward Reactive 113 | RNenergyT = 0x8C # Total Reverse Reactive 114 | RNenergyA = 0x8D # A Reverse Reactive 115 | RNenergyB = 0x8E # B Reverse Reactive 116 | RNenergyC = 0x8F # C Reverse Reactive 117 | 118 | SAenergyT = 0x90 # Total Apparent Energy 119 | SenergyA = 0x91 # A Apparent Energy 120 | SenergyB = 0x92 # B Apparent Energy 121 | SenergyC = 0x93 # C Apparent Energy 122 | 123 | 124 | # FUNDAMENTAL # HARMONIC ENERGY REGISTERS *# 125 | APenergyTF = 0xA0 # Total Forward Fund. Energy 126 | APenergyAF = 0xA1 # A Forward Fund. Energy 127 | APenergyBF = 0xA2 # B Forward Fund. Energy 128 | APenergyCF = 0xA3 # C Forward Fund. Energy 129 | ANenergyTF = 0xA4 # Total Reverse Fund Energy 130 | ANenergyAF = 0xA5 # A Reverse Fund. Energy 131 | ANenergyBF = 0xA6 # B Reverse Fund. Energy 132 | ANenergyCF = 0xA7 # C Reverse Fund. Energy 133 | APenergyTH = 0xA8 # Total Forward Harm. Energy 134 | APenergyAH = 0xA9 # A Forward Harm. Energy 135 | APenergyBH = 0xAA # B Forward Harm. Energy 136 | APenergyCH = 0xAB # C Forward Harm. Energy 137 | ANenergyTH = 0xAC # Total Reverse Harm. Energy 138 | ANenergyAH = 0xAD # A Reverse Harm. Energy 139 | ANenergyBH = 0xAE # B Reverse Harm. Energy 140 | ANenergyCH = 0xAF # C Reverse Harm. Energy 141 | 142 | # POWER & P.F. REGISTERS *# 143 | PmeanT = 0xB0 # Total Mean Power (P) 144 | PmeanA = 0xB1 # A Mean Power (P) 145 | PmeanB = 0xB2 # B Mean Power (P) 146 | PmeanC = 0xB3 # C Mean Power (P) 147 | QmeanT = 0xB4 # Total Mean Power (Q) 148 | QmeanA = 0xB5 # A Mean Power (Q) 149 | QmeanB = 0xB6 # B Mean Power (Q) 150 | QmeanC = 0xB7 # C Mean Power (Q) 151 | SmeanT = 0xB8 # Total Mean Power (S) 152 | SmeanA = 0xB9 # A Mean Power (S) 153 | SmeanB = 0xBA # B Mean Power (S) 154 | SmeanC = 0xBB # C Mean Power (S) 155 | PFmeanT = 0xBC # Mean Power Factor 156 | PFmeanA = 0xBD # A Power Factor 157 | PFmeanB = 0xBE # B Power Factor 158 | PFmeanC = 0xBF # C Power Factor 159 | 160 | PmeanTLSB = 0xC0 # Lower Word (Tot. Act. Power) 161 | PmeanALSB = 0xC1 # Lower Word (A Act. Power) 162 | PmeanBLSB = 0xC2 # Lower Word (B Act. Power) 163 | PmeanCLSB = 0xC3 # Lower Word (C Act. Power) 164 | QmeanTLSB = 0xC4 # Lower Word (Tot. React. Power) 165 | QmeanALSB = 0xC5 # Lower Word (A React. Power) 166 | QmeanBLSB = 0xC6 # Lower Word (B React. Power) 167 | QmeanCLSB = 0xC7 # Lower Word (C React. Power) 168 | SAmeanTLSB = 0xC8 # Lower Word (Tot. App. Power) 169 | SmeanALSB = 0xC9 # Lower Word (A App. Power) 170 | SmeanBLSB = 0xCA # Lower Word (B App. Power) 171 | SmeanCLSB = 0xCB # Lower Word (C App. Power) 172 | 173 | # FUND#HARM POWER & V#I RMS REGISTERS *# 174 | PmeanTF = 0xD0 # Total Active Fund. Power 175 | PmeanAF = 0xD1 # A Active Fund. Power 176 | PmeanBF = 0xD2 # B Active Fund. Power 177 | PmeanCF = 0xD3 # C Active Fund. Power 178 | PmeanTH = 0xD4 # Total Active Harm. Power 179 | PmeanAH = 0xD5 # A Active Harm. Power 180 | PmeanBH = 0xD6 # B Active Harm. Power 181 | PmeanCH = 0xD7 # C Active Harm. Power 182 | UrmsA = 0xD9 # A RMS Voltage 183 | UrmsB = 0xDA # B RMS Voltage 184 | UrmsC = 0xDB # C RMS Voltage 185 | IrmsA = 0xDD # A RMS Current 186 | IrmsB = 0xDE # B RMS Current 187 | IrmsC = 0xDF # C RMS Current 188 | IrmsN = 0xD8 # Calculated N RMS Current 189 | 190 | PmeanTFLSB = 0xE0 # Lower Word (Tot. Act. Fund. Power) 191 | PmeanAFLSB = 0xE1 # Lower Word (A Act. Fund. Power) 192 | PmeanBFLSB = 0xE2 # Lower Word (B Act. Fund. Power) 193 | PmeanCFLSB = 0xE3 # Lower Word (C Act. Fund. Power) 194 | PmeanTHLSB = 0xE4 # Lower Word (Tot. Act. Harm. Power) 195 | PmeanAHLSB = 0xE5 # Lower Word (A Act. Harm. Power) 196 | PmeanBHLSB = 0xE6 # Lower Word (B Act. Harm. Power) 197 | PmeanCHLSB = 0xE7 # Lower Word (C Act. Harm. Power) 198 | # 0xE8 ## Reserved Register 199 | UrmsALSB = 0xE9 # Lower Word (A RMS Voltage) 200 | UrmsBLSB = 0xEA # Lower Word (B RMS Voltage) 201 | UrmsCLSB = 0xEB # Lower Word (C RMS Voltage) 202 | # 0xEC ## Reserved Register 203 | IrmsALSB = 0xED # Lower Word (A RMS Current) 204 | IrmsBLSB = 0xEE # Lower Word (B RMS Current) 205 | IrmsCLSB = 0xEF # Lower Word (C RMS Current) 206 | 207 | # THD, FREQUENCY, ANGLE & TEMPTEMP REGISTERS*# 208 | THDNUA = 0xF1 # A Voltage THD+N 209 | THDNUB = 0xF2 # B Voltage THD+N 210 | THDNUC = 0xF3 # C Voltage THD+N 211 | # 0xF4 ## Reserved Register 212 | THDNIA = 0xF5 # A Current THD+N 213 | THDNIB = 0xF6 # B Current THD+N 214 | THDNIC = 0xF7 # C Current THD+N 215 | Freq = 0xF8 # Frequency 216 | PAngleA = 0xF9 # A Mean Phase Angle 217 | PAngleB = 0xFA # B Mean Phase Angle 218 | PAngleC = 0xFB # C Mean Phase Angle 219 | Temp = 0xFC # Measured Temperature 220 | UangleA = 0xFD # A Voltage Phase Angle 221 | UangleB = 0xFE # B Voltage Phase Angle 222 | UangleC = 0xFF # C Voltage Phase Angle 223 | -------------------------------------------------------------------------------- /circuitsetup_mqtt/monitor.py: -------------------------------------------------------------------------------- 1 | ########################################################################## 2 | # Much of this code was copied from Margaret Johnson's (@BitKnitting) 3 | # FHmonitor project https://github.com/BitKnitting/FHmonitor 4 | # 5 | # 2020-09: Eric Tsai 6 | # parameterize SPI groupings to read multiple CircuitSetup 7 | # monitoring boards and publish readings to MQTT 8 | # Not Done: THD, phase angle, publish on minimum change 9 | ######################################################################### 10 | 11 | ''' 12 | 13 | --start script 14 | python3 monitor.py 15 | 16 | --view logs 17 | journalctl | tail 18 | 19 | *** todo *** 20 | 1. Set MQTT broker 21 | 2. Append the right number of circuits 22 | 3. Calibrate AC adapter gain and current transformer gain 23 | 4. Set reading rate by changing sleep time 24 | 25 | 26 | ''' 27 | 28 | 29 | from error_handling import handle_exception 30 | from atm90_e32_pi import ATM90e32 31 | 32 | import board 33 | import digitalio 34 | import logging 35 | logger = logging.getLogger(__name__) 36 | import paho.mqtt.client as mqtt 37 | import time 38 | from datetime import datetime 39 | 40 | logging.basicConfig(level=logging.WARNING) 41 | 42 | # Set to false if don't want regular readings, and want to send readings if > minimum change 43 | ALWAYS_PUBLISH = True 44 | 45 | class Monitor: 46 | """ 47 | 48 | """ 49 | def on_connect(client, userdata, flags, rc): 50 | print("Connected with result code "+str(rc)) 51 | try: 52 | self.client.publish("/emon/heartbeat", "connected", qos=0) 53 | except: 54 | time.sleep(3) 55 | 56 | def on_disconnect(client, userdata,rc=0): 57 | print("disconnected with result code "+str(rc)) 58 | self.client.reconnect 59 | 60 | def __init__(self, led_pin=None): 61 | #logger.setLevel(logging.warning) 62 | self.debug = False 63 | #if using ctrl_c or restarting a lot, set this to False. If run in background, set to True 64 | self.as_service = True 65 | 66 | self.db = None 67 | self.t_heartbeat = time.perf_counter() 68 | 69 | self.circuit = [] 70 | self.num_circuit = 0 71 | 72 | self.energy_sensor = None 73 | self.energy_sensor2 = None 74 | self.energy_sensor3 = None 75 | self.energy_sensor4 = None 76 | if led_pin is None: 77 | led_pin = board.D18 # We always wire to GPIO 18. 78 | self.led = digitalio.DigitalInOut(board.D18) 79 | self.led.direction = digitalio.Direction.OUTPUT 80 | 81 | self.client = mqtt.Client() 82 | self.client.on_disconnect = self.on_disconnect 83 | 84 | logging.warning('started emon..') 85 | try: 86 | # *** todo *** 87 | self.client.connect("192.168.1.115",1883,60) 88 | if self.as_service: 89 | logging.warning('emon start in threaded mqtt mode') 90 | try: 91 | self.client.loop_start() 92 | except: 93 | logging.warning('emon fail to start loop') 94 | except: 95 | time.sleep(10) 96 | 97 | #################################################### 98 | # Initialize the energy sensor. The properties are 99 | # are written to atm90e32 registers during initialization. 100 | # They are specific to the Power and Current Transformers 101 | # being used. An exception occurs if the write cannot 102 | # be verified. 103 | #################################################### 104 | 105 | def init_sensor(self): 106 | """ 107 | Initialize the atm90e32 by setting the calibration registry properties. 108 | """ 109 | linefreq_60hz = 4485 110 | #pga gain = amps to house. 100amps = 21, >100amps = 42 111 | gain_pga_100amp = 21 112 | 113 | 114 | # *** todo *** 115 | # Replace values with the correct calibration values after performing calibration 116 | gain_voltage_ACA_01 = 7508 # AC adapter model 01, voltage gain for 1st module (circuits 0 and 1) 117 | gain_voltage_ACA_02 = 7261 # AC adapter model 02, voltage gain for 2nd module (circuits 2 and 3) 118 | 119 | # *** todo *** 120 | # current gain for different current transformers (20A, 80A, and 100A transformers) 121 | # if you buy identical 20A split core transformers, they can use the same current gain. If you ebay it and 122 | # use a bunch of different 20A current transformers, may need to calibrate individually ie "20A_00", "20A_01" 123 | gain_current_20A_00 = 5511 # emon_current / real current = x/gain_current, new_gain_current=5511 124 | gain_current_80A_00 = 20966 # emon_current / real current = x/gain_current, new_gain_current=20966 125 | gain_current_100A_00 = 13777 # emon_current / real current = x/gain_current, new_gain_current=13777 126 | 127 | try: 128 | 129 | # Hardcode each circuit's calibration parameters based on AC adapter used and voltage transformer used. 130 | # Using two 6-channel CircuitSetup modules, we actually have four circuits and 3 channels per circuit. 131 | # Each circuit needs to be instantiated with a single AC adapter voltage gain and three 132 | # current transformer current gain (one per channel). 133 | # ATM90e32(linefreq, gain_pga_100amp, AC_ADAPTER_voltage_gain, ... 134 | # ch_0_xfmr_current_gain, ch_1_xfmr_current_gain, ch_2_xfmr_current_gain, raspberry_pi_cc_pin) 135 | # 136 | # 137 | # *** todo *** 138 | #First Module: circuit 0, channel 0-2; Phase A 139 | self.circuit.append(ATM90e32(linefreq_60hz, gain_pga_100amp, gain_voltage_ACA_01, gain_current_100A_00, gain_current_80A_00, gain_current_20A_00, board.D5)) 140 | 141 | #First Module: circuit 1, channel 0-2; Phase A 142 | self.circuit.append(ATM90e32(linefreq_60hz, gain_pga_100amp, gain_voltage_ACA_01, gain_current_20A_00, gain_current_20A_00, gain_current_20A_00, board.D6)) 143 | 144 | #Second Module: circuit 2, channel 0-2; Phase B 145 | self.circuit.append(ATM90e32(linefreq_60hz, gain_pga_100amp, gain_voltage_ACA_02, gain_current_100A_00, gain_current_20A_00, gain_current_20A_00, board.D13)) 146 | 147 | #Second Module: circuit 3, channel 0-2; Phase B 148 | self.circuit.append(ATM90e32(linefreq_60hz, gain_pga_100amp, gain_voltage_ACA_02, gain_current_20A_00, gain_current_80A_00, gain_current_20A_00, board.D19)) 149 | 150 | #append more circuits as needed, or delete as needed 151 | 152 | self.num_circuit = len(self.circuit) # returns 4 for two 6-channel boards 153 | 154 | # generate unique mqtt topics based on circuit number and channel 155 | # circuit 0, channel 0 156 | # self.circuit[0].readings_ray[0]["name"] = "c0c0" 157 | # With two 6-channel circuit setup modules, we have 4 circuits with 3 channels each, like this: 158 | # c0c0, c0c1, c0c2 159 | # c1c0, c1c1, c1c2 160 | # c2c0, c2c1, c2c2 161 | # c3c0, c3c1, c3c2 162 | for i in range(len(self.circuit)): 163 | for j in range(len(self.circuit[i].readings_ray)): 164 | # c0c1 = circuit 0, channel 1 165 | # self.circuit[0].readings_ray[1]["name"] = "c0c1" 166 | self.circuit[i].readings_ray[j]["name"] = "c" + str(i) + "c" + str(j) 167 | 168 | # Option to use minimum change to send updates 169 | # intialize sane min change values so we're not reporting too much 170 | # individual: self.circuit[0].readings_min_change["line_current"] = .02 171 | for x in self.circuit: 172 | # means self.circuit[0, 1, 2, 3].get_all_readings() 173 | #newreadings = x.get_all_readings() 174 | #for y in range(len(x.readings_ray)): 175 | for y in x.readings_min_change: 176 | # means x.readings_ray[0, 1, 2], y=0, 1, 2 objects 177 | y["line_volt"] = 2.0 178 | y["line_current"] = -1 179 | y["power_active"] = -1 180 | y["power_reactive"] = -1 181 | y["power_apparent"] = -1 182 | y["power_factor"] = 1000.0 183 | y["phase_angle"] = 1 184 | y["THD_volt"] = 1 185 | y["THD_current"] = 1 186 | for z in range(len(x.update_interval)): 187 | x.update_interval[z] = 25.0 188 | # todo: add minimum time 189 | #for x.readings_min_time = 10 seconds 190 | 191 | #debug print 192 | #print("----- double check min values ----") 193 | for x in self.circuit: 194 | for y in x.readings_min_change: 195 | # means x.readings_min_change[0, 1, 2], y=0, 1, 2 objects 196 | for z in y: 197 | #print(str(z), y[z]) 198 | pass 199 | 200 | 201 | logger.info('Energy meter has been initialized.') 202 | sys0 = self.circuit[0].sys_status0 203 | if (sys0 == 0xFFFF or sys0 == 0): 204 | e = 'EXCEPTION: Cannot connect to the energy meter.' 205 | handle_exception(e) 206 | logger.info('Energy meter is working.') 207 | return True 208 | except Exception as e: 209 | handle_exception(e) 210 | return False 211 | 212 | 213 | #################################################### 214 | # Take electrical reading 215 | #################################################### 216 | 217 | def take_reading(self): 218 | 219 | if not self.as_service: 220 | #use loop_start() if running as service. If testing, use this. 221 | try: 222 | self.client.loop(.1) 223 | except: 224 | time.sleep(10) 225 | 226 | if (time.perf_counter() - self.t_heartbeat > 240) or (time.perf_counter() - self.t_heartbeat < 0): 227 | self.t_heartbeat = time.perf_counter() 228 | now = datetime.now() 229 | current_time = now.strftime("%d/%m/%y %H:%M:%S") 230 | try: 231 | self.client.publish("/emon/heartbeat", self.t_heartbeat, qos=0) 232 | except: 233 | logging.warning('emon failed to publish heartbeat') 234 | time.sleep(5) 235 | 236 | 237 | # individual readings 238 | # self.circuit[?].get_all_readings() 239 | # self.circuit[X].readings_ray[Y]["metric"] 240 | # X = circuit number (0 to 3 when using two CircuitSetup 6 channel modules) 241 | # Y = channel number (0 to 2), each circuit has 3 channels 242 | # metric = power_active, power_factor, etc... 243 | 244 | for x in self.circuit: 245 | # means self.circuit[0, 1, 2, 3], x=circuit number, iterate through all circuits 246 | x.get_all_readings() 247 | for y in range(len(x.readings_ray)): 248 | # means x.readings_ray[0, 1, 2], y=channels, iterate through all channels 249 | publish_flag = False 250 | if ((x.update_interval[y] == 0.0) or (time.perf_counter() - x.previous_timestamp[y])) > x.update_interval[y]: 251 | publish_flag = True 252 | x.previous_timestamp[y] = time.perf_counter() 253 | if self.debug: 254 | print("time stamp for ", x.readings_ray[y]["name"], " @ ", time.perf_counter(), " more than ", x.update_interval[y], " seconds ago") 255 | 256 | for z in x.readings_ray[y]: 257 | # means readings_ray[y]["metric"], z=metric like line_current, power_active, etc.. 258 | # the first item in list is "name" - don't publish that. 259 | if z != "name": 260 | # decide if change is great enough 261 | if ( abs(x.readings_ray[y][z] - x.readings_previous[y][z]) > x.readings_min_change[y][z]): 262 | if self.debug: 263 | print("changed! ", x.readings_ray[y]["name"], "/", str(z), " from ", x.readings_previous[y][z], " to ", x.readings_ray[y][z]) 264 | x.readings_previous[y][z] = x.readings_ray[y][z] 265 | publish_flag = True 266 | else: 267 | pass 268 | 269 | # publish the data if 1) update interval exceeded 2) metric value changed enough 3) we're just supposed to always publish 270 | if publish_flag or ALWAYS_PUBLISH: 271 | mqtt_topic = "/emon/reading/" + x.readings_ray[y]["name"] + "/" + str(z) 272 | #print(mqtt_topic) 273 | try: 274 | self.client.publish(mqtt_topic, x.readings_ray[y][z]) 275 | except: 276 | time.sleep(5) 277 | #print(" loop=", y["name"], " attribute", str(z), " = ", y[z]) 278 | else: 279 | #no publish 280 | pass 281 | 282 | return 283 | 284 | 285 | 286 | m = Monitor() 287 | 288 | m.init_sensor() 289 | 290 | while(1): 291 | 292 | #t0= time.perf_counter() 293 | m.take_reading() 294 | #print("read duration = ", time.perf_counter()-t0) 295 | # it takes 0.25 seconds to take reading and publish on 12 channels. 296 | 297 | # *** todo *** 298 | # Set how frequently to make readings and publish data 299 | time.sleep(0.5) 300 | 301 | 302 | 303 | 304 | -------------------------------------------------------------------------------- /circuitsetup_mqtt/atm90_e32_pi.py: -------------------------------------------------------------------------------- 1 | ##################################################################### 2 | # SPI handling copied from Margaret Johnson (@BitKnitting) 3 | # atm90e32 SPI utility to read power, power factor, current, voltage, etc. 4 | # Eric Tsai - 2020-09: added power factor and group readings 5 | # todo: THD, minimum change updates 6 | ##################################################################### 7 | from error_handling import RegistryWriteError 8 | import sys 9 | # Used R instead of * to accomodate Flake8. 10 | import atm90_e32_registers as R 11 | 12 | 13 | from adafruit_bus_device.spi_device import SPIDevice 14 | 15 | import board 16 | import busio 17 | import digitalio 18 | import math 19 | import time 20 | import struct 21 | import logging 22 | logger = logging.getLogger(__name__) 23 | 24 | 25 | SPI_WRITE = 0 26 | SPI_READ = 1 27 | 28 | 29 | class ATM90e32: 30 | """This code runs on a Raspberry Pi and talks to an atm90e32 chip 31 | over SPI. We use `Circuit Setup's Single Phase Energy Meter 32 | `_ 33 | 34 | Initialize the atm90e32 by setting the calibration registry properties. 35 | Calibration is discussed within our 36 | `FitHome wiki `_ . 37 | 38 | The SPI pins (including the cs pin) will need to be wired between 39 | a Raspberry Pi and the atm90e32. SPI pins on the Raspberry Pi and Circuit Setup's 40 | energy meter are discussed within our `FitHome wiki 41 | 42 | """ # noqa 43 | ########################################################################## 44 | 45 | ''' 46 | self.readings = { 47 | 'name': "ch_x", 48 | 'line_volt': 0.5, 49 | 'line_current': 0.5, 50 | 'power_active': 0.5, 51 | 'power_reactive': 0.5, 52 | 'power_apparent': 0.5, 53 | 'power_factor': 0.5, 54 | 'phase_angle': 0.5, 55 | 'THD_volt': 1, 56 | 'THD_current': 1 57 | } 58 | ''' 59 | 60 | def __init__(self, linefreq, pgagain, ugain, igainA, igainB, igainC, 61 | cs_pin=None): 62 | pin = board.D5 if cs_pin is None else cs_pin 63 | # We'll need all the settings for the 64 | self._linefreq = linefreq 65 | self._pgagain = pgagain 66 | self._ugain = ugain 67 | self._igainA = igainA 68 | self._igainB = igainB 69 | self._igainC = igainC 70 | spi = busio.SPI(board.SCLK, board.MOSI, board.MISO) 71 | 72 | #cable select pin is same as GPIO name, i.e. "board.D18" = GPIO18 73 | #board.D5 = pin 29 / GPIO5, which is what's already connected for gray wire. 74 | #cs = digitalio.DigitalInOut(pin) 75 | cs = digitalio.DigitalInOut(pin) 76 | # How to use SPI 77 | # https://learn.adafruit.com/circuitpython-basics-i2c-and-spi/spi-devices 78 | # https://circuitpython.readthedocs.io/projects/busdevice/en/latest/_modules/adafruit_bus_device/spi_device.html 79 | self._device = SPIDevice(spi, cs, baudrate=200000, polarity=1, phase=1) 80 | self._init_config() 81 | 82 | # create holder for all readings all channels (A, B, C) 83 | # self.readings_ray = [] 84 | self.readings_ray = [ 85 | { 86 | 'name': "ch_x", 87 | 'line_volt': 0.5, 88 | 'line_current': 0.5, 89 | 'power_active': 0.5, 90 | 'power_reactive': 0.5, 91 | 'power_apparent': 0.5, 92 | 'power_factor': 0.5, 93 | 'phase_angle': 0.5, 94 | 'THD_volt': 1, 95 | 'THD_current': 1 96 | } 97 | for k in range(3) 98 | ] 99 | 100 | self.readings_previous =[ 101 | { 102 | 'line_volt': 0.5, 103 | 'line_current': 0.5, 104 | 'power_active': 0.5, 105 | 'power_reactive': 0.5, 106 | 'power_apparent': 0.5, 107 | 'power_factor': 0.5, 108 | 'phase_angle': 0.5, 109 | 'THD_volt': 1, 110 | 'THD_current': 1 111 | } 112 | for k in range(3) 113 | ] 114 | 115 | self.readings_min_change = [ 116 | { 117 | 'line_volt': 0.0, 118 | 'line_current': 0.0, 119 | 'power_active': 0.0, 120 | 'power_reactive': 0.0, 121 | 'power_apparent': 0.0, 122 | 'power_factor': 0.0, 123 | 'phase_angle': 0.0, 124 | 'THD_volt': 0.0, 125 | 'THD_current': 0.0 126 | } 127 | for k in range(3) 128 | ] 129 | 130 | self.readings_timestamp = [ 131 | { 132 | 'line_volt': 0.5, 133 | 'line_current': 0.5, 134 | 'power_active': 0.5, 135 | 'power_reactive': 0.5, 136 | 'power_apparent': 0.5, 137 | 'power_factor': 0.5, 138 | 'phase_angle': 0.5, 139 | 'THD_volt': 1, 140 | 'THD_current': 1 141 | } 142 | for k in range(3) 143 | ] 144 | self.previous_timestamp = [0.0, 0.0, 0.0] 145 | self.update_interval = [0.0, 0.0, 0.0] 146 | self.readings_min_time = [ 147 | { 148 | 'line_volt': 0.5, 149 | 'line_current': 0.5, 150 | 'power_active': 0.5, 151 | 'power_reactive': 0.5, 152 | 'power_apparent': 0.5, 153 | 'power_factor': 0.5, 154 | 'phase_angle': 0.5, 155 | 'THD_volt': 1, 156 | 'THD_current': 1 157 | } 158 | for k in range(3) 159 | ] 160 | 161 | self.readings_per_moodule = { 162 | 'sys_status0': 0, 163 | 'sys_tatus1': 0, 164 | 'meter_status0': 0, 165 | 'meter_status1': 0, 166 | 'freq': 0.5 167 | } 168 | 169 | 170 | def _usleep(self, x): return time.sleep(x/(1e7)) 171 | 172 | def _init_config(self): 173 | # CurrentGainCT2 = 25498 #25498 - SCT-013-000 100A/50mA 174 | if (self._linefreq == 4485 or self._linefreq == 5231): 175 | # North America power frequency 176 | FreqHiThresh = 61 * 100 177 | FreqLoThresh = 59 * 100 178 | sagV = 90 179 | else: 180 | FreqHiThresh = 51 * 100 181 | FreqLoThresh = 49 * 100 182 | sagV = 190 183 | 184 | # calculation for voltage sag threshold - assumes we do not want to 185 | # go under 90v for split phase and 190v otherwise sqrt(2) = 1.41421356 186 | fvSagTh = (sagV * 100 * 1.41421356) / (2 * self._ugain / 32768) 187 | # convert to int for sending to the atm90e32. 188 | vSagTh = self._round_number(fvSagTh) 189 | 190 | self._spi_rw(SPI_WRITE, R.SoftReset, 0x789A) # Perform soft reset 191 | # enable register config access 192 | self._spi_rw(SPI_WRITE, R.CfgRegAccEn, 0x55AA) 193 | self._spi_rw(SPI_WRITE, R.MeterEn, 0x0001) # Enable Metering 194 | 195 | # Voltage sag threshold 196 | self._spi_rw(SPI_WRITE, R.SagTh, vSagTh) 197 | # High frequency threshold - 61.00Hz 198 | self._spi_rw(SPI_WRITE, R.FreqHiTh, FreqHiThresh) 199 | # Lo frequency threshold - 59.00Hz 200 | self._spi_rw(SPI_WRITE, R.FreqLoTh, FreqLoThresh) 201 | self._spi_rw(SPI_WRITE, R.EMMIntEn0, 0xB76F) # Enable interrupts 202 | self._spi_rw(SPI_WRITE, R.EMMIntEn1, 0xDDFD) # Enable interrupts 203 | # Clear interrupt flags 204 | self._spi_rw(SPI_WRITE, R.EMMIntState0, 0x0001) 205 | # Clear interrupt flags 206 | self._spi_rw(SPI_WRITE, R.EMMIntState1, 0x0001) 207 | # ZX2, ZX1, ZX0 pin config 208 | self._spi_rw(SPI_WRITE, R.ZXConfig, 0x0A55) 209 | 210 | # Set metering config values (CONFIG) 211 | # PL Constant MSB (default) - Meter Constant 212 | # = 3200 - PL Constant = 140625000 213 | self._spi_rw(SPI_WRITE, R.PLconstH, 0x0861) 214 | # PL Constant LSB (default) - this is 4C68 in the application note, 215 | # which is incorrect 216 | self._spi_rw(SPI_WRITE, R.PLconstL, 0xC468) 217 | # Mode Config (frequency set in main program) 218 | self._spi_rw(SPI_WRITE, R.MMode0, self._linefreq) 219 | # PGA Gain Configuration for Current Channels - 0x002A (x4) 220 | # # 0x0015 (x2) # 0x0000 (1x) 221 | self._spi_rw(SPI_WRITE, R.MMode1, self._pgagain) 222 | # Active Startup Power Threshold - 50% of startup current 223 | # = 0.9/0.00032 = 2812.5 224 | # self._spi_rw(SPI_WRITE, PStartTh, 0x0AFC) 225 | # Testing a little lower setting...because readings aren't 226 | # happening if power is off then turned on.... 227 | # Startup Power Threshold = .4/.00032 = 1250 = 0x04E2 228 | # Just checking with 0. 229 | self._spi_rw(SPI_WRITE, R.PStartTh, 0x0000) 230 | # Reactive Startup Power Threshold 231 | # self._spi_rw(SPI_WRITE, QStartTh, 0x0AEC) 232 | self._spi_rw(SPI_WRITE, R.QStartTh, 0x0000) 233 | # Apparent Startup Power Threshold 234 | self._spi_rw(SPI_WRITE, R.SStartTh, 0x0000) 235 | # Active Phase Threshold = 10% of startup current 236 | # = 0.06/0.00032 = 187.5 237 | # self._spi_rw(SPI_WRITE, PPhaseTh, 0x00BC) 238 | self._spi_rw(SPI_WRITE, R.PPhaseTh, 0x0000) 239 | # Reactive Phase Threshold 240 | self._spi_rw(SPI_WRITE, R.QPhaseTh, 0x0000) 241 | # Apparent Phase Threshold 242 | self._spi_rw(SPI_WRITE, R.SPhaseTh, 0x0000) 243 | 244 | # Set metering calibration values (CALIBRATION) 245 | # Line calibration gain 246 | self._spi_rw(SPI_WRITE, R.PQGainA, 0x0000) 247 | # Line calibration angle 248 | self._spi_rw(SPI_WRITE, R.PhiA, 0x0000) 249 | # Line calibration gain 250 | self._spi_rw(SPI_WRITE, R.PQGainB, 0x0000) 251 | # Line calibration angle 252 | self._spi_rw(SPI_WRITE, R.PhiB, 0x0000) 253 | # Line calibration gain 254 | self._spi_rw(SPI_WRITE, R.PQGainC, 0x0000) 255 | # Line calibration angle 256 | self._spi_rw(SPI_WRITE, R.PhiC, 0x0000) 257 | # A line active power offset 258 | self._spi_rw(SPI_WRITE, R.PoffsetA, 0x0000) 259 | # A line reactive power offset 260 | self._spi_rw(SPI_WRITE, R.QoffsetA, 0x0000) 261 | # B line active power offset 262 | self._spi_rw(SPI_WRITE, R.PoffsetB, 0x0000) 263 | # B line reactive power offset 264 | self._spi_rw(SPI_WRITE, R.QoffsetB, 0x0000) 265 | # C line active power offset 266 | self._spi_rw(SPI_WRITE, R.PoffsetC, 0x0000) 267 | # C line reactive power offset 268 | self._spi_rw(SPI_WRITE, R.QoffsetC, 0x0000) 269 | 270 | # Set metering calibration values (HARMONIC) 271 | # A Fund. active power offset 272 | self._spi_rw(SPI_WRITE, R.POffsetAF, 0x0000) 273 | # B Fund. active power offset 274 | self._spi_rw(SPI_WRITE, R.POffsetBF, 0x0000) 275 | # C Fund. active power offset 276 | self._spi_rw(SPI_WRITE, R.POffsetCF, 0x0000) 277 | # A Fund. active power gain 278 | self._spi_rw(SPI_WRITE, R.PGainAF, 0x0000) 279 | # B Fund. active power gain 280 | self._spi_rw(SPI_WRITE, R.PGainBF, 0x0000) 281 | # C Fund. active power gain 282 | self._spi_rw(SPI_WRITE, R.PGainCF, 0x0000) 283 | 284 | # Set measurement calibration values (ADJUST) 285 | # A Voltage rms gain 286 | self._spi_rw(SPI_WRITE, R.UgainA, self._ugain) 287 | # A line current gain 288 | self._spi_rw(SPI_WRITE, R.IgainA, self._igainA) 289 | self._spi_rw(SPI_WRITE, R.UoffsetA, 0x0000) # A Voltage offset 290 | # A line current offset 291 | self._spi_rw(SPI_WRITE, R.IoffsetA, 0x0000) 292 | # B Voltage rms gain 293 | self._spi_rw(SPI_WRITE, R.UgainB, self._ugain) 294 | # B line current gain 295 | self._spi_rw(SPI_WRITE, R.IgainB, self._igainB) 296 | self._spi_rw(SPI_WRITE, R.UoffsetB, 0x0000) # B Voltage offset 297 | # B line current offset 298 | self._spi_rw(SPI_WRITE, R.IoffsetB, 0x0000) 299 | # C Voltage rms gain 300 | self._spi_rw(SPI_WRITE, R.UgainC, self._ugain) 301 | # C line current gain 302 | self._spi_rw(SPI_WRITE, R.IgainC, self._igainC) 303 | self._spi_rw(SPI_WRITE, R.UoffsetC, 0x0000) # C Voltage offset 304 | # C line current offset 305 | self._spi_rw(SPI_WRITE, R.IoffsetC, 0x0000) 306 | self._spi_rw(SPI_WRITE, R.CfgRegAccEn, 0x0000) # end configuration 307 | 308 | # In order to get correct results, I needed to insert 309 | # a 'significant' delay. 310 | time.sleep(1) 311 | ########################################################################## 312 | @property 313 | def lastSpiData(self): 314 | reading = self._spi_rw(SPI_READ, R.LastSPIData, 0xFFFF) 315 | return reading 316 | ########################################################################## 317 | @property 318 | def sys_status0(self): 319 | reading = self._spi_rw(SPI_READ, R.EMMIntState0, 0xFFFF) 320 | return reading 321 | ########################################################################## 322 | @property 323 | def sys_status1(self): 324 | reading = self._spi_rw(SPI_READ, R.EMMIntState1, 0xFFFF) 325 | return reading 326 | ######################################################################### 327 | 328 | @property 329 | def meter_status0(self): 330 | reading = self._spi_rw(SPI_READ, R.EMMState0, 0xFFFF) 331 | return reading 332 | 333 | ########################################################################## 334 | @property 335 | def en_status0(self): 336 | reading = self._spi_rw(SPI_READ, R.ENStatus0, 0xFFFF) 337 | return reading 338 | ######################################################################### 339 | @property 340 | def meter_status1(self): 341 | reading = self._spi_rw(SPI_READ, R.EMMState1, 0xFFFF) 342 | return reading 343 | ######################################################################### 344 | @property 345 | def line_voltageA(self): 346 | reading = self._spi_rw(SPI_READ, R.UrmsA, 0xFFFF) 347 | return reading / 100.0 348 | ######################################################################### 349 | @property 350 | def line_voltageB(self): 351 | reading = self._spi_rw(SPI_READ, R.UrmsB, 0xFFFF) 352 | return reading / 100.0 353 | ######################################################################### 354 | @property 355 | def line_voltageC(self): 356 | reading = self._spi_rw(SPI_READ, R.UrmsC, 0xFFFF) 357 | return reading / 100.0 358 | ######################################################################### 359 | @property 360 | def line_currentA(self): 361 | reading = self._spi_rw(SPI_READ, R.IrmsA, 0xFFFF) 362 | return reading / 1000.0 363 | ######################################################################### 364 | @property 365 | def line_currentB(self): 366 | reading = self._spi_rw(SPI_READ, R.IrmsB, 0xFFFF) 367 | return reading / 1000.0 368 | ######################################################################### 369 | @property 370 | def line_currentC(self): 371 | reading = self._spi_rw(SPI_READ, R.IrmsC, 0xFFFF) 372 | return reading / 1000.0 373 | ######################################################################### 374 | @property 375 | def frequency(self): 376 | reading = self._spi_rw(SPI_READ, R.Freq, 0xFFFF) 377 | return reading / 100.0 378 | ######################################################################### 379 | @property 380 | def total_active_power(self): 381 | reading = self._read32Register(R.PmeanT, R.PmeanTLSB) 382 | return reading * 0.00032 383 | ######################################################################### 384 | 385 | @property 386 | def active_power_A(self): 387 | reading = self._read32Register(R.PmeanA, R.PmeanALSB) 388 | return round(reading * 0.00032, 3) 389 | ##################################################################### 390 | #tsai: added B 391 | @property 392 | def active_power_B(self): 393 | reading = self._read32Register(R.PmeanB, R.PmeanBLSB) 394 | return round(reading * 0.00032, 3) 395 | ##################################################################### 396 | 397 | @property 398 | def active_power_C(self): 399 | reading = self._read32Register(R.PmeanC, R.PmeanCLSB) 400 | return round(reading * 0.00032) 401 | ##################################################################### 402 | 403 | @property 404 | def total_reactive_power(self): 405 | reading = self._read32Register(R.QmeanT, R.PmeanTLSB) 406 | return round(reading * 0.00032, 3) 407 | ######################################################################### 408 | 409 | @property 410 | def reactive_power_A(self): 411 | reading = self._read32Register(R.QmeanA, R.PmeanALSB) 412 | return round(reading * 0.00032, 3) 413 | ##################################################################### 414 | #tsai 415 | @property 416 | def reactive_power_B(self): 417 | reading = self._read32Register(R.QmeanB, R.PmeanBLSB) 418 | return round(reading * 0.00032, 3) 419 | ##################################################################### 420 | 421 | @property 422 | def reactive_power_C(self): 423 | reading = self._read32Register(R.QmeanC, R.PmeanCLSB) 424 | return round(reading * 0.00032, 3) 425 | ##################################################################### 426 | 427 | @property 428 | def apparent_power_A(self): 429 | reading = self._read32Register(R.SmeanA, R.SmeanALSB) 430 | return round(reading * 0.00032, 3) 431 | 432 | ##################################################################### 433 | 434 | @property 435 | def apparent_power_B(self): 436 | reading = self._read32Register(R.SmeanB, R.SmeanBLSB) 437 | return round(reading * 0.00032, 3) 438 | 439 | ##################################################################### 440 | 441 | @property 442 | def apparent_power_C(self): 443 | reading = self._read32Register(R.SmeanC, R.SmeanCLSB) 444 | return round(reading * 0.00032, 3) 445 | 446 | ##################################################################### 447 | 448 | @property 449 | def power_factor_A(self): 450 | reading = self._spi_rw(SPI_READ, R.PFmeanA, 0xFFFF) 451 | return reading / 1000.0 452 | 453 | ##################################################################### 454 | 455 | @property 456 | def power_factor_B(self): 457 | reading = self._spi_rw(SPI_READ, R.PFmeanB, 0xFFFF) 458 | return reading / 1000.0 459 | 460 | ##################################################################### 461 | 462 | @property 463 | def power_factor_C(self): 464 | reading = self._spi_rw(SPI_READ, R.PFmeanC, 0xFFFF) 465 | return reading / 1000.0 466 | 467 | 468 | ##################################################################### 469 | def get_all_readings(self): 470 | 471 | self.readings_ray[0]["line_volt"] = self.line_voltageA 472 | self.readings_ray[1]["line_volt"] = self.line_voltageB 473 | self.readings_ray[2]["line_volt"] = self.line_voltageC 474 | self.readings_ray[0]["line_current"] = self.line_currentA 475 | self.readings_ray[1]["line_current"] = self.line_currentB 476 | self.readings_ray[2]["line_current"] = self.line_currentC 477 | self.readings_ray[0]["power_active"] = self.active_power_A 478 | self.readings_ray[1]["power_active"] = self.active_power_B 479 | self.readings_ray[2]["power_active"] = self.active_power_C 480 | self.readings_ray[0]["power_reactive"] = self.reactive_power_A 481 | self.readings_ray[1]["power_reactive"] = self.reactive_power_B 482 | self.readings_ray[2]["power_reactive"] = self.reactive_power_C 483 | self.readings_ray[0]["power_apparent"] = self.apparent_power_A 484 | self.readings_ray[1]["power_apparent"] = self.apparent_power_B 485 | self.readings_ray[2]["power_apparent"] = self.apparent_power_C 486 | self.readings_ray[0]["power_factor"] = self.power_factor_A 487 | self.readings_ray[1]["power_factor"] = self.power_factor_B 488 | self.readings_ray[2]["power_factor"] = self.power_factor_C 489 | 490 | return None 491 | 492 | 493 | 494 | ###################################################### 495 | # Return the value that is stored at the address. 496 | ###################################################### 497 | 498 | def read(self, address): 499 | two_byte_buf = bytearray(2) 500 | results_buf = bytearray(2) 501 | # Let atm90e32 know want to read the register. 502 | address |= 1 << 15 503 | struct.pack_into('>H', two_byte_buf, 0, address) 504 | with self._device as spi: 505 | # send address w/ read request to the atm90e32 506 | spi.write(two_byte_buf) 507 | # Get the unsigned short register values sent from the atm90e32 508 | spi.readinto(results_buf) 509 | return struct.unpack('>H', results_buf)[0] 510 | ###################################################### 511 | # Write the val to the address. 512 | ###################################################### 513 | 514 | def write(self, address, val): 515 | # pack the address into a the bytearray. It is an unsigned short(H) 516 | # that needs to be in MSB(>) 517 | 518 | four_byte_buf = bytearray(4) 519 | struct.pack_into('>H', four_byte_buf, 0, address) 520 | struct.pack_into('>H', four_byte_buf, 2, val) 521 | with self._device as spi: 522 | spi.write(four_byte_buf) 523 | ###################################################### 524 | # Verify the right. 525 | ###################################################### 526 | 527 | def verify(self, address, val): 528 | if address == R.SoftReset: 529 | return True 530 | result = self.read(R.LastSPIData) 531 | return True if result == val else False 532 | 533 | ###################################################### 534 | # read or write to spi. 535 | # Writes are verified. If verification shows our 536 | # write didn't work, we raise an exception. 537 | ###################################################### 538 | 539 | def _spi_rw(self, rw, address, val): 540 | """read or write to spi. 541 | Writes are verified. If verification shows our 542 | write didn't work, we raise an exception. 543 | 544 | :param rw: 1 if read, 0 if write. 545 | :param address: One of the registers identified 546 | in atm90_e32_registers.py. 547 | :param val: The two byte hex value to write to the 548 | register (assumes rw = 0). 549 | """ 550 | 551 | if(rw): # read 552 | return self.read(address) 553 | else: 554 | self.write(address, val) 555 | if not self.verify(address, val): 556 | e = f'EXCEPTION: Write to address 0x{address:02x} Failed.' 557 | raise RegistryWriteError(e) 558 | ###################################################### 559 | 560 | def _round_number(self, f_num): 561 | if f_num - math.floor(f_num) < 0.5: 562 | return math.floor(f_num) 563 | return math.ceil(f_num) 564 | 565 | ######################################################################### 566 | def _read32Register(self, regh_addr, regl_addr): 567 | val_h = self._spi_rw(SPI_READ, regh_addr, 0xFFFF) 568 | val_l = self._spi_rw(SPI_READ, regl_addr, 0xFFFF) 569 | val = val_h << 16 570 | val |= val_l # concatenate the 2 registers to make 1 32 bit number 571 | if ((val & 0x80000000) != 0): # if val is negative, 572 | val = (0xFFFFFFFF - val) + 1 # 2s compliment + 1 573 | return (val) 574 | 575 | 576 | if __name__ == "__main__": 577 | print('hello') 578 | sys.exit() 579 | --------------------------------------------------------------------------------