Skip to content

Commit

Permalink
extend meter check, request values only once per cycle
Browse files Browse the repository at this point in the history
  • Loading branch information
LKuemmel committed Sep 18, 2024
1 parent 134b827 commit cfcb380
Show file tree
Hide file tree
Showing 14 changed files with 207 additions and 149 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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

log = logging.getLogger(__name__)
Expand Down Expand Up @@ -66,7 +66,10 @@ def get_values(self) -> None:
)

if json_rsp.get("voltages"):
meter_msg = check_meter_values(json_rsp["voltages"])
meter_msg = check_meter_values(CounterState(voltages=json_rsp["voltages"],
currents=json_rsp["currents"],
powers=json_rsp["powers"],
power=json_rsp["power_all"]))
if meter_msg:
self.fault_state.warning(meter_msg)
chargepoint_state.voltages = json_rsp["voltages"]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,29 +76,27 @@ def get_values(self) -> None:
with self.client_error_context:
try:
self.delay_second_cp(self.CP1_DELAY)
with self._client.client:
self._client.check_hardware(self.fault_state)
if self.version is False:
raise ValueError(
"Firmware des openWB Satellit ist nicht mit openWB 2 kompatibel. "
"Bitte den Support kontaktieren.")
currents = self._client.meter_client.get_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()
evse_state, counter_state = self._client.request_and_check_hardware()
if self.version is False:
raise ValueError(
"Firmware des openWB Satellit ist nicht mit openWB 2 kompatibel. "
"Bitte den Support kontaktieren.")

currents = counter_state.currents
phases_in_use = sum(1 for current in currents if current > 3)

chargepoint_state = ChargepointState(
power=self._client.meter_client.get_power()[1],
currents=currents,
imported=self._client.meter_client.get_imported(),
exported=0,
voltages=self._client.meter_client.get_voltages(),
plug_state=plug_state,
charge_state=charge_state,
phases_in_use=phases_in_use,
serial_number=self._client.meter_client.get_serial_number()
)
chargepoint_state = ChargepointState(
power=counter_state.power,
currents=currents,
imported=counter_state.imported,
exported=0,
voltages=counter_state.voltages,
plug_state=evse_state.plug_state,
charge_state=evse_state.charge_state,
phases_in_use=phases_in_use,
serial_number=counter_state.serial_number,
)
self.store.set(chargepoint_state)
self.client_error_context.reset_error_counter()
except AttributeError:
self._create_client()
self._validate_version()
Expand All @@ -115,7 +113,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:
Expand All @@ -130,7 +127,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)
Expand Down
8 changes: 8 additions & 0 deletions packages/modules/common/component_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -202,3 +202,11 @@ def __init__(self,
class RcrState:
def __init__(self, override_value: float) -> None:
self.override_value = override_value


class EvseState:
def __init__(self, plug_state: bool, charge_state: bool, set_current: int, version: int) -> None:
self.plug_state = plug_state
self.charge_state = charge_state
self.set_current = set_current
self.version = version
15 changes: 12 additions & 3 deletions packages/modules/common/evse.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -41,8 +42,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
Expand All @@ -54,6 +55,14 @@ def get_firmware_version(self) -> int:
version = self.client.read_holding_registers(1005, ModbusDataType.UINT_16, unit=self.id)
return 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,
version=self.get_firmware_version())
return state

def is_precise_current_active(self) -> bool:
time.sleep(0.1)
value = self.client.read_holding_registers(2005, ModbusDataType.UINT_16, unit=self.id)
Expand Down
46 changes: 30 additions & 16 deletions packages/modules/common/hardware_check.py
Original file line number Diff line number Diff line change
@@ -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_
Expand All @@ -16,21 +17,26 @@
"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. "
f"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."


def check_meter_values(voltages: List[float]) -> Optional[str]:
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
(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
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_BROKEN
return None


class ClientHandlerProtocol(Protocol):
Expand Down Expand Up @@ -60,16 +66,22 @@ 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:
with self.client:
evse_state = self.evse_client.get_evse_state()
if evse_state.version > EVSE_MIN_FIRMWARE:
evse_check_passed = True
else:
evse_check_passed = False
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_):
Expand All @@ -87,12 +99,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:
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
return True, check_meter_values(self.meter_client.get_voltages())
return True, check_meter_values(counter_state), counter_state
except Exception:
return False, METER_PROBLEM
return False, METER_PROBLEM, None
76 changes: 42 additions & 34 deletions packages/modules/common/hardware_check_test.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
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.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, 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

Expand Down Expand Up @@ -42,59 +42,67 @@ 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_meter_client = Mock(spec=sdm.Sdm630_72, get_voltages=Mock(
side_effect=meter_side_effect, return_value=meter_return_value))
evse_state = Mock(spec=EvseState, version=evse_return_value)
mock_evse_client = Mock(spec=Evse, get_evse_state=Mock(side_effect=evse_side_effect, return_value=evse_state))
mock_evse_facotry = Mock(spec=Evse, return_value=mock_evse_client)
monkeypatch.setattr(ClientHandler, "_evse_factory", mock_evse_facotry)

counter_state_mock = Mock(spec=CounterState, side_effect=meter_side_effect,
voltages=meter_return_value, currents=[0, 0, 0], powers=[0, 0, 0], power=0)
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)

handle_exception_mock = Mock(side_effect=handle_exception_side_effect, return_value=handle_exception_return_value)
monkeypatch.setattr(SeriesHardwareCheckMixin, "handle_exception", handle_exception_mock)

mock_modbus_client = MagicMock(spec=client_spec, address=SAMPLE_IP, port=SAMPLE_PORT)
mock_modbus_client.__enter__.return_value = mock_modbus_client
enter_mock = Mock(return_value=None)
exit_mock = Mock(return_value=True)
client = Mock(spec=client_spec, __enter__=enter_mock, __exit__=exit_mock)

# execution and evaluation
with pytest.raises(Exception, match=re.escape(expected_error_msg)):
ClientHandler(0, mock_modbus_client, [1], Mock())
with pytest.raises(Exception, match=expected_error_msg):
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)
evse_state = Mock(spec=EvseState, version=17)
mock_evse_client = Mock(spec=Evse, get_evse_state=Mock(return_value=evse_state))
mock_evse_facotry = Mock(spec=Evse, 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)
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, 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, id="zweiphasig, L1 defekt"),
pytest.param([230, 230, 230], 0, None, id="dreiphasig"),
pytest.param([0, 230, 230], 0, METER_BROKEN_VOLTAGES, id="dreiphasig, L1 defekt"),
pytest.param([230, 0, 230], 0, METER_BROKEN_VOLTAGES, id="dreiphasig, L2 defekt"),
pytest.param([230]*3, 100, METER_BROKEN, id="Phantom-Leistung"),
]
)
def test_check_meter_values(voltages, expected_msg, monkeypatch):
# setup & execution
msg = check_meter_values(voltages)
def test_check_meter(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)
Expand Down
12 changes: 12 additions & 0 deletions packages/modules/common/lovato.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
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.modbus import ModbusDataType


Expand Down Expand Up @@ -36,3 +37,14 @@ 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()
return CounterState(
power=power,
voltages=self.get_voltages(),
currents=self.get_currents(),
powers=powers,
power_factors=self.get_power_factors(),
frequency=self.get_frequency()
)
Loading

0 comments on commit cfcb380

Please sign in to comment.