Skip to content

Commit

Permalink
Merge pull request #1784 from yankee42/manual-soc-python
Browse files Browse the repository at this point in the history
convert SoC "manual+calculation" to python
  • Loading branch information
snaptec authored Dec 10, 2021
2 parents ab3d5e9 + d06940b commit 9a2fc93
Show file tree
Hide file tree
Showing 10 changed files with 222 additions and 152 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/github-actions-python.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,4 @@ jobs:
path: packages
- name: Test with pytest
run: |
cd packages && python -m pytest
PYTHONPATH=packages python -m pytest packages modules
135 changes: 4 additions & 131 deletions modules/soc_manual/main.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,135 +2,8 @@

OPENWBBASEDIR=$(cd `dirname $0`/../../ && pwd)
RAMDISKDIR="$OPENWBBASEDIR/ramdisk"
MODULEDIR=$(cd `dirname $0` && pwd)
DMOD="EVSOC"
CHARGEPOINT=$1
CHARGEPOINT=${1:-1}

# check if config file is already in env
if [[ -z "$debug" ]]; then
echo "soc_manual: Seems like openwb.conf is not loaded. Reading file."
# try to load config
. $OPENWBBASEDIR/loadconfig.sh
# load helperFunctions
. $OPENWBBASEDIR/helperFunctions.sh
fi

case $CHARGEPOINT in
2)
# second charge point
manualSocFile="$RAMDISKDIR/manual_soc_lp2"
manualMeterFile="$RAMDISKDIR/manual_soc_meter_lp2"
socFile="$RAMDISKDIR/soc1"
soctimerfile="$RAMDISKDIR/soctimer1"
socIntervall=1 # update every minute if script is called every 10 seconds
meterFile="$RAMDISKDIR/llkwhs1"
akkug=$akkuglp2
efficiency=$wirkungsgradlp2
;;
*)
# defaults to first charge point for backward compatibility
# set CHARGEPOINT in case it is empty (needed for logging)
CHARGEPOINT=1
manualSocFile="$RAMDISKDIR/manual_soc_lp1"
manualMeterFile="$RAMDISKDIR/manual_soc_meter_lp1"
socFile="$RAMDISKDIR/soc"
soctimerfile="$RAMDISKDIR/soctimer"
socIntervall=1 # update every minute if script is called every 10 seconds
meterFile="$RAMDISKDIR/llkwh"
akkug=$akkuglp1
efficiency=$wirkungsgradlp1
;;
esac

incrementTimer(){
case $dspeed in
1)
# Regelgeschwindigkeit 10 Sekunden
ticksize=1
;;
2)
# Regelgeschwindigkeit 20 Sekunden
ticksize=2
;;
3)
# Regelgeschwindigkeit 60 Sekunden
ticksize=1
;;
*)
# Regelgeschwindigkeit unbekannt
ticksize=1
;;
esac
soctimer=$((soctimer+$ticksize))
echo $soctimer > $soctimerfile
}

soctimer=$(<$soctimerfile)
openwbDebugLog ${DMOD} 1 "Lp$CHARGEPOINT: timer = $soctimer"

if (( soctimer < socIntervall )); then
openwbDebugLog ${DMOD} 1 "Lp$CHARGEPOINT: Nothing to do yet. Incrementing timer."
incrementTimer
else
openwbDebugLog ${DMOD} 1 "Lp$CHARGEPOINT: Calculating manual SoC"
# reset timer
echo 0 > $soctimerfile

# read current meter
if [[ -f "$meterFile" ]]; then
currentMeter=$(<$meterFile)
openwbDebugLog ${DMOD} 1 "Lp$CHARGEPOINT: currentMeter: $currentMeter"

# read manual Soc
if [[ -f "$manualSocFile" ]]; then
manualSoc=$(<$manualSocFile)
else
# set manualSoc to 0 as a starting point
manualSoc=0
echo $manualSoc > $manualSocFile
fi
openwbDebugLog ${DMOD} 1 "Lp$CHARGEPOINT: manual SoC: $manualSoc"

# read manualMeterFile if file exists and manualMeterFile is newer than manualSocFile
if [[ -f "$manualMeterFile" ]] && [ "$manualMeterFile" -nt "$manualSocFile" ]; then
manualMeter=$(<$manualMeterFile)
else
# manualMeterFile does not exist or is outdated
# update manualMeter with currentMeter
manualMeter=$currentMeter
echo $manualMeter > $manualMeterFile
fi
openwbDebugLog ${DMOD} 1 "Lp$CHARGEPOINT: manualMeter: $manualMeter"

# read current soc
if [[ -f "$socFile" ]]; then
currentSoc=$(<$socFile)
else
currentSoc=$manualSoc
echo $currentSoc > $socFile
fi
openwbDebugLog ${DMOD} 1 "Lp$CHARGEPOINT: currentSoc: $currentSoc"

