diff --git a/packages/modules/chargepoints/openwb_pro/chargepoint_module.py b/packages/modules/chargepoints/openwb_pro/chargepoint_module.py index 837642a404..da8c7e0791 100644 --- a/packages/modules/chargepoints/openwb_pro/chargepoint_module.py +++ b/packages/modules/chargepoints/openwb_pro/chargepoint_module.py @@ -10,7 +10,7 @@ from modules.common.fault_state import ComponentInfo, FaultState from modules.common.hardware_check import check_meter_values from modules.common.store import get_chargepoint_value_store -from modules.common.component_state import ChargepointState +from modules.common.component_state import ChargepointState, CounterState from modules.common import req from modules.internal_chargepoint_handler.internal_chargepoint_handler_config import InternalChargepoint @@ -82,9 +82,11 @@ def request_values(self) -> ChargepointState: ) if json_rsp.get("voltages"): - meter_msg = check_meter_values(json_rsp["voltages"]) - if meter_msg: - self.fault_state.warning(meter_msg) + check_meter_values(CounterState(voltages=json_rsp["voltages"], + currents=json_rsp["currents"], + powers=json_rsp["powers"], + power=json_rsp["power_all"]), + self.fault_state) chargepoint_state.voltages = json_rsp["voltages"] if json_rsp.get("soc_value"): chargepoint_state.soc = json_rsp["soc_value"] diff --git a/packages/modules/chargepoints/openwb_series2_satellit/chargepoint_module.py b/packages/modules/chargepoints/openwb_series2_satellit/chargepoint_module.py index a596633ce1..0e24638ac0 100644 --- a/packages/modules/chargepoints/openwb_series2_satellit/chargepoint_module.py +++ b/packages/modules/chargepoints/openwb_series2_satellit/chargepoint_module.py @@ -42,7 +42,6 @@ def __init__(self, config: OpenWBseries2Satellit) -> None: f"openWB/set/chargepoint/{self.config.id}/get/error_timestamp", CP_ERROR, hide_exception=True) self._create_client() self._validate_version() - self.max_evse_current = self._client.evse_client.get_max_current() def delay_second_cp(self, delay: float): if self.config.configuration.duo_num == 0: @@ -79,23 +78,24 @@ def get_values(self) -> None: try: self.delay_second_cp(self.CP1_DELAY) with self._client.client, self.client_error_context: - self._client.check_hardware(self.fault_state) + evse_state, counter_state = self._client.request_and_check_hardware(self.fault_state) if self.version is False: self._validate_version() - currents = self._client.meter_client.get_currents() + + currents = counter_state.currents phases_in_use = sum(1 for current in currents if current > 3) - plug_state, charge_state, _ = self._client.evse_client.get_plug_charge_state() + chargepoint_state = ChargepointState( - power=self._client.meter_client.get_power()[1], + power=counter_state.power, currents=currents, - imported=self._client.meter_client.get_imported(), + imported=counter_state.imported, exported=0, - voltages=self._client.meter_client.get_voltages(), - plug_state=plug_state, - charge_state=charge_state, + voltages=counter_state.voltages, + plug_state=evse_state.plug_state, + charge_state=evse_state.charge_state, phases_in_use=phases_in_use, - serial_number=self._client.meter_client.get_serial_number(), - max_evse_current=self.max_evse_current + serial_number=counter_state.serial_number, + max_evse_current=evse_state.max_current ) self.store.set(chargepoint_state) self.client_error_context.reset_error_counter() @@ -119,7 +119,6 @@ def set_current(self, current: float) -> None: try: self.delay_second_cp(self.CP1_DELAY) with self._client.client: - self._client.check_hardware(self.fault_state) if self.version: self._client.evse_client.set_current(int(current)) else: @@ -134,7 +133,6 @@ def switch_phases(self, phases_to_use: int, duration: int) -> None: with self.client_error_context: try: with self._client.client: - self._client.check_hardware(self.fault_state) if phases_to_use == 1: self._client.client.delegate.write_register( 0x0001, 256, unit=self.ID_PHASE_SWITCH_UNIT) diff --git a/packages/modules/common/component_state.py b/packages/modules/common/component_state.py index 437c02a68a..c3e308e681 100644 --- a/packages/modules/common/component_state.py +++ b/packages/modules/common/component_state.py @@ -88,6 +88,7 @@ def __init__( powers: Optional[List[Optional[float]]] = None, power_factors: Optional[List[Optional[float]]] = None, frequency: float = 50, + serial_number: str = "", ): """Args: imported: total imported energy in Wh @@ -107,6 +108,7 @@ def __init__( self.exported = exported self.power = power self.frequency = frequency + self.serial_number = serial_number @auto_str @@ -234,3 +236,11 @@ def __init__(self, analog_input: Dict[str, float] = None, self.digital_input = digital_input self.analog_output = analog_output self.digital_output = digital_output + + +class EvseState: + def __init__(self, plug_state: bool, charge_state: bool, set_current: int, max_current: int) -> None: + self.plug_state = plug_state + self.charge_state = charge_state + self.set_current = set_current + self.max_current = max_current diff --git a/packages/modules/common/evse.py b/packages/modules/common/evse.py index 670b779056..b50779cebb 100644 --- a/packages/modules/common/evse.py +++ b/packages/modules/common/evse.py @@ -6,12 +6,13 @@ from helpermodules.logger import ModifyLoglevelContext from modules.common import modbus +from modules.common.component_state import EvseState from modules.common.modbus import ModbusDataType log = logging.getLogger(__name__) -class EvseState(IntEnum): +class EvseStatusCode(IntEnum): READY = (1, False, False) EV_PRESENT = (2, True, False) CHARGING = (3, True, True) @@ -32,6 +33,19 @@ class Evse: def __init__(self, modbus_id: int, client: modbus.ModbusSerialClient_) -> None: self.client = client self.id = modbus_id + with client: + time.sleep(0.1) + self.version = self.client.read_holding_registers(1005, ModbusDataType.UINT_16, unit=self.id) + time.sleep(0.1) + self.max_current = self.client.read_holding_registers(2007, ModbusDataType.UINT_16, unit=self.id) + with ModifyLoglevelContext(log, logging.DEBUG): + log.debug(f"Firmware-Version der EVSE: {self.version}") + if self.version < 17: + self._precise_current = False + else: + if self.is_precise_current_active() is False: + self.activate_precise_current() + self._precise_current = self.is_precise_current_active() def get_plug_charge_state(self) -> Tuple[bool, bool, float]: time.sleep(0.1) @@ -41,8 +55,8 @@ def get_plug_charge_state(self) -> Tuple[bool, bool, float]: set_current = int(set_current) log.debug("Gesetzte Stromstärke EVSE: "+str(set_current) + ", Status: "+str(state_number)+", Modbus-ID: "+str(self.id)) - state = EvseState(state_number) - if state == EvseState.FAILURE: + state = EvseStatusCode(state_number) + if state == EvseStatusCode.FAILURE: raise ValueError("Unbekannter Zustand der EVSE: State " + str(state)+", Soll-Stromstärke: "+str(set_current)) plugged = state.plugged @@ -52,9 +66,15 @@ def get_plug_charge_state(self) -> Tuple[bool, bool, float]: return plugged, charging, set_current def get_firmware_version(self) -> int: - time.sleep(0.1) - version = self.client.read_holding_registers(1005, ModbusDataType.UINT_16, unit=self.id) - return version + return self.version + + def get_evse_state(self) -> EvseState: + plugged, charging, set_current = self.get_plug_charge_state() + state = EvseState(plug_state=plugged, + charge_state=charging, + set_current=set_current, + max_current=self.max_current) + return state def is_precise_current_active(self) -> bool: time.sleep(0.1) @@ -92,8 +112,3 @@ def deactivate_precise_current(self) -> None: def set_current(self, current: int) -> None: time.sleep(0.1) self.client.write_registers(1000, current, unit=self.id) - - def get_max_current(self) -> int: - time.sleep(0.1) - current = self.client.read_holding_registers(2007, ModbusDataType.UINT_16, unit=self.id) - return current diff --git a/packages/modules/common/hardware_check.py b/packages/modules/common/hardware_check.py index fcb5cb508c..5515f41547 100644 --- a/packages/modules/common/hardware_check.py +++ b/packages/modules/common/hardware_check.py @@ -1,6 +1,7 @@ import pymodbus -from typing import Any, List, Optional, Protocol, Tuple, Union +from typing import Any, Optional, Protocol, Tuple, Union +from modules.common.component_state import CounterState, EvseState from modules.common.evse import Evse from modules.common.fault_state import FaultState from modules.common.modbus import ModbusSerialClient_, ModbusTcpClient_ @@ -15,25 +16,36 @@ LAN_ADAPTER_BROKEN = (f"{RS485_ADAPTER_BROKEN.format('der LAN-Konverter abgestürzt,')} " "Bitte den openWB series2 satellit stromlos machen.") METER_PROBLEM = "Der Zähler konnte nicht ausgelesen werden. Vermutlich ist der Zähler falsch konfiguriert oder defekt." -METER_BROKEN = "Die Spannungen des Zählers konnten nicht korrekt ausgelesen werden: {}V Der Zähler ist defekt." +METER_BROKEN_VOLTAGES = "Die Spannungen des Zählers konnten nicht korrekt ausgelesen werden: {}V Der Zähler ist defekt." METER_NO_SERIAL_NUMBER = ("Die Seriennummer des Zählers für das Ladelog kann nicht ausgelesen werden. Wenn Sie die " "Seriennummer für Abrechnungszwecke benötigen, wenden Sie sich bitte an unseren Support. Die " "Funktionalität wird dadurch nicht beeinträchtigt!") EVSE_BROKEN = ("Auslesen der EVSE nicht möglich. Vermutlich ist die EVSE defekt oder hat eine unbekannte Modbus-ID. " "(Fehlermeldung nur relevant, wenn diese auf der Startseite oder im Status angezeigt wird.)") +METER_IMPLAUSIBLE_VALUE = ("Der Zähler hat einen unplausiblen Wert zurückgegeben: Leistungen {}W, Ströme {}A, " + "Spannungen {}V.") -def check_meter_values(voltages: List[float]) -> Optional[str]: +def check_meter_values(counter_state: CounterState, fault_state: Optional[FaultState] = None) -> None: + meter_msg = _check_meter_values(counter_state) + if fault_state and meter_msg: + fault_state.warning(meter_msg) + + +def _check_meter_values(counter_state: CounterState) -> Optional[str]: def valid_voltage(voltage) -> bool: - return 200 < voltage < 260 - if ((valid_voltage(voltages[0]) and voltages[1] == 0 and voltages[2] == 0) or + return 200 < voltage < 250 + voltages = counter_state.voltages + if not ((valid_voltage(voltages[0]) and voltages[1] == 0 and voltages[2] == 0) or # Zoe lädt einphasig an einphasiger Wallbox und erzeugt Spannung auf L2 (ca 126V) (valid_voltage(voltages[0]) and 115 < voltages[1] < 135 and voltages[2] == 0) or (valid_voltage(voltages[0]) and valid_voltage(voltages[1]) and voltages[2] == 0) or (valid_voltage(voltages[0]) and valid_voltage(voltages[1]) and valid_voltage((voltages[2])))): - return None - else: - return METER_BROKEN.format(voltages) + return METER_BROKEN_VOLTAGES.format(voltages) + interdependent_values = [sum(counter_state.currents), counter_state.power] + if not (all(v == 0 for v in interdependent_values) or all(v != 0 for v in interdependent_values)): + return METER_IMPLAUSIBLE_VALUE.format(counter_state.powers, counter_state.currents, counter_state.voltages) + return None class ClientHandlerProtocol(Protocol): @@ -49,6 +61,12 @@ def evse_client(self) -> Evse: ... def meter_client(self) -> Any: ... @property def read_error(self) -> int: ... + @property + def handle_exception(self, exception: Exception) -> bool: ... + @property + def request_and_check_hardware(self, fault_state: FaultState) -> Tuple[EvseState, CounterState]: ... + @property + def check_meter(self) -> Tuple[bool, Optional[str], CounterState]: ... class SeriesHardwareCheckMixin: @@ -63,16 +81,20 @@ def handle_exception(self: ClientHandlerProtocol, exception: Exception): else: return False - def check_hardware(self: ClientHandlerProtocol, fault_state: FaultState): - + def request_and_check_hardware(self: ClientHandlerProtocol, + fault_state: FaultState) -> Tuple[EvseState, CounterState]: try: - if self.evse_client.get_firmware_version() > EVSE_MIN_FIRMWARE: - evse_check_passed = True - else: - evse_check_passed = False + with self.client: + evse_state = self.evse_client.get_evse_state() + evse_check_passed = True except Exception as e: evse_check_passed = self.handle_exception(e) - meter_check_passed, meter_error_msg = self.check_meter() + meter_check_passed, meter_error_msg, counter_state = self.check_meter() + if meter_check_passed is False and evse_check_passed is False: + if isinstance(self.client, ModbusTcpClient_): + raise Exception(LAN_ADAPTER_BROKEN) + else: + raise Exception(USB_ADAPTER_BROKEN) if meter_check_passed is False: if evse_check_passed is False: if isinstance(self.client, ModbusTcpClient_): @@ -90,12 +112,14 @@ def check_hardware(self: ClientHandlerProtocol, fault_state: FaultState): raise Exception(EVSE_BROKEN + " " + meter_error_msg + OPEN_TICKET) else: raise Exception(EVSE_BROKEN + OPEN_TICKET) + return evse_state, counter_state - def check_meter(self: ClientHandlerProtocol) -> Tuple[bool, Optional[str]]: + def check_meter(self: ClientHandlerProtocol) -> Tuple[bool, Optional[str], CounterState]: try: - serial_number = self.meter_client.get_serial_number() - if serial_number == "0" or serial_number is None: - return True, METER_NO_SERIAL_NUMBER - return True, check_meter_values(self.meter_client.get_voltages()) + with self.client: + counter_state = self.meter_client.get_counter_state() + if counter_state.serial_number == "0" or counter_state.serial_number is None: + return True, METER_NO_SERIAL_NUMBER, counter_state + return True, _check_meter_values(counter_state), counter_state except Exception: - return False, METER_PROBLEM + return False, METER_PROBLEM, None diff --git a/packages/modules/common/hardware_check_test.py b/packages/modules/common/hardware_check_test.py index b88dac6464..2748001853 100644 --- a/packages/modules/common/hardware_check_test.py +++ b/packages/modules/common/hardware_check_test.py @@ -1,39 +1,40 @@ import re from typing import List, Optional, Tuple, Union -from unittest.mock import MagicMock, Mock, patch +from unittest.mock import Mock, patch import pytest from modules.common import sdm +from modules.common import hardware_check +from modules.common.component_state import CounterState, EvseState from modules.common.evse import Evse from modules.common.hardware_check import ( - EVSE_BROKEN, LAN_ADAPTER_BROKEN, METER_BROKEN, METER_NO_SERIAL_NUMBER, METER_PROBLEM, OPEN_TICKET, - USB_ADAPTER_BROKEN, SeriesHardwareCheckMixin, check_meter_values) -from modules.common.modbus import NO_CONNECTION, ModbusClient, ModbusSerialClient_, ModbusTcpClient_ + EVSE_BROKEN, LAN_ADAPTER_BROKEN, METER_BROKEN_VOLTAGES, METER_NO_SERIAL_NUMBER, METER_PROBLEM, + OPEN_TICKET, USB_ADAPTER_BROKEN, SeriesHardwareCheckMixin, _check_meter_values) +from modules.common.modbus import NO_CONNECTION, ModbusSerialClient_, ModbusTcpClient_ from modules.conftest import SAMPLE_IP, SAMPLE_PORT from modules.internal_chargepoint_handler.clients import ClientHandler @pytest.mark.parametrize( - ("evse_side_effect, evse_return_value, meter_side_effect, meter_return_value, handle_exception_side_effect," + ("evse_side_effect, meter_side_effect, meter_return_value, handle_exception_side_effect," "handle_exception_return_value, client_spec, expected_error_msg"), - [pytest.param(Exception("Modbus"), None, None, [230]*3, None, False, ModbusSerialClient_, EVSE_BROKEN, + [pytest.param(Exception("Modbus"), None, [230]*3, None, False, ModbusSerialClient_, EVSE_BROKEN, id="EVSE defekt"), - pytest.param(Exception("Modbus"), None, None, [230, 0, 230], None, False, ModbusSerialClient_, - EVSE_BROKEN + " " + METER_BROKEN.format([230, 0, 230]) + OPEN_TICKET, + pytest.param(Exception("Modbus"), None, [230, 0, 230], None, False, ModbusSerialClient_, + EVSE_BROKEN + " " + METER_BROKEN_VOLTAGES.format([230, 0, 230]) + OPEN_TICKET, id="EVSE defekt und Zähler eine Phase defekt"), - pytest.param(None, 18, Exception("Modbus"), None, None, None, + pytest.param(None, Exception("Modbus"), None, None, None, ModbusSerialClient_, METER_PROBLEM, id="Zähler falsch konfiguriert"), - pytest.param(Exception("Modbus"), None, Exception("Modbus"), None, None, False, ModbusSerialClient_, + pytest.param(Exception("Modbus"), Exception("Modbus"), None, None, False, ModbusSerialClient_, USB_ADAPTER_BROKEN, id="USB-Adapter defekt"), - pytest.param(Exception("Modbus"), None, Exception("Modbus"), None, None, False, ModbusTcpClient_, + pytest.param(Exception("Modbus"), Exception("Modbus"), None, None, False, ModbusTcpClient_, LAN_ADAPTER_BROKEN, id="LAN-Adapter defekt"), - pytest.param(Exception("Modbus"), None, Exception("Modbus"), None, + pytest.param(Exception("Modbus"), Exception("Modbus"), None, Exception(NO_CONNECTION.format(SAMPLE_IP, SAMPLE_PORT)), None, ModbusTcpClient_, NO_CONNECTION.format(SAMPLE_IP, SAMPLE_PORT), id="LAN-Adapter nicht erreichbar"), ] ) def test_hardware_check_fails(evse_side_effect, - evse_return_value, meter_side_effect, meter_return_value, handle_exception_side_effect, @@ -42,87 +43,102 @@ def test_hardware_check_fails(evse_side_effect, expected_error_msg, monkeypatch): # setup - mock_evse_client = Mock(spec=Evse, get_firmware_version=Mock( - side_effect=evse_side_effect, return_value=evse_return_value)) - mock_evse_factory = Mock(spec=Evse, return_value=mock_evse_client) - monkeypatch.setattr(ClientHandler, "_evse_factory", mock_evse_factory) + mock_evse_client = Mock(spec=Evse, version=18, get_evse_state=Mock(side_effect=[evse_side_effect])) + monkeypatch.setattr(ClientHandler, "_evse_factory", Mock(return_value=mock_evse_client)) - mock_meter_client = Mock(spec=sdm.Sdm630_72, get_voltages=Mock( - side_effect=meter_side_effect, return_value=meter_return_value)) - mock_find_meter_client = Mock(spec=sdm.Sdm630_72, return_value=mock_meter_client) - monkeypatch.setattr(ClientHandler, "find_meter_client", mock_find_meter_client) + counter_state_mock = Mock(spec=CounterState, + voltages=meter_return_value, + currents=[0, 0, 0], + powers=[0, 0, 0], + power=0, + serial_number="1234") + mock_meter_client = Mock(spec=sdm.Sdm630_72, get_counter_state=Mock( + side_effect=meter_side_effect, return_value=counter_state_mock)) + monkeypatch.setattr(ClientHandler, "find_meter_client", Mock(return_value=mock_meter_client)) - handle_exception_mock = Mock(side_effect=handle_exception_side_effect, return_value=handle_exception_return_value) - monkeypatch.setattr(SeriesHardwareCheckMixin, "handle_exception", handle_exception_mock) + monkeypatch.setattr(SeriesHardwareCheckMixin, "handle_exception", Mock( + side_effect=handle_exception_side_effect, return_value=handle_exception_return_value)) - mock_modbus_client = MagicMock(spec=client_spec, address=SAMPLE_IP, port=SAMPLE_PORT) - mock_modbus_client.__enter__.return_value = mock_modbus_client + client = Mock(spec=client_spec, __enter__=Mock(return_value=None), __exit__=Mock(return_value=None)) # execution and evaluation with pytest.raises(Exception, match=re.escape(expected_error_msg)): - ClientHandler(0, mock_modbus_client, [1], Mock()) + ClientHandler(0, client, [1], Mock()) def test_hardware_check_succeeds(monkeypatch): # setup - mock_evse_client = Mock(spec=Evse, get_firmware_version=Mock(return_value=18)) - mock_evse_factory = Mock(spec=Evse, return_value=mock_evse_client) - monkeypatch.setattr(ClientHandler, "_evse_factory", mock_evse_factory) + mock_evse_client = Mock(spec=Evse, get_evse_state=Mock(return_value=Mock(spec=EvseState)), version=17) + mock_evse_facotry = Mock(return_value=mock_evse_client) + monkeypatch.setattr(ClientHandler, "_evse_factory", mock_evse_facotry) - mock_meter_client = Mock(spec=sdm.Sdm630_72, get_voltages=Mock(return_value=[230]*3)) + counter_state_mock = Mock(spec=CounterState, voltages=[ + 230]*3, currents=[0, 0, 0], powers=[0, 0, 0], power=0, serial_number="1234") + mock_meter_client = Mock(spec=sdm.Sdm630_72, get_counter_state=Mock(return_value=counter_state_mock)) mock_find_meter_client = Mock(spec=sdm.Sdm630_72, return_value=mock_meter_client) monkeypatch.setattr(ClientHandler, "find_meter_client", mock_find_meter_client) - mock_modbus_client = MagicMock(spec=ModbusClient) - mock_modbus_client.__enter__.return_value = mock_modbus_client + enter_mock = Mock(return_value=None) + exit_mock = Mock(return_value=True) + client = Mock(spec=ModbusTcpClient_, __enter__=enter_mock, __exit__=exit_mock) # execution and evaluation # keine Exception - ClientHandler(0, mock_modbus_client, [1], Mock()) + ClientHandler(0, client, [1], Mock()) @pytest.mark.parametrize( - "voltages, expected_msg", - [pytest.param([230, 0, 0], None, id="einphasig oder zweiphasig L2 defekt (nicht erkennbar)"), - pytest.param([0, 0, 0], METER_BROKEN, id="einphasig, L1 defekt"), - pytest.param([230, 230, 0], None, id="zweiphasig oder dreiphasig, L3 defekt (nicht erkennbar)"), - pytest.param([0, 230, 0], METER_BROKEN, id="zweiphasig, L1 defekt"), - pytest.param([230, 230, 230], None, id="dreiphasig"), - pytest.param([0, 230, 230], METER_BROKEN, id="dreiphasig, L1 defekt"), - pytest.param([230, 0, 230], METER_BROKEN, id="dreiphasig, L2 defekt"), + "voltages, power, expected_msg", + [pytest.param([230, 0, 0], 0, None, id="einphasig oder zweiphasig L2 defekt (nicht erkennbar)"), + pytest.param([0, 0, 0], 0, METER_BROKEN_VOLTAGES.format([0]*3), id="einphasig, L1 defekt"), + pytest.param([230, 230, 0], 0, None, id="zweiphasig oder dreiphasig, L3 defekt (nicht erkennbar)"), + pytest.param([0, 230, 0], 0, METER_BROKEN_VOLTAGES.format([0, 230, 0]), id="zweiphasig, L1 defekt"), + pytest.param([230, 230, 230], 0, None, id="dreiphasig"), + pytest.param([0, 230, 230], 0, METER_BROKEN_VOLTAGES.format([0, 230, 230]), id="dreiphasig, L1 defekt"), + pytest.param([230, 0, 230], 0, METER_BROKEN_VOLTAGES.format([230, 0, 230]), id="dreiphasig, L2 defekt"), + pytest.param([230]*3, 100, METER_PROBLEM, id="Phantom-Leistung"), ] ) -def test_check_meter_values(voltages, expected_msg, monkeypatch): - # setup & execution - msg = check_meter_values(voltages) +def test_check_meter_values(voltages, power, expected_msg, monkeypatch): + # setup + counter_state = Mock(voltages=voltages, currents=[0, 0, 0], powers=[0, 0, 0], power=power) + # execution + msg = _check_meter_values(counter_state) # assert assert msg == expected_msg if expected_msg is None else expected_msg.format(voltages) @patch('modules.common.hardware_check.ClientHandlerProtocol') -@pytest.mark.parametrize("serial_number_return, voltages_return, expected", - [("0", [230]*3, (True, METER_NO_SERIAL_NUMBER)), - (12345, [230]*3, (True, None)), - (Exception(), [230]*3, (False, METER_PROBLEM))]) +@pytest.mark.parametrize("serial_number, voltages, expected", + [("0", [230]*3, (True, METER_NO_SERIAL_NUMBER, CounterState)), + (12345, [230]*3, (True, None, CounterState)), + (Exception(), [230]*3, (False, METER_PROBLEM, None))]) def test_check_meter( MockClientHandlerProtocol: Mock, - serial_number_return: Union[int, Exception], - voltages_return: List[int], + serial_number: Union[int, Exception], + voltages: List[int], expected: Tuple[bool, Optional[str]], + monkeypatch, ): # Arrange - mock_meter_client = Mock() - if isinstance(serial_number_return, Exception): - mock_meter_client.get_serial_number.side_effect = serial_number_return + + if isinstance(serial_number, Exception): + counter_state_mock = Mock(spec=CounterState, side_effect=serial_number) else: - mock_meter_client.get_serial_number.return_value = serial_number_return - mock_meter_client.get_voltages.return_value = voltages_return + counter_state_mock = Mock(spec=CounterState, serial_number=serial_number, + voltages=voltages, currents=[0, 0, 0], powers=[0, 0, 0], power=0) + mock_meter_client = Mock() + mock_meter_client.get_counter_state.return_value = counter_state_mock MockClientHandlerProtocol.meter_client = mock_meter_client mixin = SeriesHardwareCheckMixin + mock_check_meter_values = Mock(return_value=expected[1]) + monkeypatch.setattr(hardware_check, "check_meter_values", mock_check_meter_values) # Act result = mixin.check_meter(MockClientHandlerProtocol) # Assert - assert result == expected + assert result[0] == expected[0] + assert result[1] == expected[1] + assert isinstance(result[2], expected[2] if expected[2] is not None else type(result[2])) diff --git a/packages/modules/common/lovato.py b/packages/modules/common/lovato.py index 504f37687d..cb8a0b596e 100644 --- a/packages/modules/common/lovato.py +++ b/packages/modules/common/lovato.py @@ -3,13 +3,17 @@ from modules.common import modbus from typing import List, Tuple from modules.common.abstract_counter import AbstractCounter +from modules.common.component_state import CounterState +from modules.common.fault_state import FaultState +from modules.common.hardware_check import check_meter_values from modules.common.modbus import ModbusDataType class Lovato(AbstractCounter): - def __init__(self, modbus_id: int, client: modbus.ModbusTcpClient_) -> None: + def __init__(self, modbus_id: int, client: modbus.ModbusTcpClient_, fault_state: FaultState) -> None: self.client = client self.id = modbus_id + self.fault_state = fault_state def get_voltages(self) -> List[float]: return [val / 100 for val in self.client.read_input_registers( @@ -36,3 +40,16 @@ def get_frequency(self) -> float: def get_currents(self) -> List[float]: return [val / 10000 for val in self.client.read_input_registers( 0x0007, [ModbusDataType.INT_32]*3, unit=self.id)] + + def get_counter_state(self) -> CounterState: + powers, power = self.get_power() + counter_state = CounterState( + power=power, + voltages=self.get_voltages(), + currents=self.get_currents(), + powers=powers, + power_factors=self.get_power_factors(), + frequency=self.get_frequency() + ) + check_meter_values(counter_state, self.fault_state) + return counter_state diff --git a/packages/modules/common/mpm3pm.py b/packages/modules/common/mpm3pm.py index 6cc99fbbe0..4791398dc7 100644 --- a/packages/modules/common/mpm3pm.py +++ b/packages/modules/common/mpm3pm.py @@ -3,13 +3,17 @@ from modules.common import modbus from modules.common.abstract_counter import AbstractCounter +from modules.common.component_state import CounterState +from modules.common.fault_state import FaultState +from modules.common.hardware_check import check_meter_values from modules.common.modbus import ModbusDataType class Mpm3pm(AbstractCounter): - def __init__(self, modbus_id: int, client: modbus.ModbusTcpClient_) -> None: + def __init__(self, modbus_id: int, client: modbus.ModbusTcpClient_, fault_state: FaultState) -> None: self.client = client self.id = modbus_id + self.fault_state = fault_state def get_voltages(self) -> List[float]: return [val / 10 for val in self.client.read_input_registers( @@ -47,3 +51,19 @@ def get_currents(self) -> List[float]: def get_serial_number(self) -> str: return str(self.client.read_input_registers(0x33, ModbusDataType.UINT_32, unit=self.id)) + + def get_counter_state(self) -> CounterState: + powers, power = self.get_power() + counter_state = CounterState( + voltages=self.get_voltages(), + currents=self.get_currents(), + powers=powers, + power=power, + power_factors=self.get_power_factors(), + imported=self.get_imported(), + exported=self.get_exported(), + frequency=self.get_frequency(), + serial_number=self.get_serial_number() + ) + check_meter_values(counter_state, self.fault_state) + return counter_state diff --git a/packages/modules/common/sdm.py b/packages/modules/common/sdm.py index 68448b8e4b..b77eec9eac 100644 --- a/packages/modules/common/sdm.py +++ b/packages/modules/common/sdm.py @@ -1,9 +1,12 @@ #!/usr/bin/env python3 import time -from typing import List, Tuple, Optional +from typing import List, Tuple from modules.common import modbus from modules.common.abstract_counter import AbstractCounter +from modules.common.component_state import CounterState +from modules.common.fault_state import FaultState +from modules.common.hardware_check import check_meter_values from modules.common.modbus import ModbusDataType @@ -13,6 +16,8 @@ def __init__(self, modbus_id: int, client: modbus.ModbusTcpClient_) -> None: self.id = modbus_id self.last_query = self._get_time_ms() self.WAIT_MS_BETWEEN_QUERIES = 100 + with client: + self.serial_number = str(self.client.read_holding_registers(0xFC00, ModbusDataType.UINT_32, unit=self.id)) def get_imported(self) -> float: self._ensure_min_time_between_queries() @@ -29,10 +34,6 @@ def get_frequency(self) -> float: frequency = frequency / 10 return frequency - def get_serial_number(self) -> Optional[str]: - self._ensure_min_time_between_queries() - return str(self.client.read_holding_registers(0xFC00, ModbusDataType.INT_32, unit=self.id)) - # These meters require some minimum time between subsequent Modbus reads. Some Eastron papers recommend 100 ms. # Sometimes the time between calls to the get_* methods are much shorter so we forcibly wait for the remaining time. def _ensure_min_time_between_queries(self) -> None: @@ -45,10 +46,14 @@ def _ensure_min_time_between_queries(self) -> None: def _get_time_ms(self) -> float: return time.time_ns() / 1e6 + def get_serial_number(self) -> str: + return self.serial_number + class Sdm630_72(Sdm): - def __init__(self, modbus_id: int, client: modbus.ModbusTcpClient_) -> None: + def __init__(self, modbus_id: int, client: modbus.ModbusTcpClient_, fault_state: FaultState) -> None: super().__init__(modbus_id, client) + self.fault_state = fault_state def get_currents(self) -> List[float]: self._ensure_min_time_between_queries() @@ -68,10 +73,27 @@ def get_voltages(self) -> List[float]: self._ensure_min_time_between_queries() return self.client.read_input_registers(0x00, [ModbusDataType.FLOAT_32]*3, unit=self.id) + def get_counter_state(self) -> CounterState: + powers, power = self.get_power() + counter_state = CounterState( + imported=self.get_imported(), + exported=self.get_exported(), + power=power, + voltages=self.get_voltages(), + currents=self.get_currents(), + powers=powers, + power_factors=self.get_power_factors(), + frequency=self.get_frequency(), + serial_number=self.get_serial_number() + ) + check_meter_values(counter_state, self.fault_state) + return counter_state + class Sdm120(Sdm): - def __init__(self, modbus_id: int, client: modbus.ModbusTcpClient_) -> None: + def __init__(self, modbus_id: int, client: modbus.ModbusTcpClient_, fault_state: FaultState) -> None: super().__init__(modbus_id, client) + self.fault_state = fault_state def get_power(self) -> Tuple[List[float], float]: self._ensure_min_time_between_queries() @@ -90,3 +112,17 @@ def get_voltages(self) -> List[float]: def get_power_factors(self) -> List[float]: self._ensure_min_time_between_queries() return [self.client.read_input_registers(0x1E, ModbusDataType.FLOAT_32, unit=self.id), 0.0, 0.0] + + def get_counter_state(self) -> CounterState: + powers, power = self.get_power() + counter_state = CounterState( + imported=self.get_imported(), + exported=self.get_exported(), + power=power, + currents=self.get_currents(), + powers=powers, + frequency=self.get_frequency(), + serial_number=self.get_serial_number() + ) + check_meter_values(counter_state, self.fault_state) + return counter_state diff --git a/packages/modules/devices/openwb/openwb_flex/bat.py b/packages/modules/devices/openwb/openwb_flex/bat.py index 494af8bf12..b8a873ebc0 100644 --- a/packages/modules/devices/openwb/openwb_flex/bat.py +++ b/packages/modules/devices/openwb/openwb_flex/bat.py @@ -5,7 +5,6 @@ from modules.common.abstract_device import AbstractBat from modules.common.component_state import BatState from modules.common.component_type import ComponentDescriptor -from modules.common.fault_state import ComponentInfo, FaultState from modules.common.lovato import Lovato from modules.common.mpm3pm import Mpm3pm from modules.common.sdm import Sdm120 @@ -30,25 +29,24 @@ def initialize(self) -> None: self.__device_id: int = self.kwargs['device_id'] self.__tcp_client: modbus.ModbusTcpClient_ = self.kwargs['client'] factory = kit_bat_version_factory(self.component_config.configuration.version) - self.__client = factory(self.component_config.configuration.id, self.__tcp_client) + self.__client = factory(self.component_config.configuration.id, self.__tcp_client, self.fault_state) self.sim_counter = SimCounter(self.__device_id, self.component_config.id, prefix="speicher") self.store = get_bat_value_store(self.component_config.id) - self.fault_state = FaultState(ComponentInfo.from_component_config(self.component_config)) def update(self): # TCP-Verbindung schließen möglichst bevor etwas anderes gemacht wird, um im Fehlerfall zu verhindern, # dass offene Verbindungen den Modbus-Adapter blockieren. with self.__tcp_client: - if isinstance(self.__client, Sdm630_72): - _, power = self.__client.get_power() - power = power * -1 - else: - _, power = self.__client.get_power() - if isinstance(self.__client, Lovato) or isinstance(self.__client, Sdm120): - imported, exported = self.sim_counter.sim_count(power) - else: - imported = self.__client.get_imported() - exported = self.__client.get_exported() + counter_state = self.__client.get_counter_state() + + power = counter_state.power + if isinstance(self.__client, Sdm630_72): + power = power * -1 + if isinstance(self.__client, Lovato) or isinstance(self.__client, Sdm120): + imported, exported = self.sim_counter.sim_count(power) + else: + imported = counter_state.imported + exported = counter_state.exported voltages = self.__client.get_voltages() powers, power = self.__client.get_power() diff --git a/packages/modules/devices/openwb/openwb_flex/counter.py b/packages/modules/devices/openwb/openwb_flex/counter.py index 7376ff0b25..172012a13c 100644 --- a/packages/modules/devices/openwb/openwb_flex/counter.py +++ b/packages/modules/devices/openwb/openwb_flex/counter.py @@ -3,10 +3,7 @@ from modules.common import modbus from modules.common.abstract_device import AbstractCounter -from modules.common.component_state import CounterState from modules.common.component_type import ComponentDescriptor -from modules.common.fault_state import ComponentInfo, FaultState -from modules.common.lovato import Lovato from modules.common.mpm3pm import Mpm3pm from modules.common.b23 import B23 from modules.common.simcount import SimCounter @@ -29,42 +26,21 @@ def initialize(self) -> None: self.__device_id: int = self.kwargs['device_id'] self.__tcp_client: modbus.ModbusTcpClient_ = self.kwargs['client'] factory = kit_counter_version_factory(self.component_config.configuration.version) - self.__client = factory(self.component_config.configuration.id, self.__tcp_client) + self.__client = factory(self.component_config.configuration.id, self.__tcp_client, self.fault_state) self.sim_counter = SimCounter(self.__device_id, self.component_config.id, prefix="bezug") self.store = get_counter_value_store(self.component_config.id) - self.fault_state = FaultState(ComponentInfo.from_component_config(self.component_config)) def update(self): # TCP-Verbindung schließen möglichst bevor etwas anderes gemacht wird, um im Fehlerfall zu verhindern, # dass offene Verbindungen den Modbus-Adapter blockieren. with self.__tcp_client: - voltages = self.__client.get_voltages() - powers, power = self.__client.get_power() - frequency = self.__client.get_frequency() - power_factors = self.__client.get_power_factors() - - if isinstance(self.__client, Mpm3pm or B23): - imported = self.__client.get_imported() - exported = self.__client.get_exported() - else: - currents = self.__client.get_currents() + counter_state = self.__client.get_counter_state() if isinstance(self.__client, Mpm3pm or B23): - currents = [powers[i] / voltages[i] for i in range(3)] + counter_state.currents = [counter_state.powers[i] / counter_state.voltages[i] for i in range(3)] else: - if isinstance(self.__client, Lovato): - power = sum(powers) - imported, exported = self.sim_counter.sim_count(power) - counter_state = CounterState( - voltages=voltages, - currents=currents, - powers=powers, - power_factors=power_factors, - imported=imported, - exported=exported, - power=power, - frequency=frequency - ) + counter_state.imported, counter_state.exported = self.sim_counter.sim_count(counter_state.power) + self.store.set(counter_state) diff --git a/packages/modules/devices/openwb/openwb_flex/inverter.py b/packages/modules/devices/openwb/openwb_flex/inverter.py index 0d72c2a5bc..7d79eee8c7 100644 --- a/packages/modules/devices/openwb/openwb_flex/inverter.py +++ b/packages/modules/devices/openwb/openwb_flex/inverter.py @@ -5,7 +5,6 @@ from modules.common.abstract_device import AbstractInverter from modules.common.component_state import InverterState from modules.common.component_type import ComponentDescriptor -from modules.common.fault_state import ComponentInfo, FaultState from modules.common.lovato import Lovato from modules.common.sdm import Sdm120 from modules.common.simcount import SimCounter @@ -28,34 +27,32 @@ def initialize(self) -> None: self.__device_id: int = self.kwargs['device_id'] self.__tcp_client: modbus.ModbusTcpClient_ = self.kwargs['client'] factory = kit_inverter_version_factory(self.component_config.configuration.version) - self.__client = factory(self.component_config.configuration.id, self.__tcp_client) + self.__client = factory(self.component_config.configuration.id, self.__tcp_client, self.fault_state) self.sim_counter = SimCounter(self.__device_id, self.component_config.id, prefix="pv") self.simulation = {} self.store = get_inverter_value_store(self.component_config.id) - self.fault_state = FaultState(ComponentInfo.from_component_config(self.component_config)) def update(self) -> None: """ liest die Werte des Moduls aus. """ with self.__tcp_client: - powers, power = self.__client.get_power() - - version = self.component_config.configuration.version - if version == 1: - power = sum(powers) - if power > 10: - power = power*-1 - currents = self.__client.get_currents() - - if isinstance(self.__client, Lovato) or isinstance(self.__client, Sdm120): - _, exported = self.sim_counter.sim_count(power) - else: - exported = self.__client.get_exported() + counter_state = self.__client.get_counter_state() + + power = counter_state.power + version = self.component_config.configuration.version + if version == 1: + power = sum(counter_state.powers) + if power > 10: + power = power*-1 + if isinstance(self.__client, Lovato) or isinstance(self.__client, Sdm120): + _, exported = self.sim_counter.sim_count(power) + else: + exported = counter_state.exported inverter_state = InverterState( power=power, exported=exported, - currents=currents + currents=counter_state.currents ) self.store.set(inverter_state) diff --git a/packages/modules/internal_chargepoint_handler/chargepoint_module.py b/packages/modules/internal_chargepoint_handler/chargepoint_module.py index 3ae9020ca0..2420e2a79b 100644 --- a/packages/modules/internal_chargepoint_handler/chargepoint_module.py +++ b/packages/modules/internal_chargepoint_handler/chargepoint_module.py @@ -56,7 +56,7 @@ def __init__(self, local_charge_point_num: int, if self._client.evse_client.is_precise_current_active() is False: self._client.evse_client.activate_precise_current() self._precise_current = self._client.evse_client.is_precise_current_active() - self.max_evse_current = self._client.evse_client.get_max_current() + self.version = SubData.system_data["system"].data["version"] self.current_branch = SubData.system_data["system"].data["current_branch"] self.current_commit = SubData.system_data["system"].data["current_commit"] @@ -77,24 +77,18 @@ def store_state(chargepoint_state: ChargepointState) -> None: chargepoint_state = self.old_chargepoint_state self.set_current_evse = chargepoint_state.evse_current - self._client.check_hardware(self.fault_state) - powers, power = self._client.meter_client.get_power() - if power < self.PLUG_STANDBY_POWER_THRESHOLD: + evse_state, counter_state = self._client.request_and_check_hardware(self.fault_state) + power = counter_state.power + if counter_state.power < self.PLUG_STANDBY_POWER_THRESHOLD: power = 0 - voltages = self._client.meter_client.get_voltages() - currents = self._client.meter_client.get_currents() - imported = self._client.meter_client.get_imported() - power_factors = self._client.meter_client.get_power_factors() - frequency = self._client.meter_client.get_frequency() - serial_number = self._client.meter_client.get_serial_number() - phases_in_use = sum(1 for current in currents if current > 3) + phases_in_use = sum(1 for current in counter_state.currents if current > 3) if phases_in_use == 0: phases_in_use = self.old_phases_in_use else: self.old_phases_in_use = phases_in_use time.sleep(0.1) - plug_state, charge_state, self.set_current_evse = self._client.evse_client.get_plug_charge_state() + self.set_current_evse = evse_state.set_current self.client_error_context.reset_error_counter() if phase_switch_cp_active: @@ -105,24 +99,25 @@ def store_state(chargepoint_state: ChargepointState) -> None: ) plug_state = self.old_plug_state else: - self.old_plug_state = plug_state + self.old_plug_state = evse_state.plug_state + plug_state = evse_state.plug_state chargepoint_state = ChargepointState( power=power, - currents=currents, - imported=imported, + currents=counter_state.currents, + imported=counter_state.imported, exported=0, - powers=powers, - voltages=voltages, - frequency=frequency, + powers=counter_state.powers, + voltages=counter_state.voltages, + frequency=counter_state.frequency, plug_state=plug_state, - charge_state=charge_state, + charge_state=evse_state.charge_state, phases_in_use=phases_in_use, - power_factors=power_factors, + power_factors=counter_state.power_factors, rfid=last_tag, evse_current=self.set_current_evse, - serial_number=serial_number, - max_evse_current=self.max_evse_current, + serial_number=counter_state.serial_number, + max_evse_current=evse_state.max_current, version=self.version, current_branch=self.current_branch, current_commit=self.current_commit diff --git a/packages/modules/internal_chargepoint_handler/clients.py b/packages/modules/internal_chargepoint_handler/clients.py index 8b4d3ebc25..d90ecb10a8 100644 --- a/packages/modules/internal_chargepoint_handler/clients.py +++ b/packages/modules/internal_chargepoint_handler/clients.py @@ -39,9 +39,8 @@ def __init__(self, self.local_charge_point_num = local_charge_point_num self.evse_client = self._evse_factory(client, evse_ids) self.meter_client = self.find_meter_client(CP0_METERS if self.local_charge_point_num == 0 else CP1_METERS, - client) - with client: - self.check_hardware(fault_state) + client, fault_state) + self.request_and_check_hardware(fault_state) self.read_error = 0 def _evse_factory(self, client: Union[ModbusSerialClient_, ModbusTcpClient_], evse_ids: List[int]) -> evse.Evse: @@ -60,9 +59,11 @@ def _evse_factory(self, client: Union[ModbusSerialClient_, ModbusTcpClient_], ev return None @staticmethod - def find_meter_client(meters: List[meter_config], client: Union[ModbusSerialClient_, ModbusTcpClient_]) -> METERS: + def find_meter_client(meters: List[meter_config], + client: Union[ModbusSerialClient_, ModbusTcpClient_], + fault_state: FaultState) -> METERS: for meter_type, modbus_id in meters: - meter_client = meter_type(modbus_id, client) + meter_client = meter_type(modbus_id, client, fault_state) with client: try: if meter_client.get_voltages()[0] > 200: @@ -94,12 +95,14 @@ def client_factory(local_charge_point_num: int, fault_state: FaultState, created_client_handler: Optional[ClientHandler] = None) -> ClientHandler: serial_client, evse_ids = get_modbus_client( - local_charge_point_num, created_client_handler) + local_charge_point_num, created_client_handler, fault_state) return ClientHandler(local_charge_point_num, serial_client, evse_ids, fault_state) def get_modbus_client(local_charge_point_num: int, - created_client_handler: Optional[ClientHandler] = None): + created_client_handler: Optional[ClientHandler] = None, + fault_state: Optional[FaultState] = None) -> Tuple[Union[ModbusSerialClient_, ModbusTcpClient_], + List[int]]: tty_devices = list(Path("/dev/serial/by-path").glob("*")) log.debug("tty_devices"+str(tty_devices)) resolved_devices = [str(file.resolve()) for file in tty_devices] @@ -141,7 +144,7 @@ def get_modbus_client(local_charge_point_num: int, serial_client = ModbusSerialClient_(device) # Source immer an der Modbus-ID des Zählers fest machen, da diese immer fest ist. # Die USB-Anschlüsse können vertauscht sein. - detected_device = ClientHandler.find_meter_client(meters, serial_client) + detected_device = ClientHandler.find_meter_client(meters, serial_client, fault_state) if detected_device: break with ModifyLoglevelContext(log, logging.DEBUG):