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

evcc soc module #1474

Merged
merged 9 commits into from
Mar 27, 2024
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 packages/modules/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
sys.modules['skodaconnect'] = type(sys)('skodaconnect')
sys.modules['skodaconnect.Connection'] = type(sys)('skodaconnect.Connection')
sys.modules['socketserver'] = type(sys)('socketserver')
sys.modules['grpc'] = type(sys)('grpc')


# sys.modules['telnetlib3'] = type(sys)('telnetlib3')

Expand Down
91 changes: 91 additions & 0 deletions packages/modules/vehicles/evcc/api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
#!/usr/bin/env python3
import logging
import time
import grpc
import modules.vehicles.evcc.vehicle_pb2 as vehicle_pb2
import modules.vehicles.evcc.vehicle_pb2_grpc as vehicle_pb2_grpc

from modules.common.abstract_vehicle import VehicleUpdateData
from modules.vehicles.evcc.config import EVCCVehicleSocConfiguration, EVCCVehicleSoc
from modules.common.component_state import CarState
from typing import Mapping, cast
from dataclass_utils import asdict
from helpermodules.pub import Pub

log = logging.getLogger(__name__)
evcc_endpoint = 'sponsor.evcc.io:8080'
gRPCRetryTime = 5
gRPCRetryCount = 5
gRPCRetryResponse = 'must retry'


def write_vehicle_id_mqtt(topic: str, vehicle_id: int, config: EVCCVehicleSocConfiguration):
try:
config.vehicle_id = vehicle_id
value: EVCCVehicleSoc = EVCCVehicleSoc(configuration=config)
log.debug("saving vehicle_id: " + str(vehicle_id))
Pub().pub(topic, asdict(value))
except Exception as e:
log.exception('Token mqtt write exception ' + str(e))


def create_vehicle(config: EVCCVehicleSocConfiguration, stub: vehicle_pb2_grpc.VehicleStub) -> int:
response = stub.New(
vehicle_pb2.NewRequest(
token=config.sponsor_token,
type=config.vehicle_type,
config=cast(Mapping[str, str], {
'User': config.user_id,
'Password': config.password,
'VIN': config.VIN # VIN is optional, but must not be None
})
)
)
return response.vehicle_id


def fetch_soc(
evcc_config: EVCCVehicleSocConfiguration,
vehicle_update_data: VehicleUpdateData,
vehicle: int
) -> CarState:
log.debug("Fetching EVCC SOC")
with grpc.secure_channel(evcc_endpoint, grpc.ssl_channel_credentials()) as channel:
stub = vehicle_pb2_grpc.VehicleStub(channel)

if not evcc_config.vehicle_id: # create and fetch vehicle id if not included in config
vehicle_to_fetch = create_vehicle(evcc_config, stub)
log.debug("Vehicle client received: " + str(vehicle_to_fetch))

# saving vehicle id in config
topic = "openWB/set/vehicle/" + str(vehicle) + "/soc_module/config"
write_vehicle_id_mqtt(topic, vehicle_to_fetch, evcc_config)
else:
log.debug("Vehicle id found in config: " + str(evcc_config.vehicle_id))
vehicle_to_fetch = evcc_config.vehicle_id
log.debug("Fetching SoC for vehicle id: " + str(vehicle_to_fetch)) # fetch SoC

RetryCounter = 0
while RetryCounter < gRPCRetryCount: # retry fetching SoC if necessary
try:
response = stub.SoC(
vehicle_pb2.SoCRequest(
token=evcc_config.sponsor_token,
vehicle_id=vehicle_to_fetch
)
)
log.debug("SoC received: " + str(response.soc)) # return SoC, exit loop
break
except grpc.RpcError as rpc_error:
if rpc_error.details() == gRPCRetryResponse: # need to wait and retry
log.debug(f"No SoC retrieved, waiting {gRPCRetryTime}s in attempt no {RetryCounter} to retry")
time.sleep(gRPCRetryTime)
log.debug("retrying now...")
RetryCounter += 1
else: # some other error, raise exception and exit
raise grpc.RpcError(rpc_error)

if RetryCounter >= gRPCRetryCount:
raise Exception(f"no SoC received after {gRPCRetryCount} retries with {gRPCRetryTime}s delay")
return CarState(
soc=response.soc)
29 changes: 29 additions & 0 deletions packages/modules/vehicles/evcc/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from typing import Optional


class EVCCVehicleSocConfiguration:
def __init__(self,
vehicle_id: Optional[int] = None,
sponsor_token: Optional[str] = None,
user_id: Optional[str] = None,
password: Optional[str] = None,
VIN: Optional[str] = "",
vehicle_type: Optional[str] = None,
calculate_soc: bool = False) -> None:
self.vehicle_id = vehicle_id
self.calculate_soc = calculate_soc
self.user_id = user_id
self.password = password
self.sponsor_token = sponsor_token
self.vehicle_type = vehicle_type
self.VIN = VIN


class EVCCVehicleSoc:
def __init__(self,
name: str = "EVCC",
type: str = "evcc",
configuration: EVCCVehicleSocConfiguration = None) -> None:
self.name = name
self.type = type
self.configuration = configuration or EVCCVehicleSocConfiguration()
29 changes: 29 additions & 0 deletions packages/modules/vehicles/evcc/protos/vehicle.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
syntax = "proto3";

// protoc -I=protos --python_out=. vehicle.proto

option go_package = "proto/pb";

service Vehicle {
rpc New (NewRequest) returns (NewReply) {}
rpc SoC (SoCRequest) returns (SoCReply) {}
}

message NewRequest {
string token = 1;
string type = 2;
map<string,string> config = 3;
}

message NewReply {
int64 vehicle_id = 1;
}

message SoCRequest {
string token = 1;
int64 vehicle_id = 2;
}

message SoCReply {
double soc = 1;
}
21 changes: 21 additions & 0 deletions packages/modules/vehicles/evcc/soc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#!/usr/bin/env python3
import logging

from modules.common.abstract_device import DeviceDescriptor
from modules.common.abstract_vehicle import VehicleUpdateData
from modules.common.component_state import CarState
from modules.common.configurable_vehicle import ConfigurableVehicle
from modules.vehicles.evcc.config import EVCCVehicleSoc
from modules.vehicles.evcc.api import fetch_soc

log = logging.getLogger(__name__)


def create_vehicle(vehicle_config: EVCCVehicleSoc, vehicle: int):
def updater(vehicle_update_data: VehicleUpdateData) -> CarState:
return fetch_soc(vehicle_config.configuration, vehicle_update_data, vehicle)
return ConfigurableVehicle(vehicle_config=vehicle_config, component_updater=updater, vehicle=vehicle,
calc_while_charging=vehicle_config.configuration.calculate_soc)


device_descriptor = DeviceDescriptor(configuration_factory=EVCCVehicleSoc)
40 changes: 40 additions & 0 deletions packages/modules/vehicles/evcc/vehicle_pb2.py

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

44 changes: 44 additions & 0 deletions packages/modules/vehicles/evcc/vehicle_pb2.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# flake8: noqa
from google.protobuf.internal import containers as _containers
from google.protobuf import descriptor as _descriptor
from google.protobuf import message as _message
from typing import ClassVar as _ClassVar, Mapping as _Mapping, Optional as _Optional

DESCRIPTOR: _descriptor.FileDescriptor

class NewRequest(_message.Message):
__slots__ = ("token", "type", "config")
class ConfigEntry(_message.Message):
__slots__ = ("key", "value")
KEY_FIELD_NUMBER: _ClassVar[int]
VALUE_FIELD_NUMBER: _ClassVar[int]
key: str
value: str
def __init__(self, key: _Optional[str] = ..., value: _Optional[str] = ...) -> None: ...
TOKEN_FIELD_NUMBER: _ClassVar[int]
TYPE_FIELD_NUMBER: _ClassVar[int]
CONFIG_FIELD_NUMBER: _ClassVar[int]
token: str
type: str
config: _containers.ScalarMap[str, str]
def __init__(self, token: _Optional[str] = ..., type: _Optional[str] = ..., config: _Optional[_Mapping[str, str]] = ...) -> None: ...

class NewReply(_message.Message):
__slots__ = ("vehicle_id",)
VEHICLE_ID_FIELD_NUMBER: _ClassVar[int]
vehicle_id: int
def __init__(self, vehicle_id: _Optional[int] = ...) -> None: ...

class SoCRequest(_message.Message):
__slots__ = ("token", "vehicle_id")
TOKEN_FIELD_NUMBER: _ClassVar[int]
VEHICLE_ID_FIELD_NUMBER: _ClassVar[int]
token: str
vehicle_id: int
def __init__(self, token: _Optional[str] = ..., vehicle_id: _Optional[int] = ...) -> None: ...

class SoCReply(_message.Message):
__slots__ = ("soc",)
SOC_FIELD_NUMBER: _ClassVar[int]
soc: float
def __init__(self, soc: _Optional[float] = ...) -> None: ...
100 changes: 100 additions & 0 deletions packages/modules/vehicles/evcc/vehicle_pb2_grpc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# flake8: noqa
# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
"""Client and server classes corresponding to protobuf-defined services."""
import grpc

import modules.vehicles.evcc.vehicle_pb2 as vehicle__pb2


class VehicleStub(object):
"""Missing associated documentation comment in .proto file."""

def __init__(self, channel):
"""Constructor.

Args:
channel: A grpc.Channel.
"""
self.New = channel.unary_unary(
'/Vehicle/New',
request_serializer=vehicle__pb2.NewRequest.SerializeToString,
response_deserializer=vehicle__pb2.NewReply.FromString,
)
self.SoC = channel.unary_unary(
'/Vehicle/SoC',
request_serializer=vehicle__pb2.SoCRequest.SerializeToString,
response_deserializer=vehicle__pb2.SoCReply.FromString,
)


class VehicleServicer(object):
"""Missing associated documentation comment in .proto file."""

def New(self, request, context):
"""Missing associated documentation comment in .proto file."""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')

def SoC(self, request, context):
"""Missing associated documentation comment in .proto file."""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')


def add_VehicleServicer_to_server(servicer, server):
rpc_method_handlers = {
'New': grpc.unary_unary_rpc_method_handler(
servicer.New,
request_deserializer=vehicle__pb2.NewRequest.FromString,
response_serializer=vehicle__pb2.NewReply.SerializeToString,
),
'SoC': grpc.unary_unary_rpc_method_handler(
servicer.SoC,
request_deserializer=vehicle__pb2.SoCRequest.FromString,
response_serializer=vehicle__pb2.SoCReply.SerializeToString,
),
}
generic_handler = grpc.method_handlers_generic_handler(
'Vehicle', rpc_method_handlers)
server.add_generic_rpc_handlers((generic_handler,))


# This class is part of an EXPERIMENTAL API.
class Vehicle(object):
"""Missing associated documentation comment in .proto file."""

@staticmethod
def New(request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None):
return grpc.experimental.unary_unary(request, target, '/Vehicle/New',
vehicle__pb2.NewRequest.SerializeToString,
vehicle__pb2.NewReply.FromString,
options, channel_credentials,
insecure, call_credentials, compression, wait_for_ready, timeout, metadata)

@staticmethod
def SoC(request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None):
return grpc.experimental.unary_unary(request, target, '/Vehicle/SoC',
vehicle__pb2.SoCRequest.SerializeToString,
vehicle__pb2.SoCReply.FromString,
options, channel_credentials,
insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
2 changes: 2 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,5 @@ python-dateutil==2.8.2
umodbus==1.0.4
pysmb==1.2.9.1
pytz==2023.3.post1
grpcio==1.60.1
protobuf==4.25.3