# calculate newSoc
currentMeterDiff=$(echo "scale=5;$currentMeter - $manualMeter" | bc)
openwbDebugLog ${DMOD} 1 "Lp$CHARGEPOINT: currentMeterDiff: $currentMeterDiff"
currentEffectiveMeterDiff=$(echo "scale=5;$currentMeterDiff * $efficiency / 100" | bc)
openwbDebugLog ${DMOD} 1 "Lp$CHARGEPOINT: currentEffectiveMeterDiff: $currentEffectiveMeterDiff ($efficiency %)"
currentSocDiff=$(echo "scale=5;100 / $akkug * $currentEffectiveMeterDiff" | bc | awk '{printf"%d\n",$1}')
openwbDebugLog ${DMOD} 1 "Lp$CHARGEPOINT: currentSocDiff: $currentSocDiff"
newSoc=$(echo "$manualSoc + $currentSocDiff" | bc)
if (( newSoc > 100 )); then
newSoc=100
fi
if (( newSoc < 0 )); then
newSoc=0
fi
openwbDebugLog ${DMOD} 1 "Lp$CHARGEPOINT: newSoc: $newSoc"
echo $newSoc > $socFile
else
# no current meter value for calculation -> Exit
openwbDebugLog ${DMOD} 0 "Lp$CHARGEPOINT: ERROR: no meter value for calculation! ($meterFile)"
fi
fi

openwbDebugLog ${DMOD} 1 "Lp$CHARGEPOINT: --- Manual SoC end ---"
efficiency_var_name=wirkungsgradlp"$CHARGEPOINT"
battery_size_var_name=akkuglp"$CHARGEPOINT"
python3 "${OPENWBBASEDIR}/modules/soc_manual/soc_manual.py" "$CHARGEPOINT" "${!efficiency_var_name}" "${!battery_size_var_name}" &>> "$RAMDISKDIR/soc.log"
65 changes: 65 additions & 0 deletions modules/soc_manual/soc_manual.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import argparse
import logging
import sys

from helpermodules.log import setup_logging_stdout
from modules.common.store import ramdisk_write, ramdisk_read_float

setup_logging_stdout()
log = logging.getLogger("SoC Manual")


def run(charge_point: int, battery_size: float, efficiency: float):
"""
:param charge_point: charge point number >= 1
:param battery_size: battery size in kWh
:param efficiency: efficiency as fraction (usually between 0 and 1)
:return:
"""
file_soc_start = "manual_soc_lp{}".format(charge_point)
file_meter_start = "manual_soc_meter_lp{}".format(charge_point)
# SoC-file contains float with SoC 0..100:
file_soc = "soc{}".format(charge_point - 1)
# meter file contains float with total energy ever charged at this charge point in kWh:
file_meter = "llkwhs{}".format(charge_point - 1)
if charge_point == 1:
# For historic reasons some file names for charge point one do not fit the usual pattern:
file_soc = "soc"
file_meter = "llkwh"

meter_now = ramdisk_read_float(file_meter)
try:
meter_start = ramdisk_read_float(file_meter_start)
soc_start = ramdisk_read_float(file_soc_start) / 100
except FileNotFoundError:
soc_now = ramdisk_read_float(file_soc) / 100
log.warning("Not initialized. Begin with meter=%g kWh, soc=%g%%", meter_now, soc_now * 100)
ramdisk_write(file_meter_start, meter_now)
ramdisk_write(file_soc_start, soc_now * 100, 1)
return

energy_counted = meter_now - meter_start
energy_battery_gain = energy_counted * efficiency
battery_soc_gain = energy_battery_gain / battery_size
soc_new = soc_start + battery_soc_gain
log.debug("Charged: %g kWh -> %g kWh = %g kWh", meter_start, meter_now, energy_counted)
log.debug(
"SoC-Gain: Charged (%g kWh) * efficiency (%g%%) / battery-size (%g kWh) = %.1f%%",
energy_counted, efficiency * 100, battery_size, battery_soc_gain * 100
)
log.info("%g%% + %g kWh = %g%%", soc_start * 100, energy_battery_gain, soc_new * 100)
ramdisk_write(file_soc, soc_new * 100, digits=0)


def run_command_line():
parser = argparse.ArgumentParser(description='Calculate SoC based on kWh charged')
parser.add_argument("charge_point", type=int)
parser.add_argument("efficiency", type=float)
parser.add_argument("battery_size", type=float)
args = parser.parse_args()
run(charge_point=args.charge_point, efficiency=args.efficiency / 100, battery_size=args.battery_size)


if __name__ == '__main__':
run_command_line()
60 changes: 60 additions & 0 deletions modules/soc_manual/soc_manual_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import pytest

import soc_manual
from test_utils.mock_ramdisk import MockRamdisk


@pytest.fixture(scope='function')
def mock_ramdisk(monkeypatch):
return MockRamdisk(monkeypatch)


def test_charge_point_1(mock_ramdisk):
# setup
mock_ramdisk["manual_soc_lp1"] = "12.6"
mock_ramdisk["manual_soc_meter_lp1"] = "42.123"
mock_ramdisk["soc"] = "42"
mock_ramdisk["llkwh"] = "52.123"

# execution
soc_manual.run(charge_point=1, battery_size=100, efficiency=.9)

# evaluation
assert mock_ramdisk["soc"] == "21"


def test_charge_point_2(mock_ramdisk):
# setup
mock_ramdisk["manual_soc_lp2"] = "12.6"
mock_ramdisk["manual_soc_meter_lp2"] = "42.123"
mock_ramdisk["soc1"] = "42"
mock_ramdisk["llkwhs1"] = "52.123"

# execution
soc_manual.run(charge_point=2, battery_size=100, efficiency=.9)

# evaluation
assert mock_ramdisk["soc1"] == "21"


@pytest.mark.parametrize(
"manual_files",
[
{"manual_soc_meter_lp2": "42.123"},
{"manual_soc_lp2": "15"},
{}
]
)
def test_reinitializes_if_state_is_missing(mock_ramdisk, manual_files: dict):
# setup
mock_ramdisk["soc1"] = "42"
mock_ramdisk["llkwhs1"] = "52.123"
mock_ramdisk.files.update(manual_files)

# execution
soc_manual.run(charge_point=2, battery_size=100, efficiency=.9)

# evaluation
assert mock_ramdisk["soc1"] == "42" # Expect value to be unchanged
assert mock_ramdisk["manual_soc_lp2"] == "42.0"
assert mock_ramdisk["manual_soc_meter_lp2"] == "52.123"
2 changes: 2 additions & 0 deletions packages/modules/common/store/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@
from modules.common.store._car import get_car_value_store
from modules.common.store._counter import get_counter_value_store
from modules.common.store._inverter import get_inverter_value_store
from modules.common.store._ramdisk import ramdisk_write, ramdisk_read, ramdisk_read_float, ramdisk_read_int, \
RAMDISK_PATH
33 changes: 29 additions & 4 deletions packages/modules/common/store/_ramdisk.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
from pathlib import Path
from typing import Iterable, Optional
from typing import Iterable, Optional, TypeVar, Callable

from modules.common.store._util import get_rounding_function_by_digits, process_error

RAMDISK_PATH = Path(__file__).resolve().parents[4] / "ramdisk"

T = TypeVar('T')


class RamdiskReadError(Exception):
def __init__(self, file: str, content: str, message: str):
super().__init__("Error reading ramdisk file <{}>, content=<{}>: {}".format(file, content, message))


def ramdisk_write_to_files(prefix: str, values: Iterable, digits: int = None):
for index, value in enumerate(values):
Expand All @@ -13,8 +20,26 @@ def ramdisk_write_to_files(prefix: str, values: Iterable, digits: int = None):

def ramdisk_write(file: str, value, digits: Optional[int] = None) -> None:
try:
rounding = get_rounding_function_by_digits(digits)
with open(str(RAMDISK_PATH / file), "w") as f:
f.write(str(rounding(value)))
(RAMDISK_PATH / file).write_text(str(get_rounding_function_by_digits(digits)(value)))
except Exception as e:
process_error(e)


def ramdisk_read(file: str) -> str:
return (RAMDISK_PATH / file).read_text()


def ramdisk_read_mapping(file: str, mapper: Callable[[str], T], error_message: str) -> T:
file_content = ramdisk_read(file)
try:
return mapper(file_content)
except ValueError as e:
raise RamdiskReadError(file, file_content, error_message) from e


def ramdisk_read_int(file: str) -> int:
return ramdisk_read_mapping(file, int, "expected int")


def ramdisk_read_float(file: str) -> float:
return ramdisk_read_mapping(file, float, "expected float")
Empty file added packages/test_utils/__init__.py
Empty file.
42 changes: 42 additions & 0 deletions packages/test_utils/mock_ramdisk.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
from pathlib import Path

from modules.common.store import RAMDISK_PATH


class MockRamdisk:
def __init__(self, monkeypatch):
self.files = {}
original_read_text = Path.read_text
original_write_text = Path.write_text

def mock_read_text(file: Path):
try:
relative = file.relative_to(RAMDISK_PATH)
except ValueError:
return original_read_text(file)
return self[str(relative)]

def mock_write_text(file: Path, content: str):
try:
relative = file.relative_to(RAMDISK_PATH)
except ValueError:
original_write_text(file, content)
return
self[str(relative)] = content

monkeypatch.setattr(Path, 'read_text', mock_read_text)
monkeypatch.setattr(Path, 'write_text', mock_write_text)

def __setitem__(self, key, value):
if not isinstance(value, str):
raise TypeError("only strings are allowed")
self.files[key] = value

def __getitem__(self, item):
try:
return self.files.__getitem__(item)
except KeyError:
raise FileNotFoundError()

def __str__(self):
return "Mock ramdisk: " + str(self.files)
1 change: 0 additions & 1 deletion runs/initRamdisk.sh
Original file line number Diff line number Diff line change
Expand Up @@ -446,7 +446,6 @@ initRamdisk(){
do
for f in \
"pluggedladunglp${i}startkwh:openWB/lp/${i}/plugStartkWh:0" \
"manual_soc_lp${i}:openWB/lp/${i}/manualSoc:0" \
"pluggedladungaktlp${i}:openWB/lp/${i}/pluggedladungakt:0" \
"lp${i}phasen::0" \
"lp${i}enabled::1" \
Expand Down
Loading

0 comments on commit 9a2fc93

Please sign in to comment.