def __read_registers(self, read_register_method: Callable, address: int, types: Union[Iterable[ModbusDataType], ModbusDataType], byteorder: Endian = Endian.Big, wordorder: Endian = Endian.Big, **kwargs): try: multi_request = isinstance(types, Iterable) if not multi_request: types = [types] def divide_rounding_up(numerator: int, denominator: int): return -(-numerator // denominator) number_of_addresses = sum( divide_rounding_up(t.bits, _MODBUS_HOLDING_REGISTER_SIZE) for t in types) response = read_register_method(address, number_of_addresses, **kwargs) if response.isError(): raise FaultState.error(__name__ + " " + str(response)) decoder = BinaryPayloadDecoder.fromRegisters( response.registers, byteorder, wordorder) result = [ struct.unpack(">e", struct.pack(">H", decoder.decode_16bit_uint())) if t == ModbusDataType.FLOAT_16 else getattr( decoder, t.decoding_method)() for t in types ] return result if multi_request else result[0] except pymodbus.exceptions.ConnectionException as e: raise FaultState.error( "TCP-Client konnte keine Verbindung zu " + str(self.address) + ":" + str(self.port) + " aufbauen. Bitte Einstellungen (IP-Adresse, ..) und " + "Hardware-Anschluss prüfen.") from e except pymodbus.exceptions.ModbusIOException as e: raise FaultState.warning( "TCP-Client " + str(self.address) + ":" + str(self.port) + " konnte keinen Wert abfragen. Falls vorhanden, parallele Verbindungen, zB. node red," + "beenden und bei anhaltender Fehlermeldung Zähler neustarten." ) from e except Exception as e: raise FaultState.error(__name__ + " " + str(type(e)) + " " + str(e)) from e
def update(self) -> None: vc_count = self.component_config.configuration.vc_count vc_type = self.component_config.configuration.vc_type with self.__tcp_client: if vc_type == 'VS': mb_unit = 40 mb_register = 20 # MB:20; ID: 15010; PV power kW elif vc_type == 'VT': mb_unit = 20 mb_register = 8 # MB:8; ID: 11004; Power of the PV generator kW else: raise FaultState.error("Unbekannter VC-Typ: " + str(vc_type)) power = 0 for i in range(1, vc_count + 1): mb_unit_dev = mb_unit + i power += self.__tcp_client.read_input_registers( mb_register, ModbusDataType.FLOAT_32, unit=mb_unit_dev) power = power * -1000 if vc_type == 'VS': mb_register = 46 # MB:46; ID: 15023; Desc: Total PV produced energy MWh elif vc_type == 'VT': mb_register = 18 # MB:18; ID: 11009; Desc: Total produced energy MWh exported = 0 for i in range(1, vc_count + 1): mb_unit_dev = mb_unit + i exported += self.__tcp_client.read_input_registers( mb_register, ModbusDataType.FLOAT_32, unit=mb_unit_dev) exported = exported * 1000000 inverter_state = InverterState(power=power, exported=exported) self.__store.set(inverter_state)
def __enter__(self) -> Iterator[dict]: ip_bind = "0.0.0.0" multicast_group = "239.12.255.254" multicast_port = 9522 sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP) try: sock.settimeout(self.__timeout_seconds) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sock.bind(('', multicast_port)) mreq = struct.pack("4s4s", socket.inet_aton(multicast_group), socket.inet_aton(ip_bind)) sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq) except BaseException: sock.close() raise FaultState.error( "could not connect to multicast group or bind to given interface" ) self.__socket = sock def generator() -> Iterator[dict]: while True: datagram = sock.recv(608) if len(datagram) >= 18 and datagram[16:18] == b'\x60\x69': yield decode_speedwire(datagram) return generator()
def update(self, bat: bool) -> Tuple[CounterState, MeterLocation]: variant = self.component_config["configuration"]["variant"] log.MainLogger().debug("Komponente " + self.component_config["name"] + " auslesen.") session = req.get_http_session() if variant == 0 or variant == 1: counter_state, meter_location = self.__update_variant_0_1(session) elif variant == 2: counter_state, meter_location = self.__update_variant_2(session) else: raise FaultState.error("Unbekannte Variante: " + str(variant)) if meter_location == MeterLocation.load: response = session.get( 'http://' + self.device_config["ip_address"] + '/solar_api/v1/GetPowerFlowRealtimeData.fcgi', params=(('Scope', 'System'), ), timeout=5) counter_state.power = float( response.json()["Body"]["Data"]["Site"]["P_Grid"]) topic_str = "openWB/set/system/device/{}/component/{}/".format( self.__device_id, self.component_config["id"]) # Beim Energiebezug ist nicht klar, welcher Anteil aus dem Netz bezogen wurde, und was aus # dem Wechselrichter kam. # Beim Energieexport ist nicht klar, wie hoch der Eigenverbrauch während der Produktion war. counter_state.imported, counter_state.exported = self.__sim_count.sim_count( counter_state.power, topic=topic_str, data=self.simulation, prefix="bezug") return counter_state, meter_location
def __update_variant_0_1( self, session: Session) -> Tuple[CounterState, MeterLocation]: variant = self.component_config["configuration"]["variant"] meter_id = self.device_config["meter_id"] if variant == 0: params = ( ('Scope', 'Device'), ('DeviceId', meter_id), ) elif variant == 1: params = ( ('Scope', 'Device'), ('DeviceId', meter_id), ('DataCollection', 'MeterRealtimeData'), ) else: raise FaultState.error("Unbekannte Generation: " + str(variant)) response = session.get('http://' + self.device_config["ip_address"] + '/solar_api/v1/GetMeterRealtimeData.cgi', params=params, timeout=5) response_json_id = response.json()["Body"]["Data"] # old request for variant == 1 # params = ( # ('Scope', 'System'), # ) # response = req.get_http_session().get( # 'http://'+self.device_config["ip_address"]+'/solar_api/v1/GetMeterRealtimeData.cgi', # params=params, timeout=5) # response_json_id = response["Body"]["Data"][meter_id] meter_location = MeterLocation( response_json_id["Meter_Location_Current"]) log.MainLogger().debug("Einbauort: " + str(meter_location)) power = response_json_id["PowerReal_P_Sum"] voltages = [ response_json_id["Voltage_AC_Phase_" + str(num)] for num in range(1, 4) ] powers = [ response_json_id["PowerReal_P_Phase_" + str(num)] for num in range(1, 4) ] currents = [powers[i] / voltages[i] for i in range(0, 3)] power_factors = [ response_json_id["PowerFactor_Phase_" + str(num)] for num in range(1, 4) ] frequency = response_json_id["Frequency_Phase_Average"] imported = response_json_id["EnergyReal_WAC_Sum_Consumed"] exported = response_json_id["EnergyReal_WAC_Sum_Produced"] return CounterState(voltages=voltages, currents=currents, powers=powers, imported=imported, exported=exported, power=power, frequency=frequency, power_factors=power_factors), meter_location
def update(self) -> None: log.MainLogger().debug("Komponente "+self.component_config["name"]+" auslesen.") vc_count = self.component_config["configuration"]["vc_count"] vc_type = self.component_config["configuration"]["vc_type"] if vc_type == 'VS': mb_unit = 40 mb_register = 20 # MB:20; ID: 15010; PV power kW elif vc_type == 'VT': mb_unit = 20 mb_register = 8 # MB:8; ID: 11004; Power of the PV generator kW else: raise FaultState.error("Unbekannter VC-Typ: "+str(vc_type)) power = 0 for i in range(1, vc_count+1): mb_unit_dev = mb_unit+i power += self.__tcp_client.read_input_registers(mb_register, ModbusDataType.FLOAT_32, unit=mb_unit_dev) power = power * -1000 if vc_type == 'VS': mb_register = 46 # MB:46; ID: 15023; Desc: Total PV produced energy MWh elif vc_type == 'VT': mb_register = 18 # MB:18; ID: 11009; Desc: Total produced energy MWh counter = 0 for i in range(1, vc_count + 1): mb_unit_dev = mb_unit + i counter += self.__tcp_client.read_input_registers(mb_register, ModbusDataType.FLOAT_32, unit=mb_unit_dev) counter = counter * 1000000 inverter_state = InverterState( power=power, counter=counter ) self.__store.set(inverter_state)
def close_connection(self) -> None: try: log.MainLogger().debug("Close Modbus TCP connection") self.delegate.close() except Exception as e: raise FaultState.error(__name__ + " " + str(type(e)) + " " + str(e)) from e
def __update_variant_0_1(self, session: Session) -> CounterState: variant = self.component_config.configuration.variant meter_id = self.component_config.configuration.meter_id if variant == 0: params = ( ('Scope', 'Device'), ('DeviceId', meter_id), ) elif variant == 1: params = ( ('Scope', 'Device'), ('DeviceId', meter_id), ('DataCollection', 'MeterRealtimeData'), ) else: raise FaultState.error("Unbekannte Generation: " + str(variant)) response = session.get('http://' + self.device_config.ip_address + '/solar_api/v1/GetMeterRealtimeData.cgi', params=params, timeout=5) response_json_id = response.json()["Body"]["Data"] meter_location = MeterLocation.get( response_json_id["Meter_Location_Current"]) log.debug("Einbauort: " + str(meter_location)) powers = [ response_json_id["PowerReal_P_Phase_" + str(num)] for num in range(1, 4) ] if meter_location == MeterLocation.load: power, power_inverter = self.__get_flow_power(session) # wenn SmartMeter im Verbrauchszweig sitzt sind folgende Annahmen getroffen: # PV Leistung wird gleichmäßig auf alle Phasen verteilt # Spannungen und Leistungsfaktoren sind am Verbrauchszweig == Einspeisepunkt # Hier gehen wir mal davon aus, dass der Wechselrichter seine PV-Leistung gleichmäßig # auf alle Phasen aufteilt. powers = [-1 * power - power_inverter / 3 for power in powers] else: power = response_json_id["PowerReal_P_Sum"] voltages = [ response_json_id["Voltage_AC_Phase_" + str(num)] for num in range(1, 4) ] currents = [powers[i] / voltages[i] for i in range(0, 3)] power_factors = [ response_json_id["PowerFactor_Phase_" + str(num)] for num in range(1, 4) ] frequency = response_json_id["Frequency_Phase_Average"] return CounterState(voltages=voltages, currents=currents, powers=powers, power=power, frequency=frequency, power_factors=power_factors)
def kit_bat_version_factory( version: int) -> Type[Union[mpm3pm.Mpm3pm, sdm.Sdm630, sdm.Sdm120]]: if version == 0: return mpm3pm.Mpm3pm elif version == 1: return sdm.Sdm120 elif version == 2: return sdm.Sdm630 else: raise FaultState.error("Version " + str(version) + " unbekannt.")
def kit_counter_inverter_version_factory( version: int) -> Type[Union[mpm3pm.Mpm3pm, lovato.Lovato, sdm.Sdm630]]: if version == 0: return mpm3pm.Mpm3pm elif version == 1: return lovato.Lovato elif version == 2: return sdm.Sdm630 else: raise FaultState.error("Version " + str(version) + " unbekannt.")
def __init__(self, device_config: dict) -> None: self.device_config = device_config self._components = {} # type: Dict[str, bat.BatKit] version = self.device_config["configuration"]["version"] if version == 0 or version == 1: ip_address = '192.168.193.19' elif version == 2: ip_address = '192.168.193.15' else: raise FaultState.error("Version " + str(version) + " unbekannt.") self.client = modbus.ModbusClient(ip_address, 8899)
def update(self) -> None: log.debug("Variante: " + str(self.__device_variant)) if self.__device_variant == 0: state = self.__update_variant_0() elif self.__device_variant == 1: state = self.__update_variant_1() elif self.__device_variant == 2: state = self.__update_variant_2() else: raise FaultState.error("Unbekannte Variante: " + str(self.__device_variant)) self.__store.set(state)
def __init__(self, device_id: int, component_config: dict, tcp_client: modbus.ModbusClient) -> None: self.data = {"config": component_config} version = self.data["config"]["configuration"]["version"] if version == 0 or version == 1: id = 0x08 elif version == 2: id = 116 else: raise FaultState.error("Version " + str(version) + " unbekannt.") self.data["config"]["configuration"]["id"] = id super().__init__(device_id, self.data["config"], tcp_client)
def get_topic(prefix: str) -> str: """ ermittelt das zum Präfix gehörende Topic.""" try: if prefix == "bezug": topic = "evu" elif prefix == "pv" or prefix == "pv2": topic = prefix elif prefix == "speicher": topic = "housebattery" else: raise FaultState.error("Fehler im Modul simcount: Unbekannter Präfix") return topic except Exception as e: process_error(e)
def update(self) -> None: log.MainLogger().debug("Komponente '" + str(self.component_config["id"]) + "' " + self.component_config["name"] + " wird auslesen.") log.MainLogger().debug("Variante: " + str(self.__device_variant)) if self.__device_variant == 0: state = self.__update_variant_0() elif self.__device_variant == 1: state = self.__update_variant_1() elif self.__device_variant == 2: state = self.__update_variant_2() else: raise FaultState.error("Unbekannte Variante: " + str(self.__device_variant)) self.__store.set(state) log.MainLogger().debug("Komponente "+self.component_config["name"]+" wurde erfolgreich auslesen.")
def __init__(self, device_id: int, component_config: dict) -> None: self.data = {"config": component_config} version = self.data["config"]["configuration"]["version"] if version == 0: id = 5 elif version == 1: id = 2 elif version == 2: id = 115 else: raise FaultState.error("Version " + str(version) + " unbekannt.") self.data["config"]["configuration"]["id"] = id super().__init__(device_id, self.data["config"], modbus.ModbusClient("192.168.193.15", 8899))
def __init__(self, device_id: int, component_config: Union[Dict, PvKitInverterSetup], tcp_client: modbus.ModbusTcpClient_) -> None: self.component_config = dataclass_from_dict(PvKitInverterSetup, component_config) version = self.component_config.configuration.version if version == 0 or version == 1: id = 0x08 elif version == 2: id = 116 else: raise FaultState.error("Version " + str(version) + " unbekannt.") super().__init__(device_id, convert_to_flex_setup(self.component_config, id), tcp_client)
def _update_session_key(self, session: Session): try: headers = { 'Content-Type': 'application/json', } data = json.dumps( {"password": self.device_config.configuration.password}) response = session.put( "https://" + self.device_config.configuration.ip_address + '/v1/login', headers=headers, data=data, verify=False, timeout=5).json() self.session_key = response["auth_key"] except (HTTPError, KeyError): raise FaultState.error("login failed! check password!")
def __init__(self, device_id: int, component_config: dict) -> None: try: self.data = {"config": component_config} version = self.data["config"]["configuration"]["version"] if version == 0 or version == 1: id = 0x08 elif version == 2: id = 116 else: raise FaultState.error("Version "+str(version) + " unbekannt.") self.data["config"]["configuration"]["id"] = id super().__init__(device_id, self.data["config"], modbus.ModbusClient("192.168.193.15", 8899)) except Exception: log.MainLogger().exception("Fehler im Modul " + self.data["config"]["components"]["component0"]["name"])
def __read_speedwire(self, speedwire: Iterator[dict]): stop_time = time.time() + timeout_seconds components_todo = self._components try: for sma_data in speedwire: components_todo = [ component for component in components_todo if not component.read_datagram(sma_data) ] if not components_todo: log.debug("All components updated") return if time.time() > stop_time: break except socket.timeout: pass raise FaultState.error( "Kein passendes Datagramm innerhalb des %ds timeout empfangen" % timeout_seconds)
def update(self) -> None: session = req.get_http_session() variant = self.component_config.configuration.variant if variant == 0 or variant == 1: counter_state = self.__update_variant_0_1(session) elif variant == 2: counter_state = self.__update_variant_2(session) else: raise FaultState.error("Unbekannte Variante: " + str(variant)) topic_str = "openWB/set/system/device/{}/component/{}/".format( self.__device_id, self.component_config.id) counter_state.imported, counter_state.exported = self.__sim_count.sim_count( counter_state.power, topic=topic_str, data=self.simulation, prefix="bezug") self.__store.set(counter_state)
def update(self) -> Tuple[CounterState, MeterLocation]: variant = self.component_config["configuration"]["variant"] log.MainLogger().debug("Komponente " + self.component_config["name"] + " auslesen.") session = req.get_http_session() if variant == 0 or variant == 1: counter_state, meter_location = self.__update_variant_0_1(session) elif variant == 2: counter_state, meter_location = self.__update_variant_2(session) else: raise FaultState.error("Unbekannte Variante: " + str(variant)) topic_str = "openWB/set/system/device/{}/component/{}/".format( self.__device_id, self.component_config["id"]) counter_state.imported, counter_state.exported = self.__sim_count.sim_count( counter_state.power, topic=topic_str, data=self.simulation, prefix="bezug") return counter_state, meter_location
def process_error(e): raise FaultState.error(__name__ + " " + str(type(e)) + " " + str(e)) from e
def __process_error(self, e): if isinstance(e, FaultState): raise else: raise FaultState.error(__name__+" "+str(type(e))+" "+str(e)) from e
# setup registry = ExceptionRegistry() registry.add(ErrorA, "A") registry.add(ErrorB, "B") registry.add(ErrorF, "F") # execution actual = registry.translate_exception(exception()) # evaluation assert actual.fault_str == expected_message @pytest.mark.parametrize("handler", [ pytest.param("msg", id="basic string"), pytest.param(lambda _: "msg", id="function returning string"), pytest.param(lambda _: FaultState.error("msg"), id="function returning fault state") ]) def test_accepts_all_supported_formats(handler): # setup registry = ExceptionRegistry() # execution registry.add(Exception, handler) actual = registry.translate_exception(Exception()) # evaluation assert isinstance(actual, FaultState) assert actual.fault_str == "msg"