├── .gitignore ├── LICENSE ├── ModbusWrapperClient.py ├── README.md ├── SchneiderElectric_iEM3255.py ├── configs ├── Map-Schneider-iEM3255.csv └── logging.json ├── logmanagement.py ├── main.py ├── requirements.txt └── settings.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # IPython Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # dotenv 79 | .env 80 | 81 | # virtualenv 82 | venv/ 83 | ENV/ 84 | 85 | # Spyder project settings 86 | .spyderproject 87 | 88 | # Rope project settings 89 | .ropeproject 90 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Luca Berton 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 | -------------------------------------------------------------------------------- /ModbusWrapperClient.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # coding: utf-8 3 | # Copyright 2017 Luca Berton 4 | __author__ = 'lucab85' 5 | 6 | import sys 7 | 8 | from pymodbus.constants import Endian 9 | from pymodbus.client.sync import ModbusSerialClient, ModbusTcpClient 10 | from pymodbus.payload import BinaryPayloadDecoder, BinaryPayloadBuilder 11 | from settings import MODBUS_CONNECTIONRETRY 12 | import unittest 13 | 14 | from logmanagement import logmanagement 15 | log = logmanagement.getlog('ModbusWrapperClient', 'utils').getLogger() 16 | 17 | class ModbusWrapperClient(): 18 | def __init__(self, modbusUnitAddress, maxRegsRead, modbusTimeout, endian="little"): 19 | self.client = None 20 | self.modbusAddress = modbusUnitAddress 21 | self.bufferStart = 0 22 | self.bufferEnd = 0 23 | self.data_buffer = None 24 | self.maxRegsRead = maxRegsRead 25 | self.timeout = modbusTimeout 26 | if endian == 'auto': 27 | self.endian = Endian.Auto 28 | elif endian == 'little': 29 | self.endian = Endian.Little 30 | else: 31 | self.endian = Endian.Big 32 | self.isConnected = False 33 | self.validaddresses = None 34 | self.validaddresses_write = None 35 | 36 | def openConnectionSerial(self, modbusSerialPort, modbusMethon, modbusByte, modbusStopBits, modbusParity, 37 | modbusBaudrate, modbusTimeout): 38 | self.client = ModbusSerialClient(method=modbusMethon, port=modbusSerialPort, stopbits=modbusStopBits, 39 | bytesize=modbusByte, parity=modbusParity, baudrate=modbusBaudrate, 40 | timeout=modbusTimeout) 41 | self.tryReconnect() 42 | 43 | def openConnectionTCP(self, modbusHost, modbusPort): 44 | self.client = ModbusTcpClient(modbusHost, modbusPort) 45 | self.tryReconnect() 46 | 47 | def closeConnection(self): 48 | if self.isConnected is True: 49 | self.client.close() 50 | 51 | def tryReconnect(self): 52 | retry = MODBUS_CONNECTIONRETRY+1 53 | for i in range(1, retry): 54 | if self.isConnected is False: 55 | self.isConnected = self.client.connect() 56 | break 57 | log.debug("riconessione %s/%s" % (i, retry)) 58 | 59 | def load_valid_addresses(self, lista=None): 60 | log.debug("load_valid_addresses") 61 | self.validaddresses = lista 62 | 63 | def check_address(self, address): 64 | if self.validaddresses is None: 65 | return True 66 | ret = True if (address in self.validaddresses) else False 67 | return ret 68 | 69 | def readRegisters(self, address, count, mb_type='uint16', mb_funcall=3, force=False): 70 | if self.isConnected is False: 71 | self.tryReconnect() 72 | tmp = None 73 | if (self.check_address(address) is True) or (force is True): 74 | try: 75 | if mb_funcall == 1: 76 | # Read Coil Status (FC=01) 77 | result = self.client.read_coils(address, count=count, unit=self.modbusAddress) 78 | tmp = result.bits 79 | elif mb_funcall == 2: 80 | # Read Dscrete Input (FC=02) 81 | result = self.client.read_discrete_inputs(address, count=count, unit=self.modbusAddress) 82 | tmp = result.bits 83 | elif mb_funcall == 3: 84 | # Read Holding Registers (FC=03) 85 | result = self.client.read_holding_registers(address, count=count, unit=self.modbusAddress) 86 | if result != None: 87 | tmp = result.registers 88 | elif mb_funcall == 4: 89 | # Read Input Registers (FC=04) 90 | result = self.client.read_input_registers(address, count=count, unit=self.modbusAddress) 91 | #tmp = result.bits 92 | if result != None: 93 | tmp = result.registers 94 | #log.debug("out: %s" % tmp) 95 | else: 96 | log.debug("Function call not supported: %s" % mb_funcall) 97 | result = None 98 | tmp = result 99 | except Exception as e: 100 | log.exception(e) 101 | return tmp 102 | 103 | def check_address_write(self, address): 104 | if self.validaddresses_write is None: 105 | return True 106 | ret = True if (address in self.validaddresses_write) else False 107 | return ret 108 | 109 | 110 | def writeRegisters(self, address, value, mb_funcall=5, force=False, skip_encode=False): 111 | # Refer to "libmodbus" C library: http://libmodbus.org/documentation/ 112 | # log.info('writeRegisters(address="%s", value="%s", mb_funcall="%s"' % (address, value, mb_funcall)) 113 | if self.isConnected is False: 114 | self.tryReconnect() 115 | result = None 116 | if (self.check_address_write(address) is True) or (force is True): 117 | try: 118 | if mb_funcall == 5: 119 | # Single Coil (FC=05) => modbus_write_bit 120 | result = self.client.write_coil(address, value, unit=self.modbusAddress) 121 | elif mb_funcall == 6: 122 | # Single Register (FC=06) 123 | result = self.client.write_register(address, value, unit=self.modbusAddress, skip_encode=skip_encode) 124 | elif mb_funcall == 15: 125 | # Multiple Coils (FC=15) => modbus_write_bits 126 | result = self.client.write_coils(address, value, unit=self.modbusAddress) 127 | elif mb_funcall == 16: 128 | # Multiple Registers (FC=16) 129 | result = self.client.write_registers(address, value, unit=self.modbusAddress, skip_encode=skip_encode) 130 | else: 131 | log.warn("Function call not supported: %s" % mb_funcall) 132 | except Exception as e: 133 | log.exception(e) 134 | return result 135 | 136 | def encode_field(self, value, mb_type='unit16'): 137 | builder = BinaryPayloadBuilder(endian=self.endian) 138 | if mb_type == 'bit' or mb_type == 'bits': 139 | builder.add_bits(value) 140 | elif mb_type == 'uint8': 141 | builder.add_8bit_uint(value) 142 | elif mb_type == 'uint16': 143 | builder.add_16bit_uint(value) 144 | elif mb_type == 'uint32': 145 | builder.add_32bit_uint(value) 146 | elif mb_type == 'uint64': 147 | builder.add_64bit_uint(value) 148 | elif mb_type == 'int8': 149 | builder.add_8bit_int(value) 150 | elif mb_type == 'int16': 151 | builder.add_16bit_int(value) 152 | elif mb_type == 'int32': 153 | builder.add_32bit_int(value) 154 | elif mb_type == 'int64': 155 | builder.add_64bit_int(value) 156 | elif mb_type == 'float32': 157 | builder.add_32bit_float(value) 158 | elif mb_type == 'float64': 159 | builder.add_64bit_float(value) 160 | elif mb_type == 'string' or mb_type == 'str': 161 | builder.add_string(value) 162 | else: 163 | log.warn('Not supported DataType: "%s"' % mb_type) 164 | return builder.build() 165 | 166 | def readRegistersAndDecode(self, registri, counter, mb_type='uint16', mb_funcall=3, force=False): 167 | tmp = None 168 | if (self.check_address(registri) is True) or (force is True): 169 | ret = self.readRegisters(registri, counter, mb_type, mb_funcall, force) 170 | if ret is not None: 171 | tmp = self.decode(ret, counter, mb_type, mb_funcall) 172 | return tmp 173 | 174 | def decode(self, raw, size, mb_type, mb_funcall=3): 175 | log.debug('decode param (raw=%s, size=%s, mb_type=%s, mb_funcall=%s)' % (raw, size, mb_type, mb_funcall)) 176 | if mb_funcall == 1: 177 | # Read Coil Status (FC=01) 178 | log.debug("decoder FC1 (raw: %s)" % raw) 179 | decoder = BinaryPayloadDecoder.fromCoils(raw, endian=self.endian) 180 | elif mb_funcall == 2: 181 | # Read Discrete Input (FC=02) 182 | log.debug("decoder FC2 (raw: %s)" % raw) 183 | decoder = BinaryPayloadDecoder(raw, endian=self.endian) 184 | elif mb_funcall == 3: 185 | # Read Holding Registers (FC=03) 186 | log.debug("decoder FC3 (raw: %s)" % raw) 187 | decoder = BinaryPayloadDecoder.fromRegisters(raw, endian=self.endian) 188 | elif mb_funcall == 4: 189 | # Read Input Registers (FC=04) 190 | log.debug("decoder stub FC4 (raw: %s)" % raw) 191 | decoder = BinaryPayloadDecoder(raw, endian=self.endian) 192 | else: 193 | log.debug("Function call not supported: %s" % mb_funcall) 194 | decoder = None 195 | 196 | result = "" 197 | if mb_type == 'bitmap': 198 | if size == 1: 199 | mb_type = 'int8' 200 | elif size == 2: 201 | mb_type = 'int16' 202 | elif size == 2: 203 | mb_type = 'int32' 204 | elif size == 4: 205 | mb_type = 'int64' 206 | 207 | if decoder is None: 208 | log.debug("decode none") 209 | result = raw 210 | else: 211 | try: 212 | if mb_type == 'string' or mb_type == 'utf8': 213 | result = decoder.decode_string(size) 214 | #elif mb_type == 'bitmap': 215 | # result = decoder.decode_string(size) 216 | elif mb_type == 'datetime': 217 | result = decoder.decode_string(size) 218 | elif mb_type == 'uint8': 219 | result = int(decoder.decode_8bit_uint()) 220 | elif mb_type == 'int8': 221 | result = int(decoder.decode_8bit_int()) 222 | elif mb_type == 'uint16': 223 | result = int(decoder.decode_16bit_uint()) 224 | elif mb_type == 'int16': 225 | result = int(decoder.decode_16bit_int()) 226 | elif mb_type == 'uint32': 227 | result = int(decoder.decode_32bit_uint()) 228 | elif mb_type == 'int32': 229 | result = int(decoder.decode_32bit_int()) 230 | elif mb_type == 'uint64': 231 | result = int(decoder.decode_64bit_uint()) 232 | elif mb_type == 'int64': 233 | result = int(decoder.decode_64bit_int()) 234 | elif mb_type == 'float32' or mb_type == 'float': 235 | result = float(decoder.decode_32bit_float()) 236 | elif mb_type == 'float64': 237 | result = float(decoder.decode_64bit_float()) 238 | elif mb_type == 'bit': 239 | result = int(decoder.decode_bits()) 240 | elif mb_type == 'bool': 241 | result = bool(raw[0]) 242 | elif mb_type == 'raw': 243 | result = raw[0] 244 | else: 245 | result = raw 246 | except ValueError as e: 247 | log.exception(e) 248 | result = raw 249 | return result 250 | 251 | def read1(self, startreg, mb_type, mb_funcall=3): 252 | return self.readRegisters(startreg, 1, mb_type, mb_funcall) 253 | 254 | def read2(self, startreg, mb_type, mb_funcall=3): 255 | return self.readRegisters(startreg, 2, mb_type, mb_funcall) 256 | 257 | def read3(self, startreg, mb_type, mb_funcall=3): 258 | return self.readRegisters(startreg, 3, mb_type, mb_funcall) 259 | 260 | def read4(self, startreg, mb_type, mb_funcall=3): 261 | return self.readRegisters(startreg, 4, mb_type, mb_funcall) 262 | 263 | def buffer_print(self): 264 | if not self.bufferReady(): 265 | log.debug('BUFFER empty ---') 266 | else: 267 | text = 'BUFFER [%s-%s]: ' % (self.bufferStart, self.bufferEnd) 268 | i = self.bufferStart 269 | for item in self.data_buffer: 270 | text += "%s(%s) " % (i, item) 271 | i += 1 272 | log.debug(text) 273 | 274 | def bufferedReadRegisters(self, startreg, counter, mb_type='uint16', mb_funcall=3): 275 | log.debug('bufferedReadRegisters param (startreg=%s, counter=%s, mb_type=%s, mb_funcall=%s)' % 276 | (startreg, counter, mb_type, mb_funcall)) 277 | 278 | valido = False 279 | offset = self.maxRegsRead 280 | while (offset >= 0) and (valido != True): 281 | valido = self.check_address(startreg + offset) 282 | if valido is True: 283 | self.data_buffer = self.readRegisters(startreg, offset, mb_type, mb_funcall) 284 | if self.data_buffer != None: 285 | self.bufferStart = startreg 286 | self.bufferEnd = startreg + len(self.data_buffer) - 1 287 | offset -= 1 288 | 289 | self.buffer_print() 290 | return self.bufferReady() 291 | 292 | def bufferReady(self): 293 | return True if (self.data_buffer is not None) else False 294 | 295 | def bufferCleanup(self): 296 | if self.bufferReady(): 297 | self.data_buffer = None 298 | 299 | def inBuffer(self, startreg, conteggio): 300 | if not self.bufferReady(): 301 | return False 302 | return True if ( 303 | (startreg >= self.bufferStart) and ((startreg + conteggio) <= self.bufferEnd)) else False 304 | 305 | def cachedRead(self, startreg, counter, mb_type='uint16', mb_funcall=3): 306 | log.debug('cachedRead param (startreg=%s, counter=%s, mb_type=%s, mb_funcall=%s)' % 307 | (startreg, counter, mb_type, mb_funcall)) 308 | if not self.bufferReady(): 309 | self.bufferedReadRegisters(startreg, counter, mb_type, mb_funcall) 310 | if not self.inBuffer(startreg, counter): 311 | self.bufferedReadRegisters(startreg, counter, mb_type, mb_funcall) 312 | regs = [] 313 | i = 0 314 | while i < counter: 315 | regs.append(self.data_buffer[startreg - self.bufferStart + i]) 316 | i += 1 317 | return self.decode(regs, counter, mb_type, mb_funcall) 318 | 319 | def cachedRead1(self, startreg, mb_type='uint16', mb_funcall=3): 320 | if not self.bufferReady(): 321 | self.bufferedReadRegisters(startreg, 1, mb_type, mb_funcall) 322 | if not self.inBuffer(startreg, 1): 323 | self.bufferedReadRegisters(startreg, 1, mb_type, mb_funcall) 324 | return self.decode(self.data_buffer[startreg - self.bufferStart], 1, mb_type) 325 | 326 | def cachedRead2(self, startreg, mb_type='uint16', mb_funcall=3): 327 | if not self.bufferReady(): 328 | self.bufferedReadRegisters(startreg, 2, mb_type, mb_funcall) 329 | if not self.inBuffer(startreg, 2): 330 | self.bufferedReadRegisters(startreg, 2, mb_type, mb_funcall) 331 | 332 | regs = [] 333 | regs.append(self.data_buffer[startreg - self.bufferStart]) 334 | regs.append(self.data_buffer[startreg - self.bufferStart + 1]) 335 | return self.decode(regs, 2, mb_type) 336 | 337 | def cachedRead3(self, startreg, mb_type='uint16', mb_funcall=3): 338 | if not self.bufferReady(): 339 | self.bufferedReadRegisters(startreg, 3, mb_type, mb_funcall) 340 | if not self.inBuffer(startreg, 3): 341 | self.bufferedReadRegisters(startreg, 3, mb_type, mb_funcall) 342 | 343 | regs = [] 344 | regs.append(self.data_buffer[startreg - self.bufferStart]) 345 | regs.append(self.data_buffer[startreg - self.bufferStart + 1]) 346 | regs.append(self.data_buffer[startreg - self.bufferStart + 2]) 347 | return self.decode(regs, 3, mb_type) 348 | 349 | def cachedRead4(self, startreg, mb_type='uint16', mb_funcall=3): 350 | if not self.bufferReady(): 351 | self.bufferedReadRegisters(startreg, 4, mb_type, mb_funcall) 352 | if not self.inBuffer(startreg, 4): 353 | self.bufferedReadRegisters(startreg, 4, mb_type, mb_funcall) 354 | 355 | regs = [] 356 | regs.append(self.data_buffer[startreg - self.bufferStart]) 357 | regs.append(self.data_buffer[startreg - self.bufferStart + 1]) 358 | regs.append(self.data_buffer[startreg - self.bufferStart + 2]) 359 | regs.append(self.data_buffer[startreg - self.bufferStart + 3]) 360 | return self.decode(regs, 4, mb_type) 361 | 362 | 363 | class TestModbusWrapperClientTest(unittest.TestCase): 364 | def test_bufferReady(self): 365 | mb = ModbusWrapperClient(1, 103, 2) 366 | self.assertEqual(mb.bufferReady(), False) 367 | 368 | 369 | if __name__ == '__main__': 370 | unittest.main() 371 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PowerMeter Reader 2 | 3 | Python code to read energetic usage data from a modbus connected 4 | PowerMeter device. 5 | 6 | ![PowerMeter Image](http://www.schneider-electric.com/en/product-image/238723-acti-9-iem3000) 7 | 8 | Tested with device [Schneider Electric iEM3255](http://www.schneider-electric.com/en/product-range/61273-acti-9-iem3000/) (Acti 9 iEM 3000 series - code _A9MEM3255_) 9 | 10 | ## List of files 11 | 12 | * main 13 | Star class file 14 | 15 | * SchneiderElectric_iEM3255 16 | Class to read data from the device 17 | 18 | * ModbusWrapperClient 19 | Wrapper of pymodbus library with buffering support 20 | 21 | * logmanagement 22 | Simple log manager to save logs under 'logs/' 23 | 24 | * settings.py 25 | Configuration variables (device IP etc.) 26 | 27 | * configs/ 28 | 29 | * logging.json 30 | Logging settings 31 | 32 | * Map-Schneider-iEM3255.csv 33 | Modbus registry map file 34 | -------------------------------------------------------------------------------- /SchneiderElectric_iEM3255.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # coding: utf-8 3 | # Copyright 2017 Luca Berton 4 | __author__ = 'lucab85' 5 | 6 | import sys 7 | 8 | from ModbusWrapperClient import ModbusWrapperClient 9 | from settings import PATH_PM_SCHNEIDERELECTRICIEM3255, PM_settings, PM_config 10 | import math 11 | import datetime 12 | 13 | from logmanagement import logmanagement 14 | log = logmanagement.getlog('SchneiderElectriciEM3255', 'powermeter').getLogger() 15 | 16 | class SchneiderElectric_iEM3255(): 17 | def __init__(self, PM_modbusHost, PM_modbusPort, PM_modbusAddress, PM_startReg, PM_maxRegsRead, 18 | PM_timeout, PM_endian, PM_modbusAddressoffset=0, PM_cacheEnabled=False, PM_base_commands=0): 19 | self.mb = ModbusWrapperClient(PM_modbusAddress, PM_maxRegsRead, PM_timeout, PM_endian) 20 | self.PM_addressoffset = PM_modbusAddressoffset 21 | self.mb.openConnectionTCP(modbusHost=PM_modbusHost, modbusPort=PM_modbusPort) 22 | self.PM_getData_start = PM_startReg 23 | self.PM_getData_count = PM_maxRegsRead 24 | self.PM_cacheEnabled = PM_cacheEnabled 25 | self.PM_base_commands = PM_base_commands 26 | 27 | self.load_modbus_map() 28 | valid_addresses = self.elaborate_validAddresses() 29 | log.debug("valid_addresses: %s" % valid_addresses) 30 | self.mb.load_valid_addresses(valid_addresses) 31 | self.buffer_skip = [] 32 | self.data_default = [] 33 | 34 | def load_modbus_map(self): 35 | filename = PATH_PM_SCHNEIDERELECTRICIEM3255 36 | self.modbusmap = {} 37 | i = 0 38 | separator = ";" 39 | with open(filename) as f: 40 | for line in f: 41 | line = line.rstrip() 42 | line.replace("'", "") 43 | if separator not in line: 44 | continue 45 | if line.startswith("#"): 46 | continue 47 | if line.startswith(separator): 48 | continue 49 | key, address, size, datatype = line.split(separator) 50 | try: 51 | self.modbusmap.setdefault(key, []) 52 | self.modbusmap[key].append(int(address) + self.PM_addressoffset) 53 | self.modbusmap[key].append(int(size)) 54 | self.modbusmap[key].append(datatype) 55 | except ValueError: 56 | pass 57 | i += 1 58 | 59 | text = "[" 60 | validaddresses = [] 61 | for k in self.modbusmap.keys(): 62 | if len(self.modbusmap[k]) == 3: 63 | text += "'%s => %s,%s,%s', " % (k, self.modbusmap[k][0], self.modbusmap[k][1], self.modbusmap[k][2]) 64 | if self.modbusmap[k][0] not in validaddresses: 65 | validaddresses.append(self.modbusmap[k][0]) 66 | else: 67 | text += "**SKIP** item \"%s\"" % k 68 | text += "]" 69 | log.debug("Read %s modbus registers: %s" % (i, text)) 70 | 71 | def elaborate_validAddresses(self): 72 | addresses = [] 73 | log.debug("modbusmap: %s" % self.modbusmap) 74 | for k in self.modbusmap.keys(): 75 | address = self.modbusmap[k][0] 76 | i = 0 77 | size = self.modbusmap[k][1] 78 | while i <= size: 79 | address = address + i 80 | if address not in addresses: 81 | addresses.append(address) 82 | i += 1 83 | return sorted(addresses) 84 | 85 | def _modbusRead(self, key): 86 | if self.PM_cacheEnabled is False: 87 | # W/O cache 88 | val = self.mb.readRegistersAndDecode(self.modbusmap[key][0],self.modbusmap[key][1],self.modbusmap[key][2]) 89 | else: 90 | # with cache 91 | val = self.mb.cachedRead(self.modbusmap[key][0], self.modbusmap[key][1], self.modbusmap[key][2]) 92 | log.debug('"%s" Modbus: (%s,%s,%s) = %s' % (key, self.modbusmap[key][0], 93 | self.modbusmap[key][1], self.modbusmap[key][2], val)) 94 | if self.modbusmap[key][2].startswith("float"): 95 | try: 96 | if math.isnan(val): 97 | log.debug("NaN regs %s => 0" % self.modbusmap[key][0]) 98 | val = 0 99 | except TypeError: 100 | val = 0 101 | return val 102 | 103 | def readL1Active(self): 104 | # *1000 kW => W 105 | return self._modbusRead('L1Active')*1000 106 | 107 | def readL2Active(self): 108 | # *1000 kW => W 109 | return self._modbusRead('L2Active')*1000 110 | 111 | def readL3Active(self): 112 | # *1000 kW => W 113 | return self._modbusRead('L3Active')*1000 114 | 115 | def readL1Current(self): 116 | return self._modbusRead('L1Current') 117 | 118 | def readL2Current(self): 119 | return self._modbusRead('L2Current') 120 | 121 | def readL3Current(self): 122 | return self._modbusRead('L3Current') 123 | 124 | def readL1Voltage(self): 125 | return self._modbusRead('L1Voltage') 126 | 127 | def readL2Voltage(self): 128 | return self._modbusRead('L2Voltage') 129 | 130 | def readL3Voltage(self): 131 | return self._modbusRead('L3Voltage') 132 | 133 | def readFreq(self): 134 | return self._modbusRead('FREQ') 135 | 136 | def readDinput(self): 137 | Dinput = self._modbusRead('Digital Input Status') 138 | 139 | def readL1Apparent(self, L1Voltage=None, L1Current=None): 140 | if L1Voltage is None: 141 | L1Voltage = self.readL1Voltage() 142 | if L1Current is None: 143 | L1Current = self.readL1Current() 144 | return L1Voltage * L1Current 145 | 146 | def readL2Apparent(self, L2Voltage=None, L2Current=None): 147 | if L2Voltage is None: 148 | L2Voltage = self.readL2Voltage() 149 | if L2Current is None: 150 | L2Current = self.readL2Current() 151 | return L2Voltage * L2Current 152 | 153 | def readL3Apparent(self, L3Voltage=None, L3Current=None): 154 | if L3Voltage is None: 155 | L3Voltage = self.readL3Voltage() 156 | if L3Current is None: 157 | L3Current = self.readL3Current() 158 | return L3Voltage * L3Current 159 | 160 | def readL1CosPhi(self, L1Voltage=None, L1C=None, L1AC=None): 161 | if L1Voltage is None: 162 | L1Voltage = self.readL1Voltage() 163 | if L1C is None: 164 | L1C = self.readL1Current() 165 | if L1AC is None: 166 | L1AC = self.readL1Active() 167 | VI = L1Voltage * L1C 168 | if VI == 0: 169 | return 0 170 | return L1AC / VI 171 | 172 | def readL2CosPhi(self, L2Voltage=None, L2C=None, L2AC=None): 173 | if L2Voltage is None: 174 | L2Voltage = self.readL2Voltage() 175 | if L2C is None: 176 | L2C = self.readL2Current() 177 | if L2AC is None: 178 | L2AC = self.readL2Active() 179 | VI = L2Voltage * L2C 180 | if VI == 0: 181 | return 0 182 | return L2AC / VI 183 | 184 | def readL3CosPhi(self, L3Voltage=None, L3C=None, L3AC=None): 185 | if L3Voltage is None: 186 | L3Voltage = self.readL3Voltage() 187 | if L3C is None: 188 | L3C = self.readL3Current() 189 | if L3AC is None: 190 | L3AC = self.readL3Active() 191 | VI = L3Voltage * L3C 192 | if VI == 0: 193 | return 0 194 | return L3AC / VI 195 | 196 | def readL1Reactive(self): 197 | V = float(self.readL1Voltage()) 198 | I = float(self.readL1Current()) 199 | return math.sqrt(math.pow((V*I), 2) - math.pow(self.readL1Active(), 2)) 200 | 201 | def readL2Reactive(self): 202 | V = float(self.readL2Voltage()) 203 | I = float(self.readL2Current()) 204 | return math.sqrt(math.pow((V*I), 2) - math.pow(self.readL2Active(), 2)) 205 | 206 | def readL3Reactive(self): 207 | V = float(self.readL3Voltage()) 208 | I = float(self.readL3Current()) 209 | return math.sqrt(math.pow((V*I), 2) - math.pow(self.readL3Active(), 2)) 210 | 211 | def readActiveEnergy(self): 212 | return self._modbusRead('PartialActiveEnergy') 213 | 214 | def readReactiveEnergy(self): 215 | return self._modbusRead('PartialReactiveEnergy') 216 | 217 | def readTotalPowerFactor(self): 218 | return self._modbusRead('TotalPowerFactor') 219 | 220 | def _capacitiveOrInductive(self): 221 | tpf = self.readTotalPowerFactor() 222 | if tpf < -1: 223 | return "-C" 224 | elif tpf >= -1 and tpf < 0: 225 | return "-I" 226 | elif tpf >= 0 and tpf < 1: 227 | return "+I" 228 | elif tpf >= 1: 229 | return "+C" 230 | 231 | def readInductiveEnergy(self): 232 | return self.readReactiveEnergy() 233 | 234 | def readCapacitiveEnergy(self): 235 | return self.readReactiveEnergy() 236 | 237 | def readTotalActivePW(self): 238 | # *1000 kW => W 239 | return self._modbusRead('TotalActivePW')*1000 240 | 241 | def readTotalApparentPW(self): 242 | # *1000 kVA => VA 243 | return self._modbusRead('TotalApparentPW')*1000 244 | 245 | def bysec_value(self, x, size=16): 246 | val_shift = size/2 247 | if size == 8: 248 | val_mask = 0xf 249 | else: 250 | val_mask = 0xff 251 | 252 | c = (x >> val_shift) & val_mask 253 | f = x & val_mask 254 | return c, f 255 | 256 | def read_date_time(self): 257 | date = None 258 | year = 2000 + int(self._modbusRead("YYYY")) 259 | month, tmp = self.bysec_value(self._modbusRead("MM-WW-GG")) 260 | weekday, day = self.bysec_value(tmp, 8) 261 | hours, minutes = self.bysec_value(self._modbusRead("HH-MM")) 262 | seconds = int(self._modbusRead("MS")) / 1000 263 | 264 | log.warn("read YYYY=%s MM=%s DD=%s HH=%s MM=%s SS=%s" % (year, month, day, hours, minutes, seconds)) 265 | date = datetime.datetime(year, month, day, hours, minutes, seconds) 266 | log.warn("data: %s" % date) 267 | return date 268 | 269 | def _modbusWrite(self, command, base_address, value, mb_funcall, force=False): 270 | log.debug('CMD: %s Modbus (FC=%s): %s = %s' % (command, mb_funcall, base_address, value)) 271 | skip_encode = True 272 | val = self.mb.writeRegisters((base_address + self.PM_addressoffset), value, mb_funcall, force, skip_encode=skip_encode) 273 | log.debug('Write done: %s' % val) 274 | val = self.mb.readRegisters(address=(5375 + self.PM_addressoffset), count=2, mb_type='int16', mb_funcall=3, force=True) 275 | log.debug("result: %s" % val) 276 | if val[1] == 0: 277 | log.warn("!!! Command SUCCESSFUL !!!") 278 | elif val[1] == 3000: 279 | log.warn("INVALID Command or COM.PROTECTION") 280 | elif val[1] == 3001: 281 | log.warn("INVALID Parameter") 282 | elif val[1] == 3002: 283 | log.warn("INVALID number of parameters") 284 | elif val[1] == 3007: 285 | log.warn("Operation Not Performed") 286 | else: 287 | log.warn('NOT valid value: "%s"' % val[1]) 288 | return val[1] 289 | 290 | def encode_data(self, payload): 291 | payload_processed = [] 292 | for i in payload: 293 | try: 294 | value = self.mb.encode_field(i[0], i[1]) 295 | for item in value: 296 | payload_processed.append(item) 297 | if len(value) > 1: 298 | log.debug("multibyte %s => %s" % (i, value)) 299 | except Exception as e: 300 | log.exception(e) 301 | return payload_processed 302 | 303 | def cmd_set_date_time(self, key='Set DateTime', data=datetime.datetime.today()): 304 | log.debug('Start "cmd_set_date_time" routine: %s %s' % (PM_settings[key], data)) 305 | payload = [ 306 | [int(PM_settings[key]['command']), 'int16'], 307 | [0, 'int16'], 308 | [int(data.year), 'int16'], 309 | [int(data.month), 'int16'], 310 | [int(data.day), 'int16'], 311 | [int(data.hour), 'int16'], 312 | [int(data.minute), 'int16'], 313 | [int(data.second), 'int16'], 314 | [0, 'int16'], 315 | ] 316 | payload_processed = self.encode_data(payload) 317 | ret = self._modbusWrite(command=PM_settings[key],base_address=self.PM_base_commands, value=payload_processed, mb_funcall=16, force=True) 318 | log.debug('End "cmd_set_date_time" routine: %s' % ret) 319 | 320 | def cmd_set_wiring(self, key='Set Wiring'): 321 | log.debug('Start "cmd_set_wiring" routine: %s' % PM_settings[key]) 322 | payload = [ 323 | [int(PM_settings[key]['command']), 'int16'], 324 | [0, 'uint16'], 325 | [0, 'uint16'], 326 | [0, 'uint16'], 327 | [int(PM_settings[key]['Power System Configuration']), 'uint16'], 328 | [int(PM_settings[key]['Nominal Frequency']), 'uint16'], 329 | [0, 'float32'], 330 | [0, 'float32'], 331 | [0, 'float32'], 332 | [0, 'uint16'], 333 | [0, 'uint16'], 334 | [float(PM_settings[key]['VT Primary']), 'float32'], 335 | [int(PM_settings[key]['VT Secondary']), 'uint16'], 336 | [int(PM_settings[key]['Number of CTs']), 'uint16'], 337 | [int(PM_settings[key]['CT Primary']), 'uint16'], 338 | [int(PM_settings[key]['CT Secondary']), 'uint16'], 339 | [0, 'uint16'], 340 | [0, 'uint16'], 341 | [0, 'uint16'], 342 | [0, 'uint16'], 343 | [int(PM_settings[key]['VT Connection type']), 'uint16'] 344 | ] 345 | payload_processed = self.encode_data(payload) 346 | ret = self._modbusWrite(command=PM_settings[key]['command'],base_address=self.PM_base_commands, value=payload_processed, mb_funcall=16, force=True) 347 | log.debug('End "cmd_set_wiring" routine: %s' % ret) 348 | 349 | def cmd_set_pulse_output(self, key='Set Pulse Output'): 350 | log.debug('Start "cmd_set_pulse_output" routine: %s' % PM_settings[key]) 351 | payload = [ 352 | [int(PM_settings[key]['command']), 'int16'], 353 | [0, 'uint16'], 354 | [0, 'uint16'], 355 | [int(PM_settings[key]['Pulse Output enable']), 'uint16'], 356 | [float(PM_settings[key]['Pulse constant']), 'float32'], 357 | [0, 'uint16'], 358 | [0, 'uint16'], 359 | [float(0), 'float32'], 360 | [0, 'uint16'], 361 | [0, 'uint16'], 362 | [float(0), 'float32'], 363 | ] 364 | payload_processed = self.encode_data(payload) 365 | ret = self._modbusWrite(command=PM_settings[key]['command'],base_address=self.PM_base_commands, value=payload_processed, mb_funcall=16, force=True) 366 | 367 | payload = [ 368 | [int(PM_settings[key]['command2']), 'int16'], 369 | [0, 'uint16'], 370 | [0, 'uint16'], 371 | [int(PM_settings[key]['Pulse width']), 'uint16'], 372 | ] 373 | payload_processed = self.encode_data(payload) 374 | ret2 = self._modbusWrite(command=PM_settings[key]['command2'],base_address=self.PM_base_commands, value=payload_processed, mb_funcall=16, force=True) 375 | log.debug('End "cmd_set_pulse_output" routine: %s %s' % (ret, ret2)) 376 | 377 | def cmd_set_tariff(self, key='Set Tariff'): 378 | log.debug('Start "cmd_set_tariff" routine: %s' % PM_settings[key]) 379 | payload = [ 380 | [int(PM_settings[key]['command']), 'int16'], 381 | [0, 'uint16'], 382 | [int(PM_settings[key]['Multi Tariff Mode']), 'uint16'] 383 | ] 384 | payload_processed = self.encode_data(payload) 385 | ret = self._modbusWrite(command=PM_settings[key]['command'],base_address=self.PM_base_commands, value=payload_processed, mb_funcall=16, force=True) 386 | payload = [ 387 | [int(PM_settings[key]['command2']), 'int16'], 388 | [0, 'uint16'], 389 | [int(PM_settings[key]['Tariff']), 'uint16'] 390 | ] 391 | payload_processed = self.encode_data(payload) 392 | ret2 = self._modbusWrite(command=PM_settings[key]['command'],base_address=self.PM_base_commands, value=payload_processed, mb_funcall=16, force=True) 393 | log.debug('End "cmd_set_tariff" routine: %s %s' % (ret, ret2)) 394 | 395 | def cmd_set_digital_input_as_partial_energy_reset(self, key='Set Digital Input as Partial Energy Reset'): 396 | log.debug('Start "cmd_set_digital_input_as_partial_energy_reset" routine: %s' % PM_settings[key]) 397 | payload = [ 398 | [int(PM_settings[key]['command']), 'int16'], 399 | [0, 'uint16'], 400 | [int(PM_settings[key]['Digital Input to Associate']), 'uint16'] 401 | ] 402 | payload_processed = self.encode_data(payload) 403 | ret = self._modbusWrite(command=PM_settings[key]['command'],base_address=self.PM_base_commands, value=payload_processed, mb_funcall=16, force=True) 404 | log.debug('End "cmd_set_digital_input_as_partial_energy_reset" routine: %s' % ret) 405 | 406 | def cmd_input_metering_setup(self, key='Input Metering Setup'): 407 | log.debug('Start "cmd_input_metering_setup" routine: %s' % PM_settings[key]) 408 | payload = [ 409 | [int(PM_settings[key]['command']), 'int16'], 410 | [0, 'uint16'], 411 | [int(PM_settings[key]['Input Metering Channel']), 'uint16'], 412 | [PM_settings[key]['Label'], 'str'], 413 | [float(PM_settings[key]['Pulse Weight']), 'float32'], 414 | [0, 'uint16'], 415 | [int(PM_settings[key]['Digital Input Association']), 'uint16'] 416 | ] 417 | payload_processed = self.encode_data(payload) 418 | ret = self._modbusWrite(command=PM_settings[key]['command'],base_address=self.PM_base_commands, value=payload_processed, mb_funcall=16, force=True) 419 | log.debug('End "cmd_input_metering_setup" routine: %s' % ret) 420 | 421 | def cmd_overload_alarm_setup(self, key='Overload Alarm Setup'): 422 | log.debug('Start "cmd_overload_alarm_setup" routine: %s' % PM_settings[key]) 423 | payload = [ 424 | [int(PM_settings[key]['command']), 'int16'], 425 | [0, 'uint16'], 426 | [int(PM_settings[key]['Alarm ID']), 'uint16'], 427 | [0, 'uint16'], 428 | [0, 'uint16'], 429 | [0, 'uint16'], 430 | [int(PM_settings[key]['Enabled']), 'uint16'], 431 | [float(PM_settings[key]['Pickup value']), 'float32'], 432 | [0, 'uint32'], 433 | [0, 'float32'], 434 | [0, 'uint32'], 435 | [0, 'uint16'], 436 | [0, 'uint16'], 437 | [0, 'uint16'], 438 | [0, 'uint16'] 439 | ] 440 | payload_processed = self.encode_data(payload) 441 | ret = self._modbusWrite(command=PM_settings[key]['command'],base_address=self.PM_base_commands, value=payload_processed, mb_funcall=16, force=True) 442 | payload = [ 443 | [int(PM_settings[key]['command2']), 'int16'], 444 | [0, 'uint16'], 445 | [0, 'float32'], 446 | [0, 'uint32'], 447 | [int(PM_settings[key]['Digital Output to Associate']), 'bitmap'] 448 | ] 449 | payload_processed = self.encode_data(payload) 450 | ret2 = self._modbusWrite(command=PM_settings[key]['command'],base_address=self.PM_base_commands, value=payload_processed, mb_funcall=16, force=True) 451 | payload = [ 452 | [int(PM_settings[key]['command2']), 'int16'], 453 | [0, 'uint16'] 454 | ] 455 | payload_processed = self.encode_data(payload) 456 | ret3 = self._modbusWrite(command=PM_settings[key]['command'],base_address=self.PM_base_commands, value=payload_processed, mb_funcall=16, force=True) 457 | log.debug('End "cmd_overload_alarm_setup" routine: %s %s %s' % (ret, ret2, ret3)) 458 | 459 | def cmd_communications_setup(self, key='Communications Setup'): 460 | log.debug('Start "cmd_communications_setup" routine: %s' % PM_settings[key]) 461 | payload = [ 462 | [int(PM_settings[key]['command']), 'int16'], 463 | [0, 'uint16'], 464 | [0, 'uint16'], 465 | [0, 'uint16'], 466 | [int(PM_settings[key]['Address']), 'uint16'], 467 | [int(PM_settings[key]['Baud Rate']), 'uint16'], 468 | [int(PM_settings[key]['Parity']), 'uint16'], 469 | [0, 'uint16'] 470 | ] 471 | payload_processed = self.encode_data(payload) 472 | ret = self._modbusWrite(command=PM_settings[key]['command'],base_address=self.PM_base_commands, value=payload_processed, mb_funcall=16, force=True) 473 | log.debug('End "cmd_communications_setup" routine: %s' % ret) 474 | 475 | def cmd_reset_partial_energy_counters(self, key='Reset Partial Energy Counters'): 476 | log.debug('Start "cmd_reset_partial_energy_counters" routine: %s' % PM_settings[key]) 477 | payload = [ 478 | [int(PM_settings[key]['command']), 'int16'] 479 | ] 480 | payload_processed = self.encode_data(payload) 481 | ret = self._modbusWrite(command=PM_settings[key]['command'],base_address=self.PM_base_commands, value=payload_processed, mb_funcall=16, force=True) 482 | log.debug('End "cmd_reset_partial_energy_counters" routine: %s' % ret) 483 | 484 | def cmd_reset_input_metering_counter(self, key='Reset Input Metering Counter'): 485 | log.debug('Start "cmd_reset_input_metering_counter" routine: %s' % PM_settings[key]) 486 | payload = [ 487 | [int(PM_settings[key]['command']), 'int16'] 488 | ] 489 | payload_processed = self.encode_data(payload) 490 | ret = self._modbusWrite(command=PM_settings[key]['command'],base_address=self.PM_base_commands, value=payload_processed, mb_funcall=16, force=True) 491 | log.debug('End "cmd_reset_input_metering_counter" routine: %s' % ret) 492 | 493 | -------------------------------------------------------------------------------- /configs/Map-Schneider-iEM3255.csv: -------------------------------------------------------------------------------- 1 | #chiave;Address;Size;Type 2 | Meter Name;30;20;utf8 3 | Meter Model;50;20;utf8 4 | Manufacturer;70;20;utf8 5 | Serial Number;130;2;uint32 6 | Date of Manufacture;132;4;datetime 7 | Hardware Revision;136;5;utf8 8 | Firmware Version;1637;1;uint16 9 | YYYY;1845;1;uint16 10 | MM-WW-GG;1846;1;raw 11 | HH-MM;1847;1;raw 12 | MS;1848;1;raw 13 | Meter Operation Timer;2004;2;uint32 14 | Number of Phases;2014;1;uint16 15 | Number of Wires;2015;1;uint16 16 | Power System;2016;1;uint16 17 | Nominal Frequency;2017;1;uint16 18 | Number VTs;2025;1;uint16 19 | VT Primary;2026;2;float32 20 | VT Secondary;2028;1;uint16 21 | Number CTs;2029;1;uint16 22 | CT Primary;2030;1;uint16 23 | CT Secondary;2031;1;uint16 24 | VT Connection Type;2036;1;uint16 25 | Energy Pulse Duration;2129;1;uint16 26 | Digital Output Association;2131;1;uint16 27 | Pulse Weight;2132;2;float32 28 | Protocol;6500;1;uint16 29 | Address;6501;1;uint16 30 | Baud Rate;6502;1;uint16 31 | Parity:;6503;1;uint16 32 | Input Label;7032;20;utf8 33 | Input Pulse Constant;7052;2;float32 34 | Input Digital Input Association;7055;1;uint16 35 | Digital Input Control Mode;7274;1;uint16 36 | Digital Input Status;8905;2;bitmap 37 | Digital Output Control Mode Status;9673;1;uint16 38 | L1Current;3000;2;float32 39 | L2Current;3002;2;float32 40 | L3Current;3004;2;float32 41 | L1Voltage;3028;2;float32 42 | L2Voltage;3030;2;float32 43 | L3Voltage;3032;2;float32 44 | L1Active;3054;2;float32 45 | L2Active;3056;2;float32 46 | L3Active;3058;2;float32 47 | TotalActivePW;3060;2;float32 48 | TotalReactivePW;3068;2;float32 49 | TotalApparentPW;3076;2;float32 50 | TotalPowerFactor;3084;2;float32 51 | FREQ;3110;2;float32 52 | TotalActiveEnergy;3204;4;int64 53 | TotalReactiveEnergy;3220;4;int64 54 | PartialActiveEnergy;3256;4;int64 55 | PartialReactiveEnergy;3272;4;int64 56 | -------------------------------------------------------------------------------- /configs/logging.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "disable_existing_loggers": false, 4 | "formatters": { 5 | "simple": { 6 | "format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s" 7 | }, 8 | "detailed": { 9 | "format": "%(asctime)s %(name)s:%(levelname)s %(module)s:%(lineno)d: %(message)s" 10 | } 11 | }, 12 | 13 | "handlers": { 14 | "handler_console": { 15 | "class": "logging.StreamHandler", 16 | "level": "DEBUG", 17 | "formatter": "simple", 18 | "stream": "ext://sys.stdout" 19 | }, 20 | "handler_file_errors": { 21 | "class": "logging.handlers.RotatingFileHandler", 22 | "formatter": "detailed", 23 | "filename": "logs/errors.log", 24 | "maxBytes": 10485760, 25 | "backupCount": 20, 26 | "encoding": "utf-8" 27 | }, 28 | "handler_file_powermeter": { 29 | "class": "logging.handlers.RotatingFileHandler", 30 | "formatter": "detailed", 31 | "filename": "logs/powermeter.log", 32 | "maxBytes": 10485760, 33 | "backupCount": 20, 34 | "encoding": "utf-8" 35 | }, 36 | "handler_file_utils": { 37 | "class": "logging.handlers.RotatingFileHandler", 38 | "formatter": "detailed", 39 | "filename": "logs/utils.log", 40 | "maxBytes": 10485760, 41 | "backupCount": 20, 42 | "encoding": "utf-8" 43 | } 44 | }, 45 | 46 | "loggers": { 47 | "utils": { 48 | "level": "DEBUG", 49 | "handlers": ["handler_console", "handler_file_utils"], 50 | "propagate": 0 51 | }, 52 | "powermeter": { 53 | "level": "DEBUG", 54 | "handlers": ["handler_console", "handler_file_powermeter"], 55 | "propagate": 0 56 | } 57 | }, 58 | 59 | "root": { 60 | "level": "ERROR", 61 | "handlers": ["handler_file_errors"] 62 | } 63 | } 64 | 65 | -------------------------------------------------------------------------------- /logmanagement.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # coding: utf-8 3 | # Copyright 2017 Luca Berton 4 | __author__ = 'lucab85' 5 | 6 | import os 7 | import json 8 | import logging.config 9 | from settings import PATH_LOGGING 10 | 11 | class logmanagement(): 12 | logobj = None 13 | @classmethod 14 | def getlog(cls, classname, loggername, forceloggername=False): 15 | if cls.logobj == None: 16 | cls.logobj = logmanagement(classname, loggername) 17 | 18 | if forceloggername is True and cls.logobj.loggername != loggername: 19 | cls.logobj = logmanagement(classname, loggername) 20 | 21 | cls.logobj.log.debug('Log Management %s => %s' % (classname, cls.logobj.loggername)) 22 | return cls.logobj 23 | 24 | def __init__(self, classname, loggername): 25 | self.setup_logging() 26 | self.classname = classname 27 | self.loggername = loggername 28 | self.log = logging.getLogger(loggername) 29 | 30 | def setup_logging(self, default_path=PATH_LOGGING, default_level=logging.INFO, env_key='LOG_CFG'): 31 | path = default_path 32 | self.logconf = None 33 | value = os.getenv(env_key, None) 34 | if value: 35 | path = value 36 | if os.path.exists(path): 37 | with open(path, 'rt') as f: 38 | config = json.load(f) 39 | self.logconf = logging.config.dictConfig(config) 40 | elif os.path.exists(path.replace("../", "")): 41 | with open(path.replace("../", ""), 'rt') as f: 42 | config = json.load(f) 43 | self._changePath(config["handlers"]) 44 | self.logconf = logging.config.dictConfig(config) 45 | else: 46 | print("Configurazione log non trovata (\"%s\"): applico le impostazioni predefinite" % path) 47 | self.logconf = logging.basicConfig(level=default_level) 48 | 49 | def _changePath(dictionary): 50 | for k1, v1 in dictionary.iteritems(): 51 | if type(v1) is dict: 52 | for k2 in v1.keys(): 53 | if k2 == "filename": 54 | original = dictionary[k1][k2] 55 | replaced = original.replace("../", "") 56 | dictionary[k1][k2] = replaced 57 | 58 | def getLogger(self): 59 | return self.log 60 | 61 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # coding: utf-8 3 | # Copyright 2017 Luca Berton 4 | __author__ = 'lucab85' 5 | 6 | from settings import PM_config, PM_SETTINGS_LABELS 7 | from SchneiderElectric_iEM3255 import SchneiderElectric_iEM3255 8 | 9 | from logmanagement import logmanagement 10 | log = logmanagement.getlog('main', 'utils').getLogger() 11 | 12 | def main(): 13 | PM_cacheEnabled = PM_config['cacheEnabled'] 14 | pm = SchneiderElectric_iEM3255(PM_config['host'], PM_config['port'], 15 | int(PM_config['address']), PM_config['start_reg'], 16 | PM_config['max_regs'], PM_config['timeout'], 17 | PM_config['endian'], PM_config['addressoffset'], 18 | PM_cacheEnabled, PM_config['base_commands']) 19 | 20 | print("Connesso? %s" % pm.mb.isConnected) 21 | 22 | print("--------------------------------------") 23 | print(" SETTINGS ") 24 | for i in PM_SETTINGS_LABELS: 25 | value = pm._modbusRead(i) 26 | print("%s = %s" % (i, value)) 27 | 28 | print("--------------------------------------") 29 | print(" ACTIVE ENERGY ") 30 | l1 = pm.readL1Active() 31 | l2 = pm.readL2Active() 32 | l3 = pm.readL3Active() 33 | print("line 1: %s 2: %s 3: %s" % (l1, l2, l3)) 34 | 35 | print("--------------------------------------") 36 | print(" CURRENT") 37 | l1 = pm.readL1Current() 38 | l2 = pm.readL2Current() 39 | l3 = pm.readL3Current() 40 | print("line 1: %s 2: %s 3: %s" % (l1, l2, l3)) 41 | 42 | print("--------------------------------------") 43 | print(" VOLTAGE") 44 | l1 = pm.readL1Voltage() 45 | l2 = pm.readL2Voltage() 46 | l3 = pm.readL3Voltage() 47 | print("line 1: %s 2: %s 3: %s" % (l1, l2, l3)) 48 | 49 | print("--------------------------------------") 50 | print(" COMMAND") 51 | print("date: %s" % pm.read_date_time()) 52 | pm.cmd_set_date_time() 53 | print("date: %s" % pm.read_date_time()) 54 | 55 | if __name__ == '__main__': 56 | main() 57 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pymodbus>=1.3.0 2 | -------------------------------------------------------------------------------- /settings.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # Copyright 2017 Luca Berton 3 | __author__ = 'lucab85' 4 | 5 | debug = True 6 | 7 | PM_config = { 8 | 'host': '127.0.0.1', 9 | 'port': 502, 10 | 'address': 1, 11 | 'addressoffset': -1, 12 | 'start_reg': 0, 13 | 'max_regs': 125, 14 | 'timeout': 2, 15 | 'endian': 'big', 16 | 'cacheEnabled': True, 17 | 'base_commands': 5250 18 | } 19 | 20 | PM_settings = { 21 | 'Set DateTime': { 22 | 'command': 1003 23 | }, 24 | 'Set Wiring': { 25 | 'command': 2000, 26 | 'Power System Configuration': 11, 27 | 'Nominal Frequency': 60, 28 | 'VT Primary': 100.0, 29 | 'VT Secondary': 100, 30 | 'Number of CTs': 3, 31 | 'CT Primary': 60, 32 | 'CT Secondary': 5, 33 | 'VT Connection type': 0 34 | }, 35 | 'Set Pulse Output': { 36 | 'command': 2003, 37 | 'Pulse Output enable': 1, 38 | 'Pulse constant': 1, 39 | 40 | 'command2': 2038, 41 | 'Pulse width': 50 42 | }, 43 | 'Set Tariff': { 44 | 'command': 2060, 45 | 'Multi Tariff Mode': 0, 46 | 'command2': 2008, 47 | 'Tariff': 1 48 | }, 49 | 'Set Digital Input as Partial Energy Reset': { 50 | 'command': 6017, 51 | 'Digital Input to Associate': 0 52 | }, 53 | 'Input Metering Setup': { 54 | 'command': 6014, 55 | 'Input Metering Channel': 1, 56 | 'Label': 'input', 57 | 'Pulse Weight': 1000, 58 | 'Digital Input Association': 0 59 | }, 60 | 'Overload Alarm Setup': { 61 | 'command': 7000, 62 | 'Alarm ID': 0, 63 | 'Enabled': 0, 64 | 'Pickup value': float(100000000), 65 | 'command2': 20000, 66 | 'Digital Output to Associate': 0, 67 | 'command3': 20001 68 | }, 69 | 'Communications Setup': { 70 | 'command': 5000, 71 | 'Address': 1, 72 | 'Baud Rate': 1, 73 | 'Parity': 0 74 | }, 75 | 'Reset Partial Energy Counters': { 76 | 'command': 2020 77 | }, 78 | 'Reset Input Metering Counter': { 79 | 'command': 2023 80 | }, 81 | } 82 | 83 | MODBUS_CONNECTIONRETRY = 3 84 | PATH_LOGGING = 'configs/logging.json' 85 | PATH_PM_SCHNEIDERELECTRICIEM3255 = 'configs/Map-Schneider-iEM3255.csv' 86 | PM_SETTINGS_LABELS = [ 87 | "Meter Name", "Meter Model", "Manufacturer", "Serial Number", 88 | "Date of Manufacture", "Hardware Revision", "Firmware Version", 89 | "Meter Operation Timer", "Number of Phases", "Number of Wires", 90 | "Power System", "Nominal Frequency", "Number VTs", "CT Primary", 91 | "CT Secondary", "Number CTs", "CT Primary", "CT Secondary", 92 | "VT Connection Type", "Energy Pulse Duration", 93 | "Digital Output Association", "Pulse Weight" 94 | ] 95 | 96 | --------------------------------------------------------------------------------