Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

use configurable_vehicle instead of Device-class #729

Merged
merged 3 commits into from
Jun 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/Neues Gerät programmieren.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ Bei Hybrid-Systemen erfolgt die Verrechnung von Speicher-und PV-Leistung automat

Das Speichern, Runden, Loggen und eine Plausibilitätsprüfung der Werte erfolgt zentral und muss daher nicht in jedem Modul implementiert werden.

Wenn keine Einstellungsseiten in vue hinterlegt sind, sind die Einstellungen als json-Objekt editierbar.

### Kompatibilität mit 1.9
Damit das Modul auch unter 1.9 lauffähig ist, müssen -wie bisher- die unter [docs/legacy](https://github.com/openWB/core/tree/master/docs/samples/legacy?v30-12-2022) angegebenen Ordner erstellt werden und das Modul im UI hinzugefügt werden. Aufßerdem muss in der device.py die read_legacy-Funktion so implementiert werden, dass anhand des übergebenen Komponenten-Typs das Update der entsprechenden Komponente getriggert wird. Beim zentralen Speichern der ausgelesenen Werte wird automatisch erkannt, ob diese in die ramdisk (1.9) oder in den Broker (2.x) geschrieben werden müssen.

Expand Down
11 changes: 11 additions & 0 deletions docs/Neues Soc-Modul programmieren.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
Um die Programmierung neuer SoC-Module zu erleichtern, findet Ihr unter [docs/samples](https://github.com/openWB/core/tree/master/docs/samples?v30-12-2022) ein Muster _sample_vehicle_.

Die Muster sind nur als einheitlicher Ausgangspunkt zu verstehen! Es kann durchaus notwendig sein, weitere Einstellungs-Parameter hinzuzufügen oder bei einem Http-Request eine Authentifizierung durchzuführen. Beim Aufruf der _updater_-Funktion wird die Variable _soc_update_data_ übergeben. Darin sind aktuelle Daten aus der Regelung, wie zB Steckerstatus oder die geladene Energie seit Anstecken, enthalten, um Besonderheiten wie zB das Aufwecken des Fahrzeugs oder eine manuelle Berechnung während des Ladevorgangs umsetzen zu können.

Das Muster kopiert Ihr in den _packages/modules/vehicles/\*Name\*_-Ordner. Ordnername und Typ in config.py->Sample->type müssen identisch sein, damit das Gerät in der automatisch generierten Auswahlliste im UI angezeigt wird.

Das Speichern, Runden, Loggen und eine Plausibilitätsprüfung der Werte sowie die Prüfung, ob das Intervall zu SoC-Abfrage abgelaufen ist, erfolgt zentral und muss daher nicht in jedem Modul implementiert werden.

Wenn keine Einstellungsseite in vue für das SoC-Modul hinterlegt ist, sind die Einstellungen als json-Objekt editierbar.

_Bei Fragen programmiert Ihr das SoC-Modul erstmal, wie Ihr es versteht, und erstellt einen (Draft-)PR. Wir unterstützen Euch gerne per Review._
Empty file.
13 changes: 13 additions & 0 deletions docs/samples/sample_vehicle/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
class SampleVehicleSocConfiguration:
def __init__(self) -> None:
pass


class SampleVehicleSoc:
def __init__(self,
name: str = "Sample Vehicle",
type: str = "sample_vehicle",
configuration: SampleVehicleSocConfiguration = None) -> None:
self.name = name
self.type = type
self.configuration = configuration or SampleVehicleSocConfiguration()
26 changes: 26 additions & 0 deletions docs/samples/sample_vehicle/soc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
#!/usr/bin/env python3
import logging

from modules.common import req
from modules.common.abstract_device import DeviceDescriptor
from modules.common.abstract_soc import SocUpdateData
from modules.common.component_state import CarState
from modules.common.configurable_vehicle import ConfigurableVehicle
from modules.vehicles.sample_vehicle.config import SampleVehicleSoc

log = logging.getLogger(__name__)


def fetch(vehicle_config: SampleVehicleSoc, soc_update_data: SocUpdateData) -> CarState:
response = req.get_http_session().get(vehicle_config.configuration.ip_address, timeout=5).json()
# Soc und -falls vorhanden- Reichweite aus Antwort parsen
return CarState(soc, range)


def create_vehicle(vehicle_config: SampleVehicleSoc, vehicle: int):
def updater(soc_update_data: SocUpdateData) -> CarState:
return fetch(vehicle_config, soc_update_data)
return ConfigurableVehicle(vehicle_config=vehicle_config, component_updater=updater, vehicle=vehicle)


device_descriptor = DeviceDescriptor(configuration_factory=SampleVehicleSoc)
3 changes: 2 additions & 1 deletion packages/helpermodules/subdata.py
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,8 @@ def process_vehicle_topic(self, var: Dict[str, ev.Ev], msg: mqtt.MQTTMessage):
else:
mod = importlib.import_module(".vehicles."+config["type"]+".soc", "modules")
config = dataclass_from_dict(mod.device_descriptor.configuration_factory, config)
var["ev"+index].soc_module = mod.Soc(config, index)
var["ev"+index].soc_module = (mod.Soc if hasattr(mod, "Soc")
else mod.create_vehicle)(config, index)
self.event_soc.set()
elif re.search("/vehicle/[0-9]+/control_parameter/", msg.topic) is not None:
self.set_json_payload_class(
Expand Down
25 changes: 25 additions & 0 deletions packages/modules/common/configurable_vehicle.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from typing import TypeVar, Generic, Callable

from modules.common import store
from modules.common.abstract_soc import SocUpdateData
from modules.common.component_context import SingleComponentUpdateContext
from modules.common.component_state import CarState
from modules.common.fault_state import ComponentInfo

T_VEHICLE_CONFIG = TypeVar("T_VEHICLE_CONFIG")


class ConfigurableVehicle(Generic[T_VEHICLE_CONFIG]):
def __init__(self,
vehicle_config: T_VEHICLE_CONFIG,
component_updater: Callable[[T_VEHICLE_CONFIG, SocUpdateData], CarState],
vehicle: int) -> None:
self.__component_updater = component_updater
self.vehicle_config = vehicle_config
self.vehicle = vehicle
self.store = store.get_car_value_store(self.vehicle)
self.component_info = ComponentInfo(self.vehicle, self.vehicle_config.name, "vehicle")

def update(self, soc_update_data: SocUpdateData):
with SingleComponentUpdateContext(self.component_info):
self.store.set(self.__component_updater(soc_update_data))
10 changes: 5 additions & 5 deletions packages/modules/update_soc_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from control.chargepoint.chargepoint import Chargepoint, Get, Log, Set
from control.ev import Ev, EvTemplate, EvTemplateData
from modules.common.abstract_soc import SocUpdateData
from modules.vehicles.tesla.soc import Soc
from modules.vehicles.tesla.soc import create_vehicle
from modules.update_soc import UpdateSoc


Expand Down Expand Up @@ -51,12 +51,12 @@ def test_get_ev_state(ev_num: int,
"soc_module, force_soc_update, soc_interval_expired, expected_threads_update",
[
pytest.param(None, False, False, [], id="soc module none"),
pytest.param(Mock(spec=Soc), False, True, ["soc_ev0"], id="interval expired"),
pytest.param(Mock(spec=Soc), True, False, ["soc_ev0"], id="force soc update"),
pytest.param(Mock(spec=Soc), False, False, [], id="no soc request needed"),
pytest.param(Mock(spec=create_vehicle, update=Mock()), False, True, ["soc_ev0"], id="interval expired"),
pytest.param(Mock(spec=create_vehicle, update=Mock()), True, False, ["soc_ev0"], id="force soc update"),
pytest.param(Mock(spec=create_vehicle, update=Mock()), False, False, [], id="no soc request needed"),
]
)
def test_get_threads(soc_module: Optional[Soc],
def test_get_threads(soc_module: Optional[create_vehicle],
force_soc_update: bool,
soc_interval_expired: bool,
expected_threads_update: List[str],
Expand Down
72 changes: 35 additions & 37 deletions packages/modules/vehicles/tesla/soc.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,53 +2,51 @@
import json
import logging
import time
from typing import Dict, List, Union
from typing import List

from dataclass_utils import asdict, dataclass_from_dict
from helpermodules.cli import run_using_positional_cli_args
from modules.common import store
from modules.common.abstract_device import DeviceDescriptor
from modules.common.abstract_soc import AbstractSoc, SocUpdateData
from modules.common.component_context import SingleComponentUpdateContext
from modules.common.abstract_soc import SocUpdateData
from modules.common.component_state import CarState
from modules.common.fault_state import ComponentInfo, FaultState
from modules.common.configurable_vehicle import ConfigurableVehicle
from modules.common.fault_state import FaultState
from modules.vehicles.tesla import api
from modules.vehicles.tesla.config import TeslaSoc, TeslaSocConfiguration, TeslaSocToken

log = logging.getLogger(__name__)


class Soc(AbstractSoc):
def __init__(self, soc_config: Union[Dict, TeslaSoc], vehicle: int):
self.soc_config = dataclass_from_dict(TeslaSoc, soc_config)
self.vehicle = vehicle
self.store = store.get_car_value_store(self.vehicle)
self.component_info = ComponentInfo(self.vehicle, self.soc_config.name, "vehicle")

def update(self, soc_update_data: SocUpdateData) -> None:
with SingleComponentUpdateContext(self.component_info):
self.soc_config.configuration.token = api.validate_token(self.soc_config.configuration.token)
if soc_update_data.charge_state is False:
self.__wake_up_car()
soc, range = api.request_soc_range(
vehicle=self.soc_config.configuration.tesla_ev_num, token=self.soc_config.configuration.token)
self.store.set(CarState(soc, range))

def __wake_up_car(self):
log.debug("Tesla"+str(self.vehicle)+": Waking up car.")
counter = 0
state = "offline"
while counter <= 12:
state = api.post_wake_up_command(
vehicle=self.soc_config.configuration.tesla_ev_num, token=self.soc_config.configuration.token)
if state == "online":
break
counter = counter+1
time.sleep(5)
log.debug("Tesla "+str(self.vehicle)+": Loop: "+str(counter)+", State: "+str(state))
log.info("Tesla "+str(self.vehicle)+": Status nach Aufwecken: "+str(state))
if state != "online":
raise FaultState.error("EV"+str(self.vehicle)+" konnte nicht geweckt werden.")
def fetch(vehicle_config: TeslaSoc, soc_update_data: SocUpdateData) -> CarState:
vehicle_config.configuration.token = api.validate_token(vehicle_config.configuration.token)
if soc_update_data.charge_state is False:
_wake_up_car(vehicle_config)
soc, range = api.request_soc_range(
vehicle=vehicle_config.configuration.tesla_ev_num, token=vehicle_config.configuration.token)
return CarState(soc, range)


def _wake_up_car(vehicle_config: TeslaSoc):
log.debug("Waking up car.")
counter = 0
state = "offline"
while counter <= 12:
state = api.post_wake_up_command(
vehicle=vehicle_config.configuration.tesla_ev_num, token=vehicle_config.configuration.token)
if state == "online":
break
counter = counter+1
time.sleep(5)
log.debug("Loop: "+str(counter)+", State: "+str(state))
log.info("Status nach Aufwecken: "+str(state))
if state != "online":
raise FaultState.error("EV konnte nicht geweckt werden.")


def create_vehicle(vehicle_config: TeslaSoc, vehicle: int):
def updater(soc_update_data: SocUpdateData) -> CarState:
return fetch(vehicle_config, soc_update_data)
return ConfigurableVehicle(vehicle_config=vehicle_config, component_updater=updater, vehicle=vehicle)


def read_legacy(id: int,
Expand All @@ -63,7 +61,7 @@ def read_legacy(id: int,

with open(token_file, "r") as f:
token = json.load(f)
soc = Soc(TeslaSoc(configuration=TeslaSocConfiguration(
soc = create_vehicle(TeslaSoc(configuration=TeslaSocConfiguration(
tesla_ev_num=tesla_ev_num, token=dataclass_from_dict(TeslaSocToken, token))), id)
soc.update(SocUpdateData(charge_state=charge_state))
with open(token_file, "w") as f:
Expand Down
8 changes: 4 additions & 4 deletions packages/modules/vehicles/tesla/soc_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from modules.common.abstract_soc import SocUpdateData
from modules.common.component_context import SingleComponentUpdateContext
from modules.vehicles.tesla import api
from modules.vehicles.tesla.soc import Soc
from modules.vehicles.tesla.soc import create_vehicle
from modules.vehicles.tesla.config import TeslaSoc, TeslaSocConfiguration, TeslaSocToken


Expand All @@ -27,7 +27,7 @@ def set_up(self, monkeypatch):

def test_update_updates_value_store_no_chargepoint(self, monkeypatch):
# execution
Soc(TeslaSoc(configuration=TeslaSocConfiguration(
create_vehicle(TeslaSoc(configuration=TeslaSocConfiguration(
tesla_ev_num=0, token=self.token)), 0).update(SocUpdateData())

# evaluation
Expand All @@ -39,7 +39,7 @@ def test_update_updates_value_store_no_chargepoint(self, monkeypatch):

def test_update_updates_value_store_not_charging(self, monkeypatch):
# execution
Soc(TeslaSoc(configuration=TeslaSocConfiguration(
create_vehicle(TeslaSoc(configuration=TeslaSocConfiguration(
tesla_ev_num=0, token=self.token)), 0).update(SocUpdateData())

# evaluation
Expand All @@ -55,7 +55,7 @@ def test_update_passes_errors_to_context(self, monkeypatch):
self.mock_request_soc_range.side_effect = dummy_error

# execution
Soc(TeslaSoc(configuration=TeslaSocConfiguration(
create_vehicle(TeslaSoc(configuration=TeslaSocConfiguration(
tesla_ev_num=0, token=self.token)), 0).update(SocUpdateData())

# evaluation
Expand Down