Skip to content

Commit adb0513

Browse files
mekrappLKuemmel
andauthored
Add a SoC module for Nissan Leaf till MY 2019 (openWB#1682)
* Create __init__.py * Add files via upload * Add files via upload includes newest Base URL to Nissan API: https://gdcportalgw.its-mo.com/api_v230317_NE/gdc/ * Add files via upload copied 1:1 from OpenWB V1.9 * Add files via upload included module "fetch-soc" is derived from former soc.py in OpenWB V1.9 * Update soc.py * Update soc.py * Update soc.py * Update soc.py * Update responses.py * Update packages/modules/vehicles/leaf/soc.py Co-authored-by: LKuemmel <[email protected]> * Update soc.py Ich habe Anzahl der Wartezyklen noch von 3 auf 9 erhöht, also insgesamt 3 Minuten Wartezeit. Bis dahin müsste der Nissan Server den Leaf in jedem Fall erreicht haben. Falls nicht, kehrt requestSoc() nach drei Minuten ohne Update des SoC auf dem Server zurück und das anschließende readSoc holt sich dann halt nur den alten SoC vom Server. In der Zeit haben die Funktionen von pycarwings2 und responses auch genug Zeit für Einträge ins Logging für eine evtl. notwendige Fehleranalyse. * Update __init__.py empty line at end of file removed accoring to warning from test run on Jul 15. * Update __init__.py * Update __init__.py * Update __init__.py empty line removed at end of file * Update pycarwings2.py At import instruction wildcard * replaced by the names of the classes to be imported * Update pycarwings2.py trailing spaces removed * Update __init__.py Empty line at end of file removed * Update soc.py missing whitespace after "," added * Update __init__.py * Delete packages/modules/vehicles/leaf/__init__.py still a LF inside, that I can't delete. So I will replace __init__.py by a really empty file to get flake 8 quiet * Add files via upload * Update requirements.txt pycarwings2 added to load this library (for the SOC module of Nissan Leaf) during start * Update packages/modules/vehicles/leaf/soc.py Co-authored-by: LKuemmel <[email protected]> * Delete packages/modules/vehicles/leaf/pycarwings2.py File deleted at this position as requested * Delete packages/modules/vehicles/leaf/responses.py File deleted at this position as requested * Delete packages/modules/vehicles/leaf/soc.py soc.py using pycarwings2 removed * Add files via upload api.py using pycarwings3 uploaded. api_wo_CarState.py using pycarwings3 uploaded. test_fetch_soc.py using fetch_soc within api_wo_CarState uploaded. test_fetch_soc.py is able to run standalone, i.e. without OpenWB overhead. It prints the whole logging of interaction with the Nissan server on console and finishes with the SoC of the Nissan Leaf. Enter your user ID and password before running. This package is using https://github.com/ev-freaks/pycarwings3 from @remuslazar * Add files via upload * Add files via upload * Add files via upload * Add files via upload replace requirements.txt with a version that includes pycarwings3. * Update api.py Definition/Import für Klasse CarState ergänzt. * Update api.py Name of parameter "vnum" within fetch_soc() changed to "charge_point". * Update api.py chargepoint * Update config.py variable "region" added. variable "name" changed to "Nissan Leaf/NV200 -05.2019 (experimental)". * Update soc.py variable "region" added parameter "calc_while_charging" added and preset to False * Update api.py variable "region" added * Update config.py Variable "region" added in line 5. * Update api.py * Update api.py Parameter range added * Update api.py Import-Pfad zur config.py korrigiert * Update api.py fetching time stamp added * Update soc.py "charge_point" renamed to "vehicle" (missunderstanding) * Update api.py "chargepoint" renamed to "vehicle" "LP" renamed to "vehicle" * Delete packages/modules/vehicles/leaf/api_wo_CarState.py needed for test purpose only * Delete packages/modules/vehicles/leaf/test_fetch_soc.py needed for test purpose only * Update soc.py trailing whitespaces removed * Update api.py according to flake8: trailing whitespace removed. import of LeafSoc und LeafConfiguration removed (not used). comment shortened. blank line added * Update api.py whitespaces removed * Update soc.py calc_while_charging removed * Update soc.py variable "vehicle" renamed to "charge_point" * Update soc.py trailing whitespaces removed * Mock pycarwings3 for pytest --------- Co-authored-by: LKuemmel <[email protected]>
1 parent 73dc3be commit adb0513

File tree

6 files changed

+144
-0
lines changed

6 files changed

+144
-0
lines changed

packages/modules/conftest.py

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
sys.modules['skodaconnect.Connection'] = type(sys)('skodaconnect.Connection')
1818
sys.modules['socketserver'] = type(sys)('socketserver')
1919
sys.modules['grpc'] = type(sys)('grpc')
20+
sys.modules['pycarwings3'] = type(sys)('pycarwings3')
2021

2122

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

packages/modules/vehicles/leaf/__init__.py

Whitespace-only changes.

packages/modules/vehicles/leaf/api.py

+77
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
#!/usr/bin/env python3
2+
import asyncio
3+
import logging
4+
from datetime import datetime, timezone
5+
6+
from modules.common.component_state import CarState
7+
8+
import pycarwings3
9+
10+
log = logging.getLogger(__name__)
11+
12+
13+
async def _fetch_soc(username, password, region, vehicle) -> CarState:
14+
15+
async def getNissanSession():
16+
# open HTTPS session with Nissan server
17+
log.debug("vehicle%s: login = %s, region = %s" % (vehicle, username, region))
18+
session = pycarwings3.Session(username, password, region)
19+
leaf = await session.get_leaf()
20+
await asyncio.sleep(1)
21+
return leaf
22+
23+
async def readSoc(leaf) -> CarState:
24+
# get SoC & range & time stamp from Nissan server
25+
leaf_info = await leaf.get_latest_battery_status()
26+
soc = float(leaf_info.battery_percent)
27+
log.debug("vehicle%s: Battery State of Charge %s" % (vehicle, soc))
28+
range = int(leaf_info.answer["BatteryStatusRecords"]["CruisingRangeAcOff"])/1000
29+
log.debug("vehicle%s: Cruising range AC Off %s" % (vehicle, range))
30+
time_stamp_str_utc = leaf_info.answer["BatteryStatusRecords"]["NotificationDateAndTime"]
31+
soc_time = datetime.strptime(f"{time_stamp_str_utc}", "%Y/%m/%d %H:%M").replace(tzinfo=timezone.utc)
32+
log.debug("vehicle%s: Date&Time of SoC (UTC) %s" % (vehicle, soc_time))
33+
soc_timestamp = soc_time.timestamp()
34+
log.debug("vehicle%s: soc_timestamp %s" % (vehicle, soc_timestamp))
35+
log.debug("vehicle%s: local Date&Time of SoC %s" % (vehicle, datetime.fromtimestamp(soc_timestamp)))
36+
return CarState(soc, range, soc_timestamp)
37+
38+
async def requestSoc(leaf: pycarwings3.Leaf):
39+
# request Nissan server to request last SoC from car
40+
log.debug("vehicle%s: Request SoC Update from vehicle" % (vehicle))
41+
key = await leaf.request_update()
42+
sleepsecs = 20
43+
for _ in range(0, 3):
44+
log.debug("Waiting {0} seconds".format(sleepsecs))
45+
await asyncio.sleep(sleepsecs)
46+
status = await leaf.get_status_from_update(key)
47+
if status is not None:
48+
log.debug("vehicle%s: Update successful" % (vehicle))
49+
return status
50+
log.debug("vehicle%s: Update not successful" % (vehicle))
51+
return status
52+
53+
try:
54+
leaf = await getNissanSession() # start HTTPS session with Nissan server
55+
soc_range = await readSoc(leaf) # read old SoC & range values from server
56+
await asyncio.sleep(1) # give Nissan server some time
57+
status = await requestSoc(leaf) # Nissan server to request new values from vehicle
58+
if status is not None: # was update of values successful?
59+
await asyncio.sleep(1) # give Nissan server some time
60+
soc_range = await readSoc(leaf) # final read of SoC & range from server
61+
except pycarwings3.CarwingsError as e:
62+
log.info("vehicle%s: SoC & range request not successful" % (vehicle))
63+
log.info(e)
64+
soc_range = CarState(0.0, 0.0)
65+
return soc_range
66+
67+
68+
# main entry - _fetch_soc needs to be run async
69+
def fetch_soc(user_id: str, password: str, region: str, vehicle: int) -> CarState:
70+
71+
loop = asyncio.new_event_loop() # prepare and call async method
72+
asyncio.set_event_loop(loop)
73+
74+
# get SoC and range from vehicle via server
75+
soc_range = loop.run_until_complete(_fetch_soc(user_id, password, region, vehicle))
76+
77+
return soc_range
+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
from typing import Optional
2+
3+
4+
class LeafConfiguration:
5+
def __init__(self, user_id: Optional[str] = None, password: Optional[str] = None, region: Optional[str] = None):
6+
self.user_id = user_id
7+
self.password = password
8+
self.region = region
9+
10+
11+
class LeafSoc:
12+
def __init__(self,
13+
name: str = "Nissan Leaf/NV200 -05.2019 (experimental)",
14+
type: str = "leaf",
15+
configuration: LeafConfiguration = None) -> None:
16+
self.name = name
17+
self.type = type
18+
self.configuration = configuration or LeafConfiguration()

packages/modules/vehicles/leaf/soc.py

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
from typing import List
2+
3+
import logging
4+
5+
from helpermodules.cli import run_using_positional_cli_args
6+
from modules.common import store
7+
from modules.common.abstract_device import DeviceDescriptor
8+
from modules.common.abstract_vehicle import VehicleUpdateData
9+
from modules.common.component_state import CarState
10+
from modules.common.configurable_vehicle import ConfigurableVehicle
11+
from modules.vehicles.leaf import api
12+
from modules.vehicles.leaf.config import LeafSoc, LeafConfiguration
13+
14+
15+
log = logging.getLogger(__name__)
16+
17+
18+
def create_vehicle(vehicle_config: LeafSoc, vehicle: int):
19+
def updater(vehicle_update_data: VehicleUpdateData) -> CarState:
20+
return api.fetch_soc(
21+
vehicle_config.configuration.user_id,
22+
vehicle_config.configuration.password,
23+
vehicle_config.configuration.region,
24+
vehicle)
25+
return ConfigurableVehicle(vehicle_config=vehicle_config,
26+
component_updater=updater,
27+
vehicle=vehicle)
28+
29+
30+
def leaf_update(user_id: str, password: str, region: str, charge_point: int):
31+
log.debug("leaf: user_id="+user_id+" region="+region+" charge_point="+str(charge_point))
32+
vehicle_config = LeafSoc(configuration=LeafConfiguration(charge_point,
33+
user_id,
34+
password,
35+
region))
36+
store.get_car_value_store(charge_point).store.set(api.fetch_soc(
37+
vehicle_config.configuration.user_id,
38+
vehicle_config.configuration.password,
39+
vehicle_config.configuration.region,
40+
charge_point))
41+
42+
43+
def main(argv: List[str]):
44+
run_using_positional_cli_args(leaf_update, argv)
45+
46+
47+
device_descriptor = DeviceDescriptor(configuration_factory=LeafSoc)

requirements.txt

+1
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,4 @@ protobuf==4.25.3
2323
bimmer_connected==0.17.2
2424
ocpp==1.0.0
2525
websockets==12.0
26+
pycarwings3==0.7.13

0 commit comments

Comments
 (0)