From ea41953537a8af02a17e5c24891f0bc5319e69ac Mon Sep 17 00:00:00 2001 From: mekrapp <158028484+mekrapp@users.noreply.github.com> Date: Sat, 15 Jun 2024 15:24:31 +0200 Subject: [PATCH 01/55] Create __init__.py --- packages/modules/vehicles/leaf/__init__.py | 1 + 1 file changed, 1 insertion(+) create mode 100644 packages/modules/vehicles/leaf/__init__.py diff --git a/packages/modules/vehicles/leaf/__init__.py b/packages/modules/vehicles/leaf/__init__.py new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/packages/modules/vehicles/leaf/__init__.py @@ -0,0 +1 @@ + From 6c053722a610f847cfc052e8ad3dedd0fbd693dd Mon Sep 17 00:00:00 2001 From: mekrapp <158028484+mekrapp@users.noreply.github.com> Date: Sat, 15 Jun 2024 15:28:45 +0200 Subject: [PATCH 02/55] Add files via upload --- packages/modules/vehicles/leaf/config.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 packages/modules/vehicles/leaf/config.py diff --git a/packages/modules/vehicles/leaf/config.py b/packages/modules/vehicles/leaf/config.py new file mode 100644 index 0000000000..7af45d37e1 --- /dev/null +++ b/packages/modules/vehicles/leaf/config.py @@ -0,0 +1,17 @@ +from typing import Optional + + +class LeafConfiguration: + def __init__(self, user_id: Optional[str] = None, password: Optional[str] = None): + self.user_id = user_id + self.password = password + + +class LeafSoc: + def __init__(self, + name: str = "Leaf", + type: str = "leaf", + configuration: LeafConfiguration = None) -> None: + self.name = name + self.type = type + self.configuration = configuration or LeafConfiguration() From 55f693987fcc90d796943125e0f6ed24011228af Mon Sep 17 00:00:00 2001 From: mekrapp <158028484+mekrapp@users.noreply.github.com> Date: Sat, 15 Jun 2024 15:42:31 +0200 Subject: [PATCH 03/55] Add files via upload includes newest Base URL to Nissan API: https://gdcportalgw.its-mo.com/api_v230317_NE/gdc/ --- packages/modules/vehicles/leaf/pycarwings2.py | 462 ++++++++++++++++++ 1 file changed, 462 insertions(+) create mode 100644 packages/modules/vehicles/leaf/pycarwings2.py diff --git a/packages/modules/vehicles/leaf/pycarwings2.py b/packages/modules/vehicles/leaf/pycarwings2.py new file mode 100644 index 0000000000..1265fe762e --- /dev/null +++ b/packages/modules/vehicles/leaf/pycarwings2.py @@ -0,0 +1,462 @@ +# Copyright 2016 Jason Horne +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +When logging in, you must specify a geographic 'region' parameter. The only +known values for this are as follows: + + NNA : USA + NE : Europe + NCI : Canada + NMA : Australia + NML : Japan + +Information about Nissan on the web (e.g. http://nissannews.com/en-US/nissan/usa/pages/executive-bios) +suggests others (this page suggests NMEX for Mexico, NLAC for Latin America) but +these have not been confirmed. + +There are three asynchronous operations in this API, paired with three follow-up +"status check" methods. + + request_update -> get_status_from_update + start_climate_control -> get_start_climate_control_result + stop_climate_control -> get_stop_climate_control_result + +The asynchronous operations immediately return a 'result key', which +is then supplied as a parameter for the corresponding status check method. + +Here's an example response from an asynchronous operation, showing the result key: + + { + "status":200, + "userId":"user@domain.com", + "vin":"1ABCDEFG2HIJKLM3N", + "resultKey":"12345678901234567890123456789012345678901234567890" + } + +The status check methods return a JSON blob containing a 'responseFlag' property. +If the communications are complete, the response flag value will be the string "1"; +otherwise the value will be the string "0". You just gotta poll until you get a +"1" back. Note that the official app seems to poll every 20 seconds. + +Example 'no response yet' result from a status check invocation: + + { + "status":200, + "responseFlag":"0" + } + +When the responseFlag does come back as "1", there will also be an "operationResult" +property. If there was an error communicating with the vehicle, it seems that +this field will contain the value "ELECTRIC_WAVE_ABNORMAL". Odd. + +""" + +import requests +from requests import Request, RequestException +import json +import logging +from datetime import date +from responses import * +import base64 +from Crypto.Cipher import Blowfish + +BASE_URL = "https://gdcportalgw.its-mo.com/api_v230317_NE/gdc/" +log = logging.getLogger(__name__) + + +# from http://stackoverflow.com/questions/17134100/python-blowfish-encryption +def _PKCS5Padding(string): + byteNum = len(string) + packingLength = 8 - byteNum % 8 + appendage = chr(packingLength) * packingLength + return string + appendage + + +class CarwingsError(Exception): + pass + + +class Session(object): + """Maintains a connection to CARWINGS, refreshing it when needed""" + + def __init__(self, username, password, region="NNA"): + self.username = username + self.password = password + self.region_code = region + self.logged_in = False + self.custom_sessionid = None + + def _request_with_retry(self, endpoint, params): + ret = self._request(endpoint, params) + + if ("status" in ret) and (ret["status"] >= 400): + log.error( + "carwings error; logging in and trying request again: %s" % ret) + # try logging in again + self.connect() + ret = self._request(endpoint, params) + + return ret + + def _request(self, endpoint, params): + params["initial_app_str"] = "9s5rfKVuMrT03RtzajWNcA" + if self.custom_sessionid: + params["custom_sessionid"] = self.custom_sessionid + else: + params["custom_sessionid"] = "" + + req = Request('POST', url=BASE_URL + endpoint, data=params, headers={"User-Agent": ""}).prepare() + # ", headers={"User-Agent": ""}" added, + # see https://github.com/filcole/pycarwings2/blob/master/pycarwings2/pycarwings2.py + # added parameter is needed, to be able to run pycarwings2.py on a Windows PC with Python 3.12 + + log.debug("invoking carwings API: %s" % req.url) + log.debug("params: %s" % json.dumps( + {k: v.decode('utf-8') if isinstance(v, bytes) + else v for k, v in params.items()}, + sort_keys=True, indent=3, separators=(',', ': ')) + ) + + try: + sess = requests.Session() + response = sess.send(req) + log.debug('Response HTTP Status Code: {status_code}'.format( + status_code=response.status_code)) + log.debug('Response HTTP Response Body: {content}'.format( + content=response.content)) + except RequestException: + log.warning('HTTP Request failed') + raise CarwingsError + + # Nissan servers can return html instead of jSOn on occassion, e.g. + # + # + # + # 503 Service Temporarily Unavailable + # + #

Service Temporarily Unavailable> + #

The server is temporarily unable to service your + # request due to maintenance downtime or capacity + # problems. Please try again later.

+ # + try: + j = json.loads(response.text) + except ValueError: + log.error("Invalid JSON returned") + raise CarwingsError + + if "message" in j and j["message"] == "INVALID PARAMS": + log.error("carwings error %s: %s" % (j["message"], j["status"])) + raise CarwingsError("INVALID PARAMS") + if "ErrorMessage" in j: + log.error("carwings error %s: %s" % + (j["ErrorCode"], j["ErrorMessage"])) + raise CarwingsError + + return j + + def connect(self): + self.custom_sessionid = None + self.logged_in = False + + response = self._request("InitialApp_v2.php", { + "RegionCode": self.region_code, + "lg": "en-US", + }) + ret = CarwingsInitialAppResponse(response) + + c1 = Blowfish.new(ret.baseprm.encode(), Blowfish.MODE_ECB) + packedPassword = _PKCS5Padding(self.password) + encryptedPassword = c1.encrypt(packedPassword.encode()) + encodedPassword = base64.standard_b64encode(encryptedPassword) + + response = self._request("UserLoginRequest.php", { + "RegionCode": self.region_code, + "UserId": self.username, + "Password": encodedPassword, + }) + + ret = CarwingsLoginResponse(response) + + self.custom_sessionid = ret.custom_sessionid + + self.gdc_user_id = ret.gdc_user_id + log.debug("gdc_user_id: %s" % self.gdc_user_id) + self.dcm_id = ret.dcm_id + log.debug("dcm_id: %s" % self.dcm_id) + self.tz = ret.tz + log.debug("tz: %s" % self.tz) + self.language = ret.language + log.debug("language: %s" % self.language) + log.debug("vin: %s" % ret.vin) + log.debug("nickname: %s" % ret.nickname) + + self.leaf = Leaf(self, ret.leafs[0]) + + self.logged_in = True + + return ret + + def get_leaf(self, index=0): + if not self.logged_in: + self.connect() + + return self.leaf + + +class Leaf: + def __init__(self, session, params): + self.session = session + self.vin = params["vin"] + self.nickname = params["nickname"] + self.bound_time = params["bound_time"] + log.debug("created leaf %s/%s" % (self.vin, self.nickname)) + + def request_update(self): + response = self.session._request_with_retry("BatteryStatusCheckRequest.php", { + "RegionCode": self.session.region_code, + "VIN": self.vin, + }) + return response["resultKey"] + + def get_status_from_update(self, result_key): + response = self.session._request_with_retry("BatteryStatusCheckResultRequest.php", { + "RegionCode": self.session.region_code, + "lg": self.session.language, + "DCMID": self.session.dcm_id, + "VIN": self.vin, + "tz": self.session.tz, + "resultKey": result_key, + }) + # responseFlag will be "1" if a response has been returned; "0" otherwise + if response["responseFlag"] == "1": + return CarwingsBatteryStatusResponse(response) + + return None + + def start_climate_control(self): + response = self.session._request_with_retry("ACRemoteRequest.php", { + "RegionCode": self.session.region_code, + "lg": self.session.language, + "DCMID": self.session.dcm_id, + "VIN": self.vin, + "tz": self.session.tz, + }) + return response["resultKey"] + + def get_start_climate_control_result(self, result_key): + response = self.session._request_with_retry("ACRemoteResult.php", { + "RegionCode": self.session.region_code, + "lg": self.session.language, + "DCMID": self.session.dcm_id, + "VIN": self.vin, + "tz": self.session.tz, + "UserId": self.session.gdc_user_id, # this userid is the 'gdc' userid + "resultKey": result_key, + }) + if response["responseFlag"] == "1": + return CarwingsStartClimateControlResponse(response) + + return None + + def stop_climate_control(self): + response = self.session._request_with_retry("ACRemoteOffRequest.php", { + "RegionCode": self.session.region_code, + "lg": self.session.language, + "DCMID": self.session.dcm_id, + "VIN": self.vin, + "tz": self.session.tz, + }) + return response["resultKey"] + + def get_stop_climate_control_result(self, result_key): + response = self.session._request_with_retry("ACRemoteOffResult.php", { + "RegionCode": self.session.region_code, + "lg": self.session.language, + "DCMID": self.session.dcm_id, + "VIN": self.vin, + "tz": self.session.tz, + "UserId": self.session.gdc_user_id, # this userid is the 'gdc' userid + "resultKey": result_key, + }) + if response["responseFlag"] == "1": + return CarwingsStopClimateControlResponse(response) + + return None + + # execute time example: "2016-02-09 17:24" + # I believe this time is specified in GMT, despite the "tz" parameter + # TODO: change parameter to python datetime object(?) + def schedule_climate_control(self, execute_time): + response = self.session._request_with_retry("ACRemoteNewRequest.php", { + "RegionCode": self.session.region_code, + "lg": self.session.language, + "DCMID": self.session.dcm_id, + "VIN": self.vin, + "tz": self.session.tz, + "ExecuteTime": execute_time, + }) + return (response["status"] == 200) + + # execute time example: "2016-02-09 17:24" + # I believe this time is specified in GMT, despite the "tz" parameter + # TODO: change parameter to python datetime object(?) + def update_scheduled_climate_control(self, execute_time): + response = self.session._request_with_retry("ACRemoteUpdateRequest.php", { + "RegionCode": self.session.region_code, + "lg": self.session.language, + "DCMID": self.session.dcm_id, + "VIN": self.vin, + "tz": self.session.tz, + "ExecuteTime": execute_time, + }) + return (response["status"] == 200) + + def cancel_scheduled_climate_control(self): + response = self.session._request_with_retry("ACRemoteCancelRequest.php", { + "RegionCode": self.session.region_code, + "lg": self.session.language, + "DCMID": self.session.dcm_id, + "VIN": self.vin, + "tz": self.session.tz, + }) + return (response["status"] == 200) + + def get_climate_control_schedule(self): + response = self.session._request_with_retry("GetScheduledACRemoteRequest.php", { + "RegionCode": self.session.region_code, + "lg": self.session.language, + "DCMID": self.session.dcm_id, + "VIN": self.vin, + "tz": self.session.tz, + }) + if (response["status"] == 200): + if response["ExecuteTime"] != "": + return CarwingsClimateControlScheduleResponse(response) + + return None + + """ + { + "status":200, + } + """ + + def start_charging(self): + response = self.session._request_with_retry("BatteryRemoteChargingRequest.php", { + "RegionCode": self.session.region_code, + "lg": self.session.language, + "DCMID": self.session.dcm_id, + "VIN": self.vin, + "tz": self.session.tz, + "ExecuteTime": date.today().isoformat() + }) + if response["status"] == 200: + # This only indicates that the charging command has been received by the + # Nissan servers, it does not indicate that the car is now charging. + return True + + return False + + def get_driving_analysis(self): + response = self.session._request_with_retry("DriveAnalysisBasicScreenRequestEx.php", { + "RegionCode": self.session.region_code, + "lg": self.session.language, + "DCMID": self.session.dcm_id, + "VIN": self.vin, + "tz": self.session.tz, + }) + if response["status"] == 200: + return CarwingsDrivingAnalysisResponse(response) + + return None + + def get_latest_battery_status(self): + response = self.session._request_with_retry("BatteryStatusRecordsRequest.php", { + "RegionCode": self.session.region_code, + "lg": self.session.language, + "DCMID": self.session.dcm_id, + "VIN": self.vin, + "tz": self.session.tz, + "TimeFrom": self.bound_time + }) + if response["status"] == 200: + if "BatteryStatusRecords" in response: + return CarwingsLatestBatteryStatusResponse(response) + else: + log.warning('no battery status record returned by server') + + return None + + def get_latest_hvac_status(self): + response = self.session._request_with_retry("RemoteACRecordsRequest.php", { + "RegionCode": self.session.region_code, + "lg": self.session.language, + "DCMID": self.session.dcm_id, + "VIN": self.vin, + "tz": self.session.tz, + "TimeFrom": self.bound_time + }) + if response["status"] == 200: + if "RemoteACRecords" in response: + return CarwingsLatestClimateControlStatusResponse(response) + else: + log.warning('no remote a/c records returned by server') + + return None + + # target_month format: "YYYYMM" e.g. "201602" + def get_electric_rate_simulation(self, target_month): + response = self.session._request_with_retry("PriceSimulatorDetailInfoRequest.php", { + "RegionCode": self.session.region_code, + "lg": self.session.language, + "DCMID": self.session.dcm_id, + "VIN": self.vin, + "tz": self.session.tz, + "TargetMonth": target_month + }) + if response["status"] == 200: + return CarwingsElectricRateSimulationResponse(response) + + return None + + def request_location(self): + # As of 25th July the Locate My Vehicle functionality of the Europe version of the + # Nissan APIs was removed. It may return, so this call is left here. + # It currently errors with a 404 MyCarFinderRequest.php was not found on this server + # for European users. + response = self.session._request_with_retry("MyCarFinderRequest.php", { + "RegionCode": self.session.region_code, + "lg": self.session.language, + "DCMID": self.session.dcm_id, + "VIN": self.vin, + "tz": self.session.tz, + "UserId": self.session.gdc_user_id, # this userid is the 'gdc' userid + }) + return response["resultKey"] + + def get_status_from_location(self, result_key): + response = self.session._request_with_retry("MyCarFinderResultRequest.php", { + "RegionCode": self.session.region_code, + "lg": self.session.language, + "DCMID": self.session.dcm_id, + "VIN": self.vin, + "tz": self.session.tz, + "resultKey": result_key, + }) + if response["responseFlag"] == "1": + return CarwingsMyCarFinderResponse(response) + + return None From 38c4aca1bf2a92525a5e22e8375e353385406c28 Mon Sep 17 00:00:00 2001 From: mekrapp <158028484+mekrapp@users.noreply.github.com> Date: Sat, 15 Jun 2024 15:44:03 +0200 Subject: [PATCH 04/55] Add files via upload copied 1:1 from OpenWB V1.9 --- packages/modules/vehicles/leaf/responses.py | 724 ++++++++++++++++++++ 1 file changed, 724 insertions(+) create mode 100644 packages/modules/vehicles/leaf/responses.py diff --git a/packages/modules/vehicles/leaf/responses.py b/packages/modules/vehicles/leaf/responses.py new file mode 100644 index 0000000000..f082e2dbb2 --- /dev/null +++ b/packages/modules/vehicles/leaf/responses.py @@ -0,0 +1,724 @@ +# Copyright 2016 Jason Horne +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging +from datetime import timedelta, datetime +import pycarwings2 + +log = logging.getLogger(__name__) + + +def _time_remaining(t): + minutes = float(0) + if t: + if ("hours" in t) and t["hours"]: + minutes = 60 * float(t["hours"]) + elif ("HourRequiredToFull" in t) and t["HourRequiredToFull"]: + minutes = 60 * float(t["HourRequiredToFull"]) + if ("minutes" in t) and t["minutes"]: + minutes += float(t["minutes"]) + elif ("MinutesRequiredToFull" in t) and t["MinutesRequiredToFull"]: + minutes += float(t["MinutesRequiredToFull"]) + + return minutes + + +class CarwingsResponse: + def __init__(self, response): + op_result = None + if ("operationResult" in response): + op_result = response["operationResult"] + elif ("OperationResult" in response): + op_result = response["OperationResult"] + + # seems to indicate that the vehicle cannot be reached + if ("ELECTRIC_WAVE_ABNORMAL" == op_result): + log.error("could not establish communications with vehicle") + raise pycarwings2.CarwingsError("could not establish communications with vehicle") + + def _set_cruising_ranges(self, status, off_key="cruisingRangeAcOff", on_key="cruisingRangeAcOn"): + if off_key in status: + self.cruising_range_ac_off_km = float(status[off_key]) / 1000 + if on_key in status: + self.cruising_range_ac_on_km = float(status[on_key]) / 1000 + + def _set_timestamp(self, status): + self.timestamp = datetime.strptime(status["timeStamp"], "%Y-%m-%d %H:%M:%S") # "2016-01-02 17:17:38" + + +class CarwingsInitialAppResponse(CarwingsResponse): + def __init__(self, response): + CarwingsResponse.__init__(self, response) + self.baseprm = response["baseprm"] + + +class CarwingsLoginResponse(CarwingsResponse): + """ + example JSON response to login: + { + "status":200, + "message":"success", + "sessionId":"12345678-1234-1234-1234-1234567890", + "VehicleInfoList": { + "VehicleInfo": [ + { + "charger20066":"false", + "nickname":"LEAF", + "telematicsEnabled":"true", + "vin":"1ABCDEFG2HIJKLM3N" + } + ], + "vehicleInfo": [ + { + "charger20066":"false", + "nickname":"LEAF", + "telematicsEnabled":"true", + "vin":"1ABCDEFG2HIJKLM3N" + } + ] + }, + "vehicle": { + "profile": { + "vin":"1ABCDEFG2HIJKLM3N", + "gdcUserId":"FG12345678", + "gdcPassword":"password", + "encAuthToken":"ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZ", + "dcmId":"123456789012", + "nickname":"Alpha124", + "status":"ACCEPTED", + "statusDate": "Aug 15, 2015 07:00 PM" + } + }, + "EncAuthToken":"ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZ", + "CustomerInfo": { + "UserId":"AB12345678", + "Language":"en-US", + "Timezone":"America/New_York", + "RegionCode":"NNA", + "OwnerId":"1234567890", + "Nickname":"Bravo456", + "Country":"US", + "VehicleImage":"/content/language/default/images/img/ph_car.jpg", + "UserVehicleBoundDurationSec":"999971200", + "VehicleInfo": { + "VIN":"1ABCDEFG2HIJKLM3N", + "DCMID":"201212345678", + "SIMID":"12345678901234567890", + "NAVIID":"1234567890", + "EncryptedNAVIID":"1234567890ABCDEFGHIJKLMNOP", + "MSN":"123456789012345", + "LastVehicleLoginTime":"", + "UserVehicleBoundTime":"2015-08-17T14:16:32Z", + "LastDCMUseTime":"" + } + }, + "UserInfoRevisionNo":"1" + } + """ + def __init__(self, response): + CarwingsResponse.__init__(self, response) + + profile = response["vehicle"]["profile"] + self.gdc_user_id = profile["gdcUserId"] + self.dcm_id = profile["dcmId"] + self.vin = profile["vin"] + + # vehicleInfo block may be top level, or contained in a VehicleInfoList object; + # why it's sometimes one way and sometimes another is not clear. + if "VehicleInfoList" in response: + self.nickname = response["VehicleInfoList"]["vehicleInfo"][0]["nickname"] + self.custom_sessionid = response["VehicleInfoList"]["vehicleInfo"][0]["custom_sessionid"] + elif "vehicleInfo" in response: + self.nickname = response["vehicleInfo"][0]["nickname"] + self.custom_sessionid = response["vehicleInfo"][0]["custom_sessionid"] + + customer_info = response["CustomerInfo"] + self.tz = customer_info["Timezone"] + self.language = customer_info["Language"] + self.user_vehicle_bound_time = customer_info["VehicleInfo"]["UserVehicleBoundTime"] + + self.leafs = [{ + "vin": self.vin, + "nickname": self.nickname, + "bound_time": self.user_vehicle_bound_time + }] + + +class CarwingsBatteryStatusResponse(CarwingsResponse): + """ + Note that before December 2018 this returned a response. Between Dec-2018 and Aug-2019 + it did not return a response from the Nissan Servers. As of Aug 2019 a response is now + returned again. + + # Original + { + "status": 200, + "message": "success", + "responseFlag": "1", + "operationResult": "START", + "timeStamp": "2016-01-02 17:17:38", + "cruisingRangeAcOn": "115328.0", + "cruisingRangeAcOff": "117024.0", + "currentChargeLevel": "0", + "chargeMode": "220V", + "pluginState": "CONNECTED", + "charging": "YES", + "chargeStatus": "CT", + "batteryDegradation": "10", + "batteryCapacity": "12", + "timeRequiredToFull": { + "hours": "", + "minutes": "" + }, + "timeRequiredToFull200": { + "hours": "", + "minutes": "" + }, + "timeRequiredToFull200_6kW": { + "hours": "", + "minutes": "" + } + } + + # As at 21/01/2019 for a 30kWh Leaf now seems that + # BatteryStatusCheckResultRequest.php always returns this + # regardless of battery status. + { + "status":200, + "responseFlag":"0" + } + + { + "status":200, + "message":"success", + "responseFlag":"1", + "operationResult":"START", + "timeStamp":"2016-02-14 20:28:45", + "cruisingRangeAcOn":"107136.0", + "cruisingRangeAcOff":"115776.0", + "currentChargeLevel":"0", + "chargeMode":"NOT_CHARGING", + "pluginState":"QC_CONNECTED", + "charging":"YES", + "chargeStatus":"CT", + "batteryDegradation":"11", + "batteryCapacity":"12", + "timeRequiredToFull":{ + "hours":"", + "minutes":"" + }, + "timeRequiredToFull200":{ + "hours":"", + "minutes":"" + }, + "timeRequiredToFull200_6kW":{ + "hours":"", + "minutes":"" + } + } + + # As at 22/08/2019 for a 30kWh Leaf now seesm that + # BatteryStatusCheckResultRequest.php returns data again + # after polling a number of times. + { + "status": 200, + "responseFlag": "1", + "operationResult": "START", + "timeStamp": "2019-08-22 10:26:51", + "cruisingRangeAcOn": "129000.0", + "cruisingRangeAcOff": "132000.0", + "currentChargeLevel": "0", + "chargeMode": "NOT_CHARGING", + "pluginState": "NOT_CONNECTED", + "charging": "NO", + "chargeStatus": "0", + "batteryDegradation": "180", + "batteryCapacity": "240", + "timeRequiredToFull": { + "hours": "11", + "minutes": "30" + }, + "timeRequiredToFull200": { + "hours": "6", + "minutes": "30" + }, + "timeRequiredToFull200_6kW": { + "hours": "2", + "minutes": "30" + } + } + + """ + def __init__(self, status): + CarwingsResponse.__init__(self, status) + + self._set_timestamp(status) + self._set_cruising_ranges(status) + + self.answer = status + + self.battery_capacity = status["batteryCapacity"] + self.battery_degradation = status["batteryDegradation"] + + self.is_connected = ("NOT_CONNECTED" != status["pluginState"]) # fun double negative + self.plugin_state = status["pluginState"] + + self.charging_status = status["chargeMode"] + + self.is_charging = ("YES" == status["charging"]) + + self.is_quick_charging = ("RAPIDLY_CHARGING" == status["chargeMode"]) + self.is_connected_to_quick_charger = ("QC_CONNECTED" == status["pluginState"]) + + self.time_to_full_trickle = timedelta(minutes=_time_remaining(status["timeRequiredToFull"])) + self.time_to_full_l2 = timedelta(minutes=_time_remaining(status["timeRequiredToFull200"])) + self.time_to_full_l2_6kw = timedelta(minutes=_time_remaining(status["timeRequiredToFull200_6kW"])) + + # For some leafs the battery_percent is not returned + self.battery_percent = 100 * float(self.battery_degradation) / 12 + + +class CarwingsLatestClimateControlStatusResponse(CarwingsResponse): + """ + climate control on: + { + "status":200, + "message":"success", + "RemoteACRecords":{ + "OperationResult":"START_BATTERY", + "OperationDateAndTime":"Feb 10, 2016 10:22 PM", + "RemoteACOperation":"START", + "ACStartStopDateAndTime":"Feb 10, 2016 10:23 PM", + "CruisingRangeAcOn":"107712.0", + "CruisingRangeAcOff":"109344.0", + "ACStartStopURL":"", + "PluginState":"NOT_CONNECTED", + "ACDurationBatterySec":"900", + "ACDurationPluggedSec":"7200" + }, + "OperationDateAndTime":"" + } + + climate control off: + { + "status":200, + "message":"success", + "RemoteACRecords":{ + "OperationResult":"START", + "OperationDateAndTime":"Feb 10, 2016 10:26 PM", + "RemoteACOperation":"STOP", + "ACStartStopDateAndTime":"Feb 10, 2016 10:27 PM", + "CruisingRangeAcOn":"111936.0", + "CruisingRangeAcOff":"113632.0", + "ACStartStopURL":"", + "PluginState":"NOT_CONNECTED", + "ACDurationBatterySec":"900", + "ACDurationPluggedSec":"7200" + }, + "OperationDateAndTime":"" + } + + error: + { + "status":200, + "RemoteACRecords":{ + "OperationResult":"ELECTRIC_WAVE_ABNORMAL", + "OperationDateAndTime":"2018/04/08 10:00", + "RemoteACOperation":"START", + "ACStartStopDateAndTime":"08-Apr-2018 11:06", + "ACStartStopURL":"", + "PluginState":"INVALID", + "ACDurationBatterySec":"900", + "ACDurationPluggedSec":"7200", + "PreAC_unit":"C", + "PreAC_temp":"22" + } + } + noinfo (from a 2014 24kWh Leaf): + { + "status":200, + "RemoteACRecords":[] + } + + """ + def __init__(self, status): + CarwingsResponse.__init__(self, status["RemoteACRecords"]) + racr = status["RemoteACRecords"] + + self._set_cruising_ranges(racr, on_key="CruisingRangeAcOn", off_key="CruisingRangeAcOff") + + # If no empty RemoteACRecords list is returned then assume CC is off. + if type(racr) is not dict: + self.is_hvac_running = False + else: + # Seems to be running only if both of these contain "START". + self.is_hvac_running = ( + racr["OperationResult"] and + racr["OperationResult"].startswith("START") and + racr["RemoteACOperation"] == "START" + ) + + +class CarwingsStartClimateControlResponse(CarwingsResponse): + """ + { + "status":200, + "message":"success", + "responseFlag":"1", + "operationResult":"START_BATTERY", + "acContinueTime":"15", + "cruisingRangeAcOn":"106400.0", + "cruisingRangeAcOff":"107920.0", + "timeStamp":"2016-02-05 12:59:46", + "hvacStatus":"ON" + } + """ + def __init__(self, status): + CarwingsResponse.__init__(self, status) + + self._set_timestamp(status) + self._set_cruising_ranges(status) + + self.operation_result = status["operationResult"] # e.g. "START_BATTERY", ...? + self.ac_continue_time = timedelta(minutes=float(status["acContinueTime"])) + self.hvac_status = status["hvacStatus"] # "ON" or "OFF" + self.is_hvac_running = ("ON" == self.hvac_status) + + +class CarwingsStopClimateControlResponse(CarwingsResponse): + """ + { + "status":200, + "message":"success", + "responseFlag":"1", + "operationResult":"START", + "timeStamp":"2016-02-09 03:32:51", + "hvacStatus":"OFF" + } + """ + def __init__(self, status): + CarwingsResponse.__init__(self, status) + + self._set_timestamp(status) + self.hvac_status = status["hvacStatus"] # "ON" or "OFF" + self.is_hvac_running = ("ON" == self.hvac_status) + + +class CarwingsClimateControlScheduleResponse(CarwingsResponse): + """ + { + "status":200, + "message":"success", + "LastScheduledTime":"Feb 9, 2016 05:39 PM", + "ExecuteTime":"2016-02-10 01:00:00", + "DisplayExecuteTime":"Feb 9, 2016 08:00 PM", + "TargetDate":"2016/02/10 01:00" + } + """ + def __init__(self, status): + CarwingsResponse.__init__(self, status) + + self.display_execute_time = status["DisplayExecuteTime"] # displayable, timezone-adjusted + self.execute_time = datetime.strptime(status["ExecuteTime"] + " UTC", "%Y-%m-%d %H:%M:%S %Z") # GMT + self.display_last_scheduled_time = status["LastScheduledTime"] # displayable, timezone-adjusted + self.last_scheduled_time = datetime.strptime(status["LastScheduledTime"], "%b %d, %Y %I:%M %p") + # unknown purpose; don't surface to avoid confusion + # self.target_date = status["TargetDate"] + + +class CarwingsDrivingAnalysisResponse(CarwingsResponse): + """ + { + "status":200, + "message":"success", + "DriveAnalysisBasicScreenResponsePersonalData": { + "DateSummary":{ + "TargetDate":"2016-02-03", + "ElectricMileage":"4.4", + "ElectricMileageLevel":"3", + "PowerConsumptMoter":"295.2", + "PowerConsumptMoterLevel":"4", + "PowerConsumptMinus":"84.8", + "PowerConsumptMinusLevel":"3", + "PowerConsumptAUX":"17.1", + "PowerConsumptAUXLevel":"5", + "DisplayDate":"Feb 3, 16" + }, + "ElectricCostScale":"miles/kWh" + }, + "AdviceList":{ + "Advice":{ + "title":"World Number of Trips Rankings (last week):", + "body":"The highest number of trips driven was 130 by a driver located in Japan." + } + } + } + """ + def __init__(self, status): + CarwingsResponse.__init__(self, status) + + summary = status["DriveAnalysisBasicScreenResponsePersonalData"]["DateSummary"] + + # avg energy economy, in units of 'electric_cost_scale' (e.g. miles/kWh) + self.electric_mileage = summary["ElectricMileage"] + # rating for above, scale of 1-5 + self.electric_mileage_level = summary["ElectricMileageLevel"] + + # "acceleration performance": "electricity used for motor activation over 1km", Watt-Hours + self.power_consumption_moter = summary["PowerConsumptMoter"] + # rating for above, scale of 1-5 + self.power_consumption_moter_level = summary["PowerConsumptMoterLevel"] + + # Watt-Hours generated by braking + self.power_consumption_minus = summary["PowerConsumptMinus"] + # rating for above, scale of 1-5 + self.power_consumption_minus_level = summary["PowerConsumptMinusLevel"] + + # Electricity used by aux devices, Watt-Hours + self.power_consumption_aux = summary["PowerConsumptAUX"] + # rating for above, scale of 1-5 + self.power_consumption_aux_level = summary["PowerConsumptAUXLevel"] + + self.display_date = summary["DisplayDate"] # "Feb 3, 16" + + self.electric_cost_scale = status["DriveAnalysisBasicScreenResponsePersonalData"]["ElectricCostScale"] + + self.advice = [status["AdviceList"]["Advice"]] # will contain "title" and "body" + + +class CarwingsLatestBatteryStatusResponse(CarwingsResponse): + """ + # not connected to a charger + { + "status":200, + "message":"success", + "BatteryStatusRecords":{ + "OperationResult":"START", + "OperationDateAndTime":"Feb 9, 2016 11:09 PM", + "BatteryStatus":{ + "BatteryChargingStatus":"NOT_CHARGING", + "BatteryCapacity":"12", + "BatteryRemainingAmount":"3", + "BatteryRemainingAmountWH":"", + "BatteryRemainingAmountkWH":"" + }, + "PluginState":"NOT_CONNECTED", + "CruisingRangeAcOn":"39192.0", + "CruisingRangeAcOff":"39744.0", + "TimeRequiredToFull":{ # 120V + "HourRequiredToFull":"18", + "MinutesRequiredToFull":"30" + }, + "TimeRequiredToFull200":{ # 240V, 3kW + "HourRequiredToFull":"6", + "MinutesRequiredToFull":"0" + }, + "TimeRequiredToFull200_6kW":{ # 240V, 6kW + "HourRequiredToFull":"4", + "MinutesRequiredToFull":"0" + }, + "NotificationDateAndTime":"2016/02/10 04:10", + "TargetDate":"2016/02/10 04:09" + } + } + + # not connected to a charger - as at 21/01/2019 20:01 (for a 30kWh leaf) + { + "status":200, + "BatteryStatusRecords": { + "OperationResult":"START", + "OperationDateAndTime":"21-Jan-2019 13:29", + "BatteryStatus":{ + "BatteryChargingStatus":"NOT_CHARGING", + "BatteryCapacity":"240", + "BatteryRemainingAmount":"220", + "BatteryRemainingAmountWH":"24480", + "BatteryRemainingAmountkWH":"", + "SOC":{ + "Value":"91" + } + }, + "PluginState":"NOT_CONNECTED", + "CruisingRangeAcOn":"146000", + "CruisingRangeAcOff":"168000", + "TimeRequiredToFull":{ + "HourRequiredToFull":"4", + "MinutesRequiredToFull":"30" + }, + "TimeRequiredToFull200":{ + "HourRequiredToFull":"3" + ,"MinutesRequiredToFull":"0" + }, + "TimeRequiredToFull200_6kW":{ + "HourRequiredToFull":"1", + "MinutesRequiredToFull":"30" + }, + "NotificationDateAndTime":"2019/01/21 13:29", + "TargetDate":"2019/01/21 13:29" + } + } + + + # connected to a quick charger + { + "status":200, + "message":"success", + "BatteryStatusRecords":{ + "OperationResult":"START", + "OperationDateAndTime":"Feb 14, 2016 03:28 PM", + "BatteryStatus":{ + "BatteryChargingStatus":"RAPIDLY_CHARGING", + "BatteryCapacity":"12", + "BatteryRemainingAmount":"11", + "BatteryRemainingAmountWH":"", + "BatteryRemainingAmountkWH":"" + }, + "PluginState":"QC_CONNECTED", + "CruisingRangeAcOn":"107136.0", + "CruisingRangeAcOff":"115776.0", + "NotificationDateAndTime":"2016/02/14 20:28", + "TargetDate":"2016/02/14 20:28" + } + } + + # connected to a charging station + { + "status": 200, + "message": "success", + "BatteryStatusRecords": { + "OperationResult": "START", + "OperationDateAndTime": "Feb 19, 2016 12:12 PM", + "BatteryStatus": { + "BatteryChargingStatus": "NORMAL_CHARGING", + "BatteryCapacity": "12", + "BatteryRemainingAmount": "12", + "BatteryRemainingAmountWH": "", + "BatteryRemainingAmountkWH": "" + }, + "PluginState": "CONNECTED", + "CruisingRangeAcOn": "132000.0", + "CruisingRangeAcOff": "134000.0", + "TimeRequiredToFull200_6kW": { + "HourRequiredToFull": "0", + "MinutesRequiredToFull": "40" + }, + "NotificationDateAndTime": "2016/02/19 17:12", + "TargetDate": "2016/02/19 17:12" + } + } + """ + def __init__(self, status): + CarwingsResponse.__init__(self, status["BatteryStatusRecords"]) + self.answer = status + + recs = status["BatteryStatusRecords"] + + bs = recs["BatteryStatus"] + self.battery_capacity = bs["BatteryCapacity"] + self.battery_remaining_amount = bs["BatteryRemainingAmount"] + self.charging_status = bs["BatteryChargingStatus"] + self.is_charging = ("NOT_CHARGING" != bs["BatteryChargingStatus"]) # double negatives are fun + self.is_quick_charging = ("RAPIDLY_CHARGING" == bs["BatteryChargingStatus"]) + + self.plugin_state = recs["PluginState"] + self.is_connected = ("NOT_CONNECTED" != recs["PluginState"]) # another double negative + self.is_connected_to_quick_charger = ("QC_CONNECTED" == recs["PluginState"]) + + self._set_cruising_ranges(recs, off_key="CruisingRangeAcOff", on_key="CruisingRangeAcOn") + + if "TimeRequiredToFull" in recs: + self.time_to_full_trickle = timedelta(minutes=_time_remaining(recs["TimeRequiredToFull"])) + else: + self.time_to_full_trickle = None + + if "TimeRequiredToFull200" in recs: + self.time_to_full_l2 = timedelta(minutes=_time_remaining(recs["TimeRequiredToFull200"])) + else: + self.time_to_full_l2 = None + + if "TimeRequiredToFull200_6kW" in recs: + self.time_to_full_l2_6kw = timedelta(minutes=_time_remaining(recs["TimeRequiredToFull200_6kW"])) + else: + self.time_to_full_l2_6kw = None + + if float(self.battery_capacity) == 0: + log.debug("battery_capacity=0, status=%s", status) + self.battery_percent = 0 + else: + self.battery_percent = 100 * float(self.battery_remaining_amount) / 12 + + # Leaf 2016 has SOC (State Of Charge) in BatteryStatus, a more accurate battery_percentage + if "SOC" in bs: + self.state_of_charge = bs["SOC"]["Value"] + # Update battery_percent with more accurate version + self.battery_percent = float(self.state_of_charge) + else: + self.state_of_charge = None + + +class CarwingsElectricRateSimulationResponse(CarwingsResponse): + def __init__(self, status): + CarwingsResponse.__init__(self, status) + + r = status["PriceSimulatorDetailInfoResponsePersonalData"] + t = r["PriceSimulatorTotalInfo"] + + self.month = r["DisplayMonth"] # e.g. "Feb/2016" + + self.total_number_of_trips = t["TotalNumberOfTrips"] + self.total_power_consumption = t["TotalPowerConsumptTotal"] # in kWh + self.total_acceleration_power_consumption = t["TotalPowerConsumptMoter"] # in kWh + self.total_power_regenerated_in_braking = t["TotalPowerConsumptMinus"] # in kWh + self.total_travel_distance_km = float(t["TotalTravelDistance"]) / 1000 # assumed to be in meters + + self.total_electric_mileage = t["TotalElectricMileage"] + self.total_co2_reduction = t["TotalCO2Reductiont"] # (yep, extra 't' at the end) + + self.electricity_rate = r["ElectricPrice"] + self.electric_bill = r["ElectricBill"] + self.electric_cost_scale = r["ElectricCostScale"] # e.g. "miles/kWh" + + +class CarwingsMyCarFinderResponse(CarwingsResponse): + """ + { + "Location": { + "Country": "", + "Home": "OUTSIDE", + "LatitudeDeg": "69", + "LatitudeMin": "41", + "LatitudeMode": "NORTH", + "LatitudeSec": "5540", + "LocationType": "WGS84", + "LongitudeDeg": "18", + "LongitudeMin": "38", + "LongitudeMode": "EAST", + "LongitudeSec": "2506", + "Position": "UNAVAILABLE" + }, + "TargetDate": "2017/11/29 20:02", + "lat": "69.698722222222", + "lng": "18.640294444444", + "receivedDate": "2017/11/29 20:02", + "responseFlag": "1", + "resultCode": "1", + "status": 200, + "timeStamp": "2017-11-29 20:02:45" + } + """ + def __init__(self, status): + CarwingsResponse.__init__(self, status) + + self.latitude = status["lat"] + self.longitude = status["lng"] From c90a5868259e76b8787caa44de2616888743ba1c Mon Sep 17 00:00:00 2001 From: mekrapp <158028484+mekrapp@users.noreply.github.com> Date: Sat, 15 Jun 2024 15:55:38 +0200 Subject: [PATCH 05/55] Add files via upload included module "fetch-soc" is derived from former soc.py in OpenWB V1.9 --- packages/modules/vehicles/leaf/soc.py | 79 +++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 packages/modules/vehicles/leaf/soc.py diff --git a/packages/modules/vehicles/leaf/soc.py b/packages/modules/vehicles/leaf/soc.py new file mode 100644 index 0000000000..e73720e4f5 --- /dev/null +++ b/packages/modules/vehicles/leaf/soc.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python3 +from typing import List + +import logging +import time + +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_vehicle import VehicleUpdateData +from modules.common.component_state import CarState +from modules.common.configurable_vehicle import ConfigurableVehicle +from modules.vehicles.leaf.config import LeafSoc, LeafConfiguration + +from modules.vehicles.leaf import pycarwings2 + +log = logging.getLogger(__name__) + + +def fetch_soc(username, password, chargepoint): + + region = "NE" + + def getNissanSession(): # open Https session with Nissan server + log.debug("LP%s: login = %s, region = %s" % (chargepoint, username, region)) + s = pycarwings2.Session(username, password, region) + leaf = s.get_leaf() + time.sleep(1) # give Nissan server some time + return leaf + + def readSoc(leaf): # get SoC from Nissan server + leaf_info = leaf.get_latest_battery_status() + bat_percent = int(leaf_info.battery_percent) + log.debug("LP%s: Battery status %s" % (chargepoint, bat_percent)) + return bat_percent + + def requestSoc(leaf): # request Nissan server to request last SoC from car + log.debug("LP%s: Request SoC Update" % (chargepoint)) + key = leaf.request_update() + status = leaf.get_status_from_update(key) + sleepsecs = 20 + while status is None: + log.debug("Waiting {0} seconds".format(sleepsecs)) + time.sleep(sleepsecs) + status = leaf.get_status_from_update(key) + log.debug("LP%s: Finished updating" % (chargepoint)) + + leaf = getNissanSession() # start Https session with Nissan Server + readSoc(leaf) # old SoC needs to be read from server before requesting new SoC from car + time.sleep(1) # give Nissan server some time + requestSoc(leaf) # Nissan server to request new SoC from car + time.sleep(1) # give Nissan server some time + soc = readSoc(leaf) # final read of SoC from server + return soc + + +def create_vehicle(vehicle_config: LeafSoc, vehicle: int): + def updater(vehicle_update_data: VehicleUpdateData) -> CarState: + return fetch_soc( + vehicle_config.configuration.user_id, + vehicle_config.configuration.password, + vehicle) + return ConfigurableVehicle(vehicle_config=vehicle_config, component_updater=updater, vehicle=vehicle) + + +def leaf_update(user_id: str, password: str, charge_point: int): + log.debug("Leaf: user_id="+user_id+"charge_point="+str(charge_point)) + vehicle_config = LeafSoc(configuration=LeafConfiguration(charge_point, user_id, password)) + store.get_car_value_store(charge_point).store.set(fetch_soc( + vehicle_config.configuration.user_id, + vehicle_config.configuration.password, + charge_point)) + + +def main(argv: List[str]): + run_using_positional_cli_args(leaf_update, argv) + + +device_descriptor = DeviceDescriptor(configuration_factory=LeafSoc) From 08d7c3eaad466c5956c5c05687a97a52a9a08142 Mon Sep 17 00:00:00 2001 From: mekrapp <158028484+mekrapp@users.noreply.github.com> Date: Sun, 16 Jun 2024 09:59:01 +0200 Subject: [PATCH 06/55] Update soc.py --- packages/modules/vehicles/leaf/soc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/modules/vehicles/leaf/soc.py b/packages/modules/vehicles/leaf/soc.py index e73720e4f5..80a1a826a2 100644 --- a/packages/modules/vehicles/leaf/soc.py +++ b/packages/modules/vehicles/leaf/soc.py @@ -12,7 +12,7 @@ from modules.common.configurable_vehicle import ConfigurableVehicle from modules.vehicles.leaf.config import LeafSoc, LeafConfiguration -from modules.vehicles.leaf import pycarwings2 +import pycarwings2 log = logging.getLogger(__name__) From d62c77b7d20ed6e78da037d212af04a14daa7ef5 Mon Sep 17 00:00:00 2001 From: mekrapp <158028484+mekrapp@users.noreply.github.com> Date: Sun, 16 Jun 2024 10:16:23 +0200 Subject: [PATCH 07/55] Update soc.py --- packages/modules/vehicles/leaf/soc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/modules/vehicles/leaf/soc.py b/packages/modules/vehicles/leaf/soc.py index 80a1a826a2..e8d7573f56 100644 --- a/packages/modules/vehicles/leaf/soc.py +++ b/packages/modules/vehicles/leaf/soc.py @@ -51,7 +51,7 @@ def requestSoc(leaf): # request Nissan server to request last SoC from car requestSoc(leaf) # Nissan server to request new SoC from car time.sleep(1) # give Nissan server some time soc = readSoc(leaf) # final read of SoC from server - return soc + return CarState(soc) def create_vehicle(vehicle_config: LeafSoc, vehicle: int): From fbfd4ec5709b1e426adaccb24377ef2f410f534d Mon Sep 17 00:00:00 2001 From: mekrapp <158028484+mekrapp@users.noreply.github.com> Date: Sun, 16 Jun 2024 10:18:25 +0200 Subject: [PATCH 08/55] Update soc.py --- packages/modules/vehicles/leaf/soc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/modules/vehicles/leaf/soc.py b/packages/modules/vehicles/leaf/soc.py index e8d7573f56..4360ae8585 100644 --- a/packages/modules/vehicles/leaf/soc.py +++ b/packages/modules/vehicles/leaf/soc.py @@ -17,7 +17,7 @@ log = logging.getLogger(__name__) -def fetch_soc(username, password, chargepoint): +def fetch_soc(username, password, chargepoint) -> CarState: region = "NE" From 320c0b331354e34bf454850570109a0cf1623d11 Mon Sep 17 00:00:00 2001 From: mekrapp <158028484+mekrapp@users.noreply.github.com> Date: Fri, 21 Jun 2024 22:12:26 +0200 Subject: [PATCH 09/55] Update soc.py --- packages/modules/vehicles/leaf/soc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/modules/vehicles/leaf/soc.py b/packages/modules/vehicles/leaf/soc.py index 4360ae8585..aca38bb55c 100644 --- a/packages/modules/vehicles/leaf/soc.py +++ b/packages/modules/vehicles/leaf/soc.py @@ -12,7 +12,7 @@ from modules.common.configurable_vehicle import ConfigurableVehicle from modules.vehicles.leaf.config import LeafSoc, LeafConfiguration -import pycarwings2 +from modules.vehicles.leaf import pycarwings2 log = logging.getLogger(__name__) From 3ba67d288d0ce8a67daa8401696bab2701864550 Mon Sep 17 00:00:00 2001 From: mekrapp <158028484+mekrapp@users.noreply.github.com> Date: Fri, 21 Jun 2024 22:13:21 +0200 Subject: [PATCH 10/55] Update responses.py --- packages/modules/vehicles/leaf/responses.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/modules/vehicles/leaf/responses.py b/packages/modules/vehicles/leaf/responses.py index f082e2dbb2..f7ce8b2581 100644 --- a/packages/modules/vehicles/leaf/responses.py +++ b/packages/modules/vehicles/leaf/responses.py @@ -14,7 +14,7 @@ import logging from datetime import timedelta, datetime -import pycarwings2 +from modules.vehicles.leaf import pycarwings2 log = logging.getLogger(__name__) From f6c97543cf4c5048974d3f7c805a9078acb4048b Mon Sep 17 00:00:00 2001 From: mekrapp <158028484+mekrapp@users.noreply.github.com> Date: Sat, 22 Jun 2024 11:10:39 +0200 Subject: [PATCH 11/55] Update packages/modules/vehicles/leaf/soc.py Co-authored-by: LKuemmel <76958050+LKuemmel@users.noreply.github.com> --- packages/modules/vehicles/leaf/soc.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/modules/vehicles/leaf/soc.py b/packages/modules/vehicles/leaf/soc.py index aca38bb55c..f2c458a8ec 100644 --- a/packages/modules/vehicles/leaf/soc.py +++ b/packages/modules/vehicles/leaf/soc.py @@ -39,10 +39,12 @@ def requestSoc(leaf): # request Nissan server to request last SoC from car key = leaf.request_update() status = leaf.get_status_from_update(key) sleepsecs = 20 - while status is None: + for i in range(0,3): log.debug("Waiting {0} seconds".format(sleepsecs)) time.sleep(sleepsecs) status = leaf.get_status_from_update(key) + if status is not None: + break log.debug("LP%s: Finished updating" % (chargepoint)) leaf = getNissanSession() # start Https session with Nissan Server From a03caf6578f5bb4d664223fb8b2014c74942975e Mon Sep 17 00:00:00 2001 From: mekrapp <158028484+mekrapp@users.noreply.github.com> Date: Sat, 22 Jun 2024 11:22:15 +0200 Subject: [PATCH 12/55] Update soc.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- packages/modules/vehicles/leaf/soc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/modules/vehicles/leaf/soc.py b/packages/modules/vehicles/leaf/soc.py index f2c458a8ec..a6a57e8cc6 100644 --- a/packages/modules/vehicles/leaf/soc.py +++ b/packages/modules/vehicles/leaf/soc.py @@ -39,7 +39,7 @@ def requestSoc(leaf): # request Nissan server to request last SoC from car key = leaf.request_update() status = leaf.get_status_from_update(key) sleepsecs = 20 - for i in range(0,3): + for i in range(0,9): log.debug("Waiting {0} seconds".format(sleepsecs)) time.sleep(sleepsecs) status = leaf.get_status_from_update(key) From b93dd5f18a88bfc261a7b5d62070de4e46ccbfca Mon Sep 17 00:00:00 2001 From: mekrapp <158028484+mekrapp@users.noreply.github.com> Date: Wed, 21 Aug 2024 21:53:26 +0200 Subject: [PATCH 13/55] Update __init__.py empty line at end of file removed accoring to warning from test run on Jul 15. From 1c142c7089b3860fb4edea873a3556beff06c7da Mon Sep 17 00:00:00 2001 From: mekrapp <158028484+mekrapp@users.noreply.github.com> Date: Wed, 21 Aug 2024 21:57:01 +0200 Subject: [PATCH 14/55] Update __init__.py From 38f7973436a3e84b94aff2e332e0fe11b8847937 Mon Sep 17 00:00:00 2001 From: mekrapp <158028484+mekrapp@users.noreply.github.com> Date: Wed, 21 Aug 2024 21:57:53 +0200 Subject: [PATCH 15/55] Update __init__.py From da78bd8078e94c0851ddcbcae99d2746a0689451 Mon Sep 17 00:00:00 2001 From: mekrapp <158028484+mekrapp@users.noreply.github.com> Date: Wed, 21 Aug 2024 22:20:26 +0200 Subject: [PATCH 16/55] Update __init__.py empty line removed at end of file From 9a0262377a60fc48598461e1fd025549963b5ebe Mon Sep 17 00:00:00 2001 From: mekrapp <158028484+mekrapp@users.noreply.github.com> Date: Sat, 24 Aug 2024 11:21:33 +0200 Subject: [PATCH 17/55] Update pycarwings2.py At import instruction wildcard * replaced by the names of the classes to be imported --- packages/modules/vehicles/leaf/pycarwings2.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/modules/vehicles/leaf/pycarwings2.py b/packages/modules/vehicles/leaf/pycarwings2.py index 1265fe762e..9ca54bc8a6 100644 --- a/packages/modules/vehicles/leaf/pycarwings2.py +++ b/packages/modules/vehicles/leaf/pycarwings2.py @@ -68,7 +68,11 @@ import json import logging from datetime import date -from responses import * +from responses import (CarwingsInitialAppResponse, CarwingsLoginResponse, CarwingsBatteryStatusResponse, + CarwingsStartClimateControlResponse, CarwingsStopClimateControlResponse, + CarwingsClimateControlScheduleResponse, CarwingsDrivingAnalysisResponse, + CarwingsLatestBatteryStatusResponse, CarwingsLatestClimateControlStatusResponse, + CarwingsElectricRateSimulationResponse, CarwingsMyCarFinderResponse) import base64 from Crypto.Cipher import Blowfish From 69d663f3cb0e2cb4aad9705d0d126b0be1c9bb50 Mon Sep 17 00:00:00 2001 From: mekrapp <158028484+mekrapp@users.noreply.github.com> Date: Sat, 24 Aug 2024 11:26:29 +0200 Subject: [PATCH 18/55] Update pycarwings2.py trailing spaces removed --- packages/modules/vehicles/leaf/pycarwings2.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/modules/vehicles/leaf/pycarwings2.py b/packages/modules/vehicles/leaf/pycarwings2.py index 9ca54bc8a6..9c94e66761 100644 --- a/packages/modules/vehicles/leaf/pycarwings2.py +++ b/packages/modules/vehicles/leaf/pycarwings2.py @@ -69,9 +69,9 @@ import logging from datetime import date from responses import (CarwingsInitialAppResponse, CarwingsLoginResponse, CarwingsBatteryStatusResponse, - CarwingsStartClimateControlResponse, CarwingsStopClimateControlResponse, - CarwingsClimateControlScheduleResponse, CarwingsDrivingAnalysisResponse, - CarwingsLatestBatteryStatusResponse, CarwingsLatestClimateControlStatusResponse, + CarwingsStartClimateControlResponse, CarwingsStopClimateControlResponse, + CarwingsClimateControlScheduleResponse, CarwingsDrivingAnalysisResponse, + CarwingsLatestBatteryStatusResponse, CarwingsLatestClimateControlStatusResponse, CarwingsElectricRateSimulationResponse, CarwingsMyCarFinderResponse) import base64 from Crypto.Cipher import Blowfish From f8ee104486b055a04d89228c1b981ae264a2374f Mon Sep 17 00:00:00 2001 From: mekrapp <158028484+mekrapp@users.noreply.github.com> Date: Sat, 24 Aug 2024 11:29:34 +0200 Subject: [PATCH 19/55] Update __init__.py Empty line at end of file removed From c629c1e0f3ab455b8049203de9f56b2414a03f7f Mon Sep 17 00:00:00 2001 From: mekrapp <158028484+mekrapp@users.noreply.github.com> Date: Sat, 24 Aug 2024 11:31:55 +0200 Subject: [PATCH 20/55] Update soc.py missing whitespace after "," added --- packages/modules/vehicles/leaf/soc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/modules/vehicles/leaf/soc.py b/packages/modules/vehicles/leaf/soc.py index a6a57e8cc6..416422b72f 100644 --- a/packages/modules/vehicles/leaf/soc.py +++ b/packages/modules/vehicles/leaf/soc.py @@ -39,7 +39,7 @@ def requestSoc(leaf): # request Nissan server to request last SoC from car key = leaf.request_update() status = leaf.get_status_from_update(key) sleepsecs = 20 - for i in range(0,9): + for i in range(0, 9): log.debug("Waiting {0} seconds".format(sleepsecs)) time.sleep(sleepsecs) status = leaf.get_status_from_update(key) From 920abadc141fbfacbf44c56134620f83f2466845 Mon Sep 17 00:00:00 2001 From: mekrapp <158028484+mekrapp@users.noreply.github.com> Date: Sat, 24 Aug 2024 11:41:27 +0200 Subject: [PATCH 21/55] Update __init__.py From 6ce31f1586f111c898efab0839dfca9329ef9ec4 Mon Sep 17 00:00:00 2001 From: mekrapp <158028484+mekrapp@users.noreply.github.com> Date: Sat, 24 Aug 2024 11:54:16 +0200 Subject: [PATCH 22/55] 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 --- packages/modules/vehicles/leaf/__init__.py | 1 - 1 file changed, 1 deletion(-) delete mode 100644 packages/modules/vehicles/leaf/__init__.py diff --git a/packages/modules/vehicles/leaf/__init__.py b/packages/modules/vehicles/leaf/__init__.py deleted file mode 100644 index 8b13789179..0000000000 --- a/packages/modules/vehicles/leaf/__init__.py +++ /dev/null @@ -1 +0,0 @@ - From 9b6a8338b349ace6b06800824f3697c4aeb8406e Mon Sep 17 00:00:00 2001 From: mekrapp <158028484+mekrapp@users.noreply.github.com> Date: Sat, 24 Aug 2024 11:55:48 +0200 Subject: [PATCH 23/55] Add files via upload --- packages/modules/vehicles/leaf/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 packages/modules/vehicles/leaf/__init__.py diff --git a/packages/modules/vehicles/leaf/__init__.py b/packages/modules/vehicles/leaf/__init__.py new file mode 100644 index 0000000000..e69de29bb2 From 2d9d5b91f07a6157009c7cd8b8e7a6f39616ed54 Mon Sep 17 00:00:00 2001 From: mekrapp <158028484+mekrapp@users.noreply.github.com> Date: Thu, 29 Aug 2024 22:51:33 +0200 Subject: [PATCH 24/55] Update requirements.txt pycarwings2 added to load this library (for the SOC module of Nissan Leaf) during start --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index f73b9e6592..fce126f59f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -23,3 +23,4 @@ pytz==2023.3.post1 grpcio==1.60.1 protobuf==4.25.3 bimmer_connected==0.15.1 +pycarwings2==2.14 From c41725f8ff4c928eaa4c7d787fed13a6b7542b05 Mon Sep 17 00:00:00 2001 From: mekrapp <158028484+mekrapp@users.noreply.github.com> Date: Fri, 30 Aug 2024 11:05:30 +0200 Subject: [PATCH 25/55] Update packages/modules/vehicles/leaf/soc.py Co-authored-by: LKuemmel <76958050+LKuemmel@users.noreply.github.com> --- packages/modules/vehicles/leaf/soc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/modules/vehicles/leaf/soc.py b/packages/modules/vehicles/leaf/soc.py index 416422b72f..dc83465b19 100644 --- a/packages/modules/vehicles/leaf/soc.py +++ b/packages/modules/vehicles/leaf/soc.py @@ -12,7 +12,7 @@ from modules.common.configurable_vehicle import ConfigurableVehicle from modules.vehicles.leaf.config import LeafSoc, LeafConfiguration -from modules.vehicles.leaf import pycarwings2 +import pycarwings2 log = logging.getLogger(__name__) From 4d8783f66278e3f355ae25f8477146b88d155505 Mon Sep 17 00:00:00 2001 From: mekrapp <158028484+mekrapp@users.noreply.github.com> Date: Fri, 30 Aug 2024 11:18:02 +0200 Subject: [PATCH 26/55] Delete packages/modules/vehicles/leaf/pycarwings2.py File deleted at this position as requested --- packages/modules/vehicles/leaf/pycarwings2.py | 466 ------------------ 1 file changed, 466 deletions(-) delete mode 100644 packages/modules/vehicles/leaf/pycarwings2.py diff --git a/packages/modules/vehicles/leaf/pycarwings2.py b/packages/modules/vehicles/leaf/pycarwings2.py deleted file mode 100644 index 9c94e66761..0000000000 --- a/packages/modules/vehicles/leaf/pycarwings2.py +++ /dev/null @@ -1,466 +0,0 @@ -# Copyright 2016 Jason Horne -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -When logging in, you must specify a geographic 'region' parameter. The only -known values for this are as follows: - - NNA : USA - NE : Europe - NCI : Canada - NMA : Australia - NML : Japan - -Information about Nissan on the web (e.g. http://nissannews.com/en-US/nissan/usa/pages/executive-bios) -suggests others (this page suggests NMEX for Mexico, NLAC for Latin America) but -these have not been confirmed. - -There are three asynchronous operations in this API, paired with three follow-up -"status check" methods. - - request_update -> get_status_from_update - start_climate_control -> get_start_climate_control_result - stop_climate_control -> get_stop_climate_control_result - -The asynchronous operations immediately return a 'result key', which -is then supplied as a parameter for the corresponding status check method. - -Here's an example response from an asynchronous operation, showing the result key: - - { - "status":200, - "userId":"user@domain.com", - "vin":"1ABCDEFG2HIJKLM3N", - "resultKey":"12345678901234567890123456789012345678901234567890" - } - -The status check methods return a JSON blob containing a 'responseFlag' property. -If the communications are complete, the response flag value will be the string "1"; -otherwise the value will be the string "0". You just gotta poll until you get a -"1" back. Note that the official app seems to poll every 20 seconds. - -Example 'no response yet' result from a status check invocation: - - { - "status":200, - "responseFlag":"0" - } - -When the responseFlag does come back as "1", there will also be an "operationResult" -property. If there was an error communicating with the vehicle, it seems that -this field will contain the value "ELECTRIC_WAVE_ABNORMAL". Odd. - -""" - -import requests -from requests import Request, RequestException -import json -import logging -from datetime import date -from responses import (CarwingsInitialAppResponse, CarwingsLoginResponse, CarwingsBatteryStatusResponse, - CarwingsStartClimateControlResponse, CarwingsStopClimateControlResponse, - CarwingsClimateControlScheduleResponse, CarwingsDrivingAnalysisResponse, - CarwingsLatestBatteryStatusResponse, CarwingsLatestClimateControlStatusResponse, - CarwingsElectricRateSimulationResponse, CarwingsMyCarFinderResponse) -import base64 -from Crypto.Cipher import Blowfish - -BASE_URL = "https://gdcportalgw.its-mo.com/api_v230317_NE/gdc/" -log = logging.getLogger(__name__) - - -# from http://stackoverflow.com/questions/17134100/python-blowfish-encryption -def _PKCS5Padding(string): - byteNum = len(string) - packingLength = 8 - byteNum % 8 - appendage = chr(packingLength) * packingLength - return string + appendage - - -class CarwingsError(Exception): - pass - - -class Session(object): - """Maintains a connection to CARWINGS, refreshing it when needed""" - - def __init__(self, username, password, region="NNA"): - self.username = username - self.password = password - self.region_code = region - self.logged_in = False - self.custom_sessionid = None - - def _request_with_retry(self, endpoint, params): - ret = self._request(endpoint, params) - - if ("status" in ret) and (ret["status"] >= 400): - log.error( - "carwings error; logging in and trying request again: %s" % ret) - # try logging in again - self.connect() - ret = self._request(endpoint, params) - - return ret - - def _request(self, endpoint, params): - params["initial_app_str"] = "9s5rfKVuMrT03RtzajWNcA" - if self.custom_sessionid: - params["custom_sessionid"] = self.custom_sessionid - else: - params["custom_sessionid"] = "" - - req = Request('POST', url=BASE_URL + endpoint, data=params, headers={"User-Agent": ""}).prepare() - # ", headers={"User-Agent": ""}" added, - # see https://github.com/filcole/pycarwings2/blob/master/pycarwings2/pycarwings2.py - # added parameter is needed, to be able to run pycarwings2.py on a Windows PC with Python 3.12 - - log.debug("invoking carwings API: %s" % req.url) - log.debug("params: %s" % json.dumps( - {k: v.decode('utf-8') if isinstance(v, bytes) - else v for k, v in params.items()}, - sort_keys=True, indent=3, separators=(',', ': ')) - ) - - try: - sess = requests.Session() - response = sess.send(req) - log.debug('Response HTTP Status Code: {status_code}'.format( - status_code=response.status_code)) - log.debug('Response HTTP Response Body: {content}'.format( - content=response.content)) - except RequestException: - log.warning('HTTP Request failed') - raise CarwingsError - - # Nissan servers can return html instead of jSOn on occassion, e.g. - # - # - # - # 503 Service Temporarily Unavailable - # - #

Service Temporarily Unavailable> - #

The server is temporarily unable to service your - # request due to maintenance downtime or capacity - # problems. Please try again later.

- # - try: - j = json.loads(response.text) - except ValueError: - log.error("Invalid JSON returned") - raise CarwingsError - - if "message" in j and j["message"] == "INVALID PARAMS": - log.error("carwings error %s: %s" % (j["message"], j["status"])) - raise CarwingsError("INVALID PARAMS") - if "ErrorMessage" in j: - log.error("carwings error %s: %s" % - (j["ErrorCode"], j["ErrorMessage"])) - raise CarwingsError - - return j - - def connect(self): - self.custom_sessionid = None - self.logged_in = False - - response = self._request("InitialApp_v2.php", { - "RegionCode": self.region_code, - "lg": "en-US", - }) - ret = CarwingsInitialAppResponse(response) - - c1 = Blowfish.new(ret.baseprm.encode(), Blowfish.MODE_ECB) - packedPassword = _PKCS5Padding(self.password) - encryptedPassword = c1.encrypt(packedPassword.encode()) - encodedPassword = base64.standard_b64encode(encryptedPassword) - - response = self._request("UserLoginRequest.php", { - "RegionCode": self.region_code, - "UserId": self.username, - "Password": encodedPassword, - }) - - ret = CarwingsLoginResponse(response) - - self.custom_sessionid = ret.custom_sessionid - - self.gdc_user_id = ret.gdc_user_id - log.debug("gdc_user_id: %s" % self.gdc_user_id) - self.dcm_id = ret.dcm_id - log.debug("dcm_id: %s" % self.dcm_id) - self.tz = ret.tz - log.debug("tz: %s" % self.tz) - self.language = ret.language - log.debug("language: %s" % self.language) - log.debug("vin: %s" % ret.vin) - log.debug("nickname: %s" % ret.nickname) - - self.leaf = Leaf(self, ret.leafs[0]) - - self.logged_in = True - - return ret - - def get_leaf(self, index=0): - if not self.logged_in: - self.connect() - - return self.leaf - - -class Leaf: - def __init__(self, session, params): - self.session = session - self.vin = params["vin"] - self.nickname = params["nickname"] - self.bound_time = params["bound_time"] - log.debug("created leaf %s/%s" % (self.vin, self.nickname)) - - def request_update(self): - response = self.session._request_with_retry("BatteryStatusCheckRequest.php", { - "RegionCode": self.session.region_code, - "VIN": self.vin, - }) - return response["resultKey"] - - def get_status_from_update(self, result_key): - response = self.session._request_with_retry("BatteryStatusCheckResultRequest.php", { - "RegionCode": self.session.region_code, - "lg": self.session.language, - "DCMID": self.session.dcm_id, - "VIN": self.vin, - "tz": self.session.tz, - "resultKey": result_key, - }) - # responseFlag will be "1" if a response has been returned; "0" otherwise - if response["responseFlag"] == "1": - return CarwingsBatteryStatusResponse(response) - - return None - - def start_climate_control(self): - response = self.session._request_with_retry("ACRemoteRequest.php", { - "RegionCode": self.session.region_code, - "lg": self.session.language, - "DCMID": self.session.dcm_id, - "VIN": self.vin, - "tz": self.session.tz, - }) - return response["resultKey"] - - def get_start_climate_control_result(self, result_key): - response = self.session._request_with_retry("ACRemoteResult.php", { - "RegionCode": self.session.region_code, - "lg": self.session.language, - "DCMID": self.session.dcm_id, - "VIN": self.vin, - "tz": self.session.tz, - "UserId": self.session.gdc_user_id, # this userid is the 'gdc' userid - "resultKey": result_key, - }) - if response["responseFlag"] == "1": - return CarwingsStartClimateControlResponse(response) - - return None - - def stop_climate_control(self): - response = self.session._request_with_retry("ACRemoteOffRequest.php", { - "RegionCode": self.session.region_code, - "lg": self.session.language, - "DCMID": self.session.dcm_id, - "VIN": self.vin, - "tz": self.session.tz, - }) - return response["resultKey"] - - def get_stop_climate_control_result(self, result_key): - response = self.session._request_with_retry("ACRemoteOffResult.php", { - "RegionCode": self.session.region_code, - "lg": self.session.language, - "DCMID": self.session.dcm_id, - "VIN": self.vin, - "tz": self.session.tz, - "UserId": self.session.gdc_user_id, # this userid is the 'gdc' userid - "resultKey": result_key, - }) - if response["responseFlag"] == "1": - return CarwingsStopClimateControlResponse(response) - - return None - - # execute time example: "2016-02-09 17:24" - # I believe this time is specified in GMT, despite the "tz" parameter - # TODO: change parameter to python datetime object(?) - def schedule_climate_control(self, execute_time): - response = self.session._request_with_retry("ACRemoteNewRequest.php", { - "RegionCode": self.session.region_code, - "lg": self.session.language, - "DCMID": self.session.dcm_id, - "VIN": self.vin, - "tz": self.session.tz, - "ExecuteTime": execute_time, - }) - return (response["status"] == 200) - - # execute time example: "2016-02-09 17:24" - # I believe this time is specified in GMT, despite the "tz" parameter - # TODO: change parameter to python datetime object(?) - def update_scheduled_climate_control(self, execute_time): - response = self.session._request_with_retry("ACRemoteUpdateRequest.php", { - "RegionCode": self.session.region_code, - "lg": self.session.language, - "DCMID": self.session.dcm_id, - "VIN": self.vin, - "tz": self.session.tz, - "ExecuteTime": execute_time, - }) - return (response["status"] == 200) - - def cancel_scheduled_climate_control(self): - response = self.session._request_with_retry("ACRemoteCancelRequest.php", { - "RegionCode": self.session.region_code, - "lg": self.session.language, - "DCMID": self.session.dcm_id, - "VIN": self.vin, - "tz": self.session.tz, - }) - return (response["status"] == 200) - - def get_climate_control_schedule(self): - response = self.session._request_with_retry("GetScheduledACRemoteRequest.php", { - "RegionCode": self.session.region_code, - "lg": self.session.language, - "DCMID": self.session.dcm_id, - "VIN": self.vin, - "tz": self.session.tz, - }) - if (response["status"] == 200): - if response["ExecuteTime"] != "": - return CarwingsClimateControlScheduleResponse(response) - - return None - - """ - { - "status":200, - } - """ - - def start_charging(self): - response = self.session._request_with_retry("BatteryRemoteChargingRequest.php", { - "RegionCode": self.session.region_code, - "lg": self.session.language, - "DCMID": self.session.dcm_id, - "VIN": self.vin, - "tz": self.session.tz, - "ExecuteTime": date.today().isoformat() - }) - if response["status"] == 200: - # This only indicates that the charging command has been received by the - # Nissan servers, it does not indicate that the car is now charging. - return True - - return False - - def get_driving_analysis(self): - response = self.session._request_with_retry("DriveAnalysisBasicScreenRequestEx.php", { - "RegionCode": self.session.region_code, - "lg": self.session.language, - "DCMID": self.session.dcm_id, - "VIN": self.vin, - "tz": self.session.tz, - }) - if response["status"] == 200: - return CarwingsDrivingAnalysisResponse(response) - - return None - - def get_latest_battery_status(self): - response = self.session._request_with_retry("BatteryStatusRecordsRequest.php", { - "RegionCode": self.session.region_code, - "lg": self.session.language, - "DCMID": self.session.dcm_id, - "VIN": self.vin, - "tz": self.session.tz, - "TimeFrom": self.bound_time - }) - if response["status"] == 200: - if "BatteryStatusRecords" in response: - return CarwingsLatestBatteryStatusResponse(response) - else: - log.warning('no battery status record returned by server') - - return None - - def get_latest_hvac_status(self): - response = self.session._request_with_retry("RemoteACRecordsRequest.php", { - "RegionCode": self.session.region_code, - "lg": self.session.language, - "DCMID": self.session.dcm_id, - "VIN": self.vin, - "tz": self.session.tz, - "TimeFrom": self.bound_time - }) - if response["status"] == 200: - if "RemoteACRecords" in response: - return CarwingsLatestClimateControlStatusResponse(response) - else: - log.warning('no remote a/c records returned by server') - - return None - - # target_month format: "YYYYMM" e.g. "201602" - def get_electric_rate_simulation(self, target_month): - response = self.session._request_with_retry("PriceSimulatorDetailInfoRequest.php", { - "RegionCode": self.session.region_code, - "lg": self.session.language, - "DCMID": self.session.dcm_id, - "VIN": self.vin, - "tz": self.session.tz, - "TargetMonth": target_month - }) - if response["status"] == 200: - return CarwingsElectricRateSimulationResponse(response) - - return None - - def request_location(self): - # As of 25th July the Locate My Vehicle functionality of the Europe version of the - # Nissan APIs was removed. It may return, so this call is left here. - # It currently errors with a 404 MyCarFinderRequest.php was not found on this server - # for European users. - response = self.session._request_with_retry("MyCarFinderRequest.php", { - "RegionCode": self.session.region_code, - "lg": self.session.language, - "DCMID": self.session.dcm_id, - "VIN": self.vin, - "tz": self.session.tz, - "UserId": self.session.gdc_user_id, # this userid is the 'gdc' userid - }) - return response["resultKey"] - - def get_status_from_location(self, result_key): - response = self.session._request_with_retry("MyCarFinderResultRequest.php", { - "RegionCode": self.session.region_code, - "lg": self.session.language, - "DCMID": self.session.dcm_id, - "VIN": self.vin, - "tz": self.session.tz, - "resultKey": result_key, - }) - if response["responseFlag"] == "1": - return CarwingsMyCarFinderResponse(response) - - return None From 8f878796c4f567e7a7a2f1b61880a8a15b8eb5bb Mon Sep 17 00:00:00 2001 From: mekrapp <158028484+mekrapp@users.noreply.github.com> Date: Fri, 30 Aug 2024 11:18:28 +0200 Subject: [PATCH 27/55] Delete packages/modules/vehicles/leaf/responses.py File deleted at this position as requested --- packages/modules/vehicles/leaf/responses.py | 724 -------------------- 1 file changed, 724 deletions(-) delete mode 100644 packages/modules/vehicles/leaf/responses.py diff --git a/packages/modules/vehicles/leaf/responses.py b/packages/modules/vehicles/leaf/responses.py deleted file mode 100644 index f7ce8b2581..0000000000 --- a/packages/modules/vehicles/leaf/responses.py +++ /dev/null @@ -1,724 +0,0 @@ -# Copyright 2016 Jason Horne -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import logging -from datetime import timedelta, datetime -from modules.vehicles.leaf import pycarwings2 - -log = logging.getLogger(__name__) - - -def _time_remaining(t): - minutes = float(0) - if t: - if ("hours" in t) and t["hours"]: - minutes = 60 * float(t["hours"]) - elif ("HourRequiredToFull" in t) and t["HourRequiredToFull"]: - minutes = 60 * float(t["HourRequiredToFull"]) - if ("minutes" in t) and t["minutes"]: - minutes += float(t["minutes"]) - elif ("MinutesRequiredToFull" in t) and t["MinutesRequiredToFull"]: - minutes += float(t["MinutesRequiredToFull"]) - - return minutes - - -class CarwingsResponse: - def __init__(self, response): - op_result = None - if ("operationResult" in response): - op_result = response["operationResult"] - elif ("OperationResult" in response): - op_result = response["OperationResult"] - - # seems to indicate that the vehicle cannot be reached - if ("ELECTRIC_WAVE_ABNORMAL" == op_result): - log.error("could not establish communications with vehicle") - raise pycarwings2.CarwingsError("could not establish communications with vehicle") - - def _set_cruising_ranges(self, status, off_key="cruisingRangeAcOff", on_key="cruisingRangeAcOn"): - if off_key in status: - self.cruising_range_ac_off_km = float(status[off_key]) / 1000 - if on_key in status: - self.cruising_range_ac_on_km = float(status[on_key]) / 1000 - - def _set_timestamp(self, status): - self.timestamp = datetime.strptime(status["timeStamp"], "%Y-%m-%d %H:%M:%S") # "2016-01-02 17:17:38" - - -class CarwingsInitialAppResponse(CarwingsResponse): - def __init__(self, response): - CarwingsResponse.__init__(self, response) - self.baseprm = response["baseprm"] - - -class CarwingsLoginResponse(CarwingsResponse): - """ - example JSON response to login: - { - "status":200, - "message":"success", - "sessionId":"12345678-1234-1234-1234-1234567890", - "VehicleInfoList": { - "VehicleInfo": [ - { - "charger20066":"false", - "nickname":"LEAF", - "telematicsEnabled":"true", - "vin":"1ABCDEFG2HIJKLM3N" - } - ], - "vehicleInfo": [ - { - "charger20066":"false", - "nickname":"LEAF", - "telematicsEnabled":"true", - "vin":"1ABCDEFG2HIJKLM3N" - } - ] - }, - "vehicle": { - "profile": { - "vin":"1ABCDEFG2HIJKLM3N", - "gdcUserId":"FG12345678", - "gdcPassword":"password", - "encAuthToken":"ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZ", - "dcmId":"123456789012", - "nickname":"Alpha124", - "status":"ACCEPTED", - "statusDate": "Aug 15, 2015 07:00 PM" - } - }, - "EncAuthToken":"ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZ", - "CustomerInfo": { - "UserId":"AB12345678", - "Language":"en-US", - "Timezone":"America/New_York", - "RegionCode":"NNA", - "OwnerId":"1234567890", - "Nickname":"Bravo456", - "Country":"US", - "VehicleImage":"/content/language/default/images/img/ph_car.jpg", - "UserVehicleBoundDurationSec":"999971200", - "VehicleInfo": { - "VIN":"1ABCDEFG2HIJKLM3N", - "DCMID":"201212345678", - "SIMID":"12345678901234567890", - "NAVIID":"1234567890", - "EncryptedNAVIID":"1234567890ABCDEFGHIJKLMNOP", - "MSN":"123456789012345", - "LastVehicleLoginTime":"", - "UserVehicleBoundTime":"2015-08-17T14:16:32Z", - "LastDCMUseTime":"" - } - }, - "UserInfoRevisionNo":"1" - } - """ - def __init__(self, response): - CarwingsResponse.__init__(self, response) - - profile = response["vehicle"]["profile"] - self.gdc_user_id = profile["gdcUserId"] - self.dcm_id = profile["dcmId"] - self.vin = profile["vin"] - - # vehicleInfo block may be top level, or contained in a VehicleInfoList object; - # why it's sometimes one way and sometimes another is not clear. - if "VehicleInfoList" in response: - self.nickname = response["VehicleInfoList"]["vehicleInfo"][0]["nickname"] - self.custom_sessionid = response["VehicleInfoList"]["vehicleInfo"][0]["custom_sessionid"] - elif "vehicleInfo" in response: - self.nickname = response["vehicleInfo"][0]["nickname"] - self.custom_sessionid = response["vehicleInfo"][0]["custom_sessionid"] - - customer_info = response["CustomerInfo"] - self.tz = customer_info["Timezone"] - self.language = customer_info["Language"] - self.user_vehicle_bound_time = customer_info["VehicleInfo"]["UserVehicleBoundTime"] - - self.leafs = [{ - "vin": self.vin, - "nickname": self.nickname, - "bound_time": self.user_vehicle_bound_time - }] - - -class CarwingsBatteryStatusResponse(CarwingsResponse): - """ - Note that before December 2018 this returned a response. Between Dec-2018 and Aug-2019 - it did not return a response from the Nissan Servers. As of Aug 2019 a response is now - returned again. - - # Original - { - "status": 200, - "message": "success", - "responseFlag": "1", - "operationResult": "START", - "timeStamp": "2016-01-02 17:17:38", - "cruisingRangeAcOn": "115328.0", - "cruisingRangeAcOff": "117024.0", - "currentChargeLevel": "0", - "chargeMode": "220V", - "pluginState": "CONNECTED", - "charging": "YES", - "chargeStatus": "CT", - "batteryDegradation": "10", - "batteryCapacity": "12", - "timeRequiredToFull": { - "hours": "", - "minutes": "" - }, - "timeRequiredToFull200": { - "hours": "", - "minutes": "" - }, - "timeRequiredToFull200_6kW": { - "hours": "", - "minutes": "" - } - } - - # As at 21/01/2019 for a 30kWh Leaf now seems that - # BatteryStatusCheckResultRequest.php always returns this - # regardless of battery status. - { - "status":200, - "responseFlag":"0" - } - - { - "status":200, - "message":"success", - "responseFlag":"1", - "operationResult":"START", - "timeStamp":"2016-02-14 20:28:45", - "cruisingRangeAcOn":"107136.0", - "cruisingRangeAcOff":"115776.0", - "currentChargeLevel":"0", - "chargeMode":"NOT_CHARGING", - "pluginState":"QC_CONNECTED", - "charging":"YES", - "chargeStatus":"CT", - "batteryDegradation":"11", - "batteryCapacity":"12", - "timeRequiredToFull":{ - "hours":"", - "minutes":"" - }, - "timeRequiredToFull200":{ - "hours":"", - "minutes":"" - }, - "timeRequiredToFull200_6kW":{ - "hours":"", - "minutes":"" - } - } - - # As at 22/08/2019 for a 30kWh Leaf now seesm that - # BatteryStatusCheckResultRequest.php returns data again - # after polling a number of times. - { - "status": 200, - "responseFlag": "1", - "operationResult": "START", - "timeStamp": "2019-08-22 10:26:51", - "cruisingRangeAcOn": "129000.0", - "cruisingRangeAcOff": "132000.0", - "currentChargeLevel": "0", - "chargeMode": "NOT_CHARGING", - "pluginState": "NOT_CONNECTED", - "charging": "NO", - "chargeStatus": "0", - "batteryDegradation": "180", - "batteryCapacity": "240", - "timeRequiredToFull": { - "hours": "11", - "minutes": "30" - }, - "timeRequiredToFull200": { - "hours": "6", - "minutes": "30" - }, - "timeRequiredToFull200_6kW": { - "hours": "2", - "minutes": "30" - } - } - - """ - def __init__(self, status): - CarwingsResponse.__init__(self, status) - - self._set_timestamp(status) - self._set_cruising_ranges(status) - - self.answer = status - - self.battery_capacity = status["batteryCapacity"] - self.battery_degradation = status["batteryDegradation"] - - self.is_connected = ("NOT_CONNECTED" != status["pluginState"]) # fun double negative - self.plugin_state = status["pluginState"] - - self.charging_status = status["chargeMode"] - - self.is_charging = ("YES" == status["charging"]) - - self.is_quick_charging = ("RAPIDLY_CHARGING" == status["chargeMode"]) - self.is_connected_to_quick_charger = ("QC_CONNECTED" == status["pluginState"]) - - self.time_to_full_trickle = timedelta(minutes=_time_remaining(status["timeRequiredToFull"])) - self.time_to_full_l2 = timedelta(minutes=_time_remaining(status["timeRequiredToFull200"])) - self.time_to_full_l2_6kw = timedelta(minutes=_time_remaining(status["timeRequiredToFull200_6kW"])) - - # For some leafs the battery_percent is not returned - self.battery_percent = 100 * float(self.battery_degradation) / 12 - - -class CarwingsLatestClimateControlStatusResponse(CarwingsResponse): - """ - climate control on: - { - "status":200, - "message":"success", - "RemoteACRecords":{ - "OperationResult":"START_BATTERY", - "OperationDateAndTime":"Feb 10, 2016 10:22 PM", - "RemoteACOperation":"START", - "ACStartStopDateAndTime":"Feb 10, 2016 10:23 PM", - "CruisingRangeAcOn":"107712.0", - "CruisingRangeAcOff":"109344.0", - "ACStartStopURL":"", - "PluginState":"NOT_CONNECTED", - "ACDurationBatterySec":"900", - "ACDurationPluggedSec":"7200" - }, - "OperationDateAndTime":"" - } - - climate control off: - { - "status":200, - "message":"success", - "RemoteACRecords":{ - "OperationResult":"START", - "OperationDateAndTime":"Feb 10, 2016 10:26 PM", - "RemoteACOperation":"STOP", - "ACStartStopDateAndTime":"Feb 10, 2016 10:27 PM", - "CruisingRangeAcOn":"111936.0", - "CruisingRangeAcOff":"113632.0", - "ACStartStopURL":"", - "PluginState":"NOT_CONNECTED", - "ACDurationBatterySec":"900", - "ACDurationPluggedSec":"7200" - }, - "OperationDateAndTime":"" - } - - error: - { - "status":200, - "RemoteACRecords":{ - "OperationResult":"ELECTRIC_WAVE_ABNORMAL", - "OperationDateAndTime":"2018/04/08 10:00", - "RemoteACOperation":"START", - "ACStartStopDateAndTime":"08-Apr-2018 11:06", - "ACStartStopURL":"", - "PluginState":"INVALID", - "ACDurationBatterySec":"900", - "ACDurationPluggedSec":"7200", - "PreAC_unit":"C", - "PreAC_temp":"22" - } - } - noinfo (from a 2014 24kWh Leaf): - { - "status":200, - "RemoteACRecords":[] - } - - """ - def __init__(self, status): - CarwingsResponse.__init__(self, status["RemoteACRecords"]) - racr = status["RemoteACRecords"] - - self._set_cruising_ranges(racr, on_key="CruisingRangeAcOn", off_key="CruisingRangeAcOff") - - # If no empty RemoteACRecords list is returned then assume CC is off. - if type(racr) is not dict: - self.is_hvac_running = False - else: - # Seems to be running only if both of these contain "START". - self.is_hvac_running = ( - racr["OperationResult"] and - racr["OperationResult"].startswith("START") and - racr["RemoteACOperation"] == "START" - ) - - -class CarwingsStartClimateControlResponse(CarwingsResponse): - """ - { - "status":200, - "message":"success", - "responseFlag":"1", - "operationResult":"START_BATTERY", - "acContinueTime":"15", - "cruisingRangeAcOn":"106400.0", - "cruisingRangeAcOff":"107920.0", - "timeStamp":"2016-02-05 12:59:46", - "hvacStatus":"ON" - } - """ - def __init__(self, status): - CarwingsResponse.__init__(self, status) - - self._set_timestamp(status) - self._set_cruising_ranges(status) - - self.operation_result = status["operationResult"] # e.g. "START_BATTERY", ...? - self.ac_continue_time = timedelta(minutes=float(status["acContinueTime"])) - self.hvac_status = status["hvacStatus"] # "ON" or "OFF" - self.is_hvac_running = ("ON" == self.hvac_status) - - -class CarwingsStopClimateControlResponse(CarwingsResponse): - """ - { - "status":200, - "message":"success", - "responseFlag":"1", - "operationResult":"START", - "timeStamp":"2016-02-09 03:32:51", - "hvacStatus":"OFF" - } - """ - def __init__(self, status): - CarwingsResponse.__init__(self, status) - - self._set_timestamp(status) - self.hvac_status = status["hvacStatus"] # "ON" or "OFF" - self.is_hvac_running = ("ON" == self.hvac_status) - - -class CarwingsClimateControlScheduleResponse(CarwingsResponse): - """ - { - "status":200, - "message":"success", - "LastScheduledTime":"Feb 9, 2016 05:39 PM", - "ExecuteTime":"2016-02-10 01:00:00", - "DisplayExecuteTime":"Feb 9, 2016 08:00 PM", - "TargetDate":"2016/02/10 01:00" - } - """ - def __init__(self, status): - CarwingsResponse.__init__(self, status) - - self.display_execute_time = status["DisplayExecuteTime"] # displayable, timezone-adjusted - self.execute_time = datetime.strptime(status["ExecuteTime"] + " UTC", "%Y-%m-%d %H:%M:%S %Z") # GMT - self.display_last_scheduled_time = status["LastScheduledTime"] # displayable, timezone-adjusted - self.last_scheduled_time = datetime.strptime(status["LastScheduledTime"], "%b %d, %Y %I:%M %p") - # unknown purpose; don't surface to avoid confusion - # self.target_date = status["TargetDate"] - - -class CarwingsDrivingAnalysisResponse(CarwingsResponse): - """ - { - "status":200, - "message":"success", - "DriveAnalysisBasicScreenResponsePersonalData": { - "DateSummary":{ - "TargetDate":"2016-02-03", - "ElectricMileage":"4.4", - "ElectricMileageLevel":"3", - "PowerConsumptMoter":"295.2", - "PowerConsumptMoterLevel":"4", - "PowerConsumptMinus":"84.8", - "PowerConsumptMinusLevel":"3", - "PowerConsumptAUX":"17.1", - "PowerConsumptAUXLevel":"5", - "DisplayDate":"Feb 3, 16" - }, - "ElectricCostScale":"miles/kWh" - }, - "AdviceList":{ - "Advice":{ - "title":"World Number of Trips Rankings (last week):", - "body":"The highest number of trips driven was 130 by a driver located in Japan." - } - } - } - """ - def __init__(self, status): - CarwingsResponse.__init__(self, status) - - summary = status["DriveAnalysisBasicScreenResponsePersonalData"]["DateSummary"] - - # avg energy economy, in units of 'electric_cost_scale' (e.g. miles/kWh) - self.electric_mileage = summary["ElectricMileage"] - # rating for above, scale of 1-5 - self.electric_mileage_level = summary["ElectricMileageLevel"] - - # "acceleration performance": "electricity used for motor activation over 1km", Watt-Hours - self.power_consumption_moter = summary["PowerConsumptMoter"] - # rating for above, scale of 1-5 - self.power_consumption_moter_level = summary["PowerConsumptMoterLevel"] - - # Watt-Hours generated by braking - self.power_consumption_minus = summary["PowerConsumptMinus"] - # rating for above, scale of 1-5 - self.power_consumption_minus_level = summary["PowerConsumptMinusLevel"] - - # Electricity used by aux devices, Watt-Hours - self.power_consumption_aux = summary["PowerConsumptAUX"] - # rating for above, scale of 1-5 - self.power_consumption_aux_level = summary["PowerConsumptAUXLevel"] - - self.display_date = summary["DisplayDate"] # "Feb 3, 16" - - self.electric_cost_scale = status["DriveAnalysisBasicScreenResponsePersonalData"]["ElectricCostScale"] - - self.advice = [status["AdviceList"]["Advice"]] # will contain "title" and "body" - - -class CarwingsLatestBatteryStatusResponse(CarwingsResponse): - """ - # not connected to a charger - { - "status":200, - "message":"success", - "BatteryStatusRecords":{ - "OperationResult":"START", - "OperationDateAndTime":"Feb 9, 2016 11:09 PM", - "BatteryStatus":{ - "BatteryChargingStatus":"NOT_CHARGING", - "BatteryCapacity":"12", - "BatteryRemainingAmount":"3", - "BatteryRemainingAmountWH":"", - "BatteryRemainingAmountkWH":"" - }, - "PluginState":"NOT_CONNECTED", - "CruisingRangeAcOn":"39192.0", - "CruisingRangeAcOff":"39744.0", - "TimeRequiredToFull":{ # 120V - "HourRequiredToFull":"18", - "MinutesRequiredToFull":"30" - }, - "TimeRequiredToFull200":{ # 240V, 3kW - "HourRequiredToFull":"6", - "MinutesRequiredToFull":"0" - }, - "TimeRequiredToFull200_6kW":{ # 240V, 6kW - "HourRequiredToFull":"4", - "MinutesRequiredToFull":"0" - }, - "NotificationDateAndTime":"2016/02/10 04:10", - "TargetDate":"2016/02/10 04:09" - } - } - - # not connected to a charger - as at 21/01/2019 20:01 (for a 30kWh leaf) - { - "status":200, - "BatteryStatusRecords": { - "OperationResult":"START", - "OperationDateAndTime":"21-Jan-2019 13:29", - "BatteryStatus":{ - "BatteryChargingStatus":"NOT_CHARGING", - "BatteryCapacity":"240", - "BatteryRemainingAmount":"220", - "BatteryRemainingAmountWH":"24480", - "BatteryRemainingAmountkWH":"", - "SOC":{ - "Value":"91" - } - }, - "PluginState":"NOT_CONNECTED", - "CruisingRangeAcOn":"146000", - "CruisingRangeAcOff":"168000", - "TimeRequiredToFull":{ - "HourRequiredToFull":"4", - "MinutesRequiredToFull":"30" - }, - "TimeRequiredToFull200":{ - "HourRequiredToFull":"3" - ,"MinutesRequiredToFull":"0" - }, - "TimeRequiredToFull200_6kW":{ - "HourRequiredToFull":"1", - "MinutesRequiredToFull":"30" - }, - "NotificationDateAndTime":"2019/01/21 13:29", - "TargetDate":"2019/01/21 13:29" - } - } - - - # connected to a quick charger - { - "status":200, - "message":"success", - "BatteryStatusRecords":{ - "OperationResult":"START", - "OperationDateAndTime":"Feb 14, 2016 03:28 PM", - "BatteryStatus":{ - "BatteryChargingStatus":"RAPIDLY_CHARGING", - "BatteryCapacity":"12", - "BatteryRemainingAmount":"11", - "BatteryRemainingAmountWH":"", - "BatteryRemainingAmountkWH":"" - }, - "PluginState":"QC_CONNECTED", - "CruisingRangeAcOn":"107136.0", - "CruisingRangeAcOff":"115776.0", - "NotificationDateAndTime":"2016/02/14 20:28", - "TargetDate":"2016/02/14 20:28" - } - } - - # connected to a charging station - { - "status": 200, - "message": "success", - "BatteryStatusRecords": { - "OperationResult": "START", - "OperationDateAndTime": "Feb 19, 2016 12:12 PM", - "BatteryStatus": { - "BatteryChargingStatus": "NORMAL_CHARGING", - "BatteryCapacity": "12", - "BatteryRemainingAmount": "12", - "BatteryRemainingAmountWH": "", - "BatteryRemainingAmountkWH": "" - }, - "PluginState": "CONNECTED", - "CruisingRangeAcOn": "132000.0", - "CruisingRangeAcOff": "134000.0", - "TimeRequiredToFull200_6kW": { - "HourRequiredToFull": "0", - "MinutesRequiredToFull": "40" - }, - "NotificationDateAndTime": "2016/02/19 17:12", - "TargetDate": "2016/02/19 17:12" - } - } - """ - def __init__(self, status): - CarwingsResponse.__init__(self, status["BatteryStatusRecords"]) - self.answer = status - - recs = status["BatteryStatusRecords"] - - bs = recs["BatteryStatus"] - self.battery_capacity = bs["BatteryCapacity"] - self.battery_remaining_amount = bs["BatteryRemainingAmount"] - self.charging_status = bs["BatteryChargingStatus"] - self.is_charging = ("NOT_CHARGING" != bs["BatteryChargingStatus"]) # double negatives are fun - self.is_quick_charging = ("RAPIDLY_CHARGING" == bs["BatteryChargingStatus"]) - - self.plugin_state = recs["PluginState"] - self.is_connected = ("NOT_CONNECTED" != recs["PluginState"]) # another double negative - self.is_connected_to_quick_charger = ("QC_CONNECTED" == recs["PluginState"]) - - self._set_cruising_ranges(recs, off_key="CruisingRangeAcOff", on_key="CruisingRangeAcOn") - - if "TimeRequiredToFull" in recs: - self.time_to_full_trickle = timedelta(minutes=_time_remaining(recs["TimeRequiredToFull"])) - else: - self.time_to_full_trickle = None - - if "TimeRequiredToFull200" in recs: - self.time_to_full_l2 = timedelta(minutes=_time_remaining(recs["TimeRequiredToFull200"])) - else: - self.time_to_full_l2 = None - - if "TimeRequiredToFull200_6kW" in recs: - self.time_to_full_l2_6kw = timedelta(minutes=_time_remaining(recs["TimeRequiredToFull200_6kW"])) - else: - self.time_to_full_l2_6kw = None - - if float(self.battery_capacity) == 0: - log.debug("battery_capacity=0, status=%s", status) - self.battery_percent = 0 - else: - self.battery_percent = 100 * float(self.battery_remaining_amount) / 12 - - # Leaf 2016 has SOC (State Of Charge) in BatteryStatus, a more accurate battery_percentage - if "SOC" in bs: - self.state_of_charge = bs["SOC"]["Value"] - # Update battery_percent with more accurate version - self.battery_percent = float(self.state_of_charge) - else: - self.state_of_charge = None - - -class CarwingsElectricRateSimulationResponse(CarwingsResponse): - def __init__(self, status): - CarwingsResponse.__init__(self, status) - - r = status["PriceSimulatorDetailInfoResponsePersonalData"] - t = r["PriceSimulatorTotalInfo"] - - self.month = r["DisplayMonth"] # e.g. "Feb/2016" - - self.total_number_of_trips = t["TotalNumberOfTrips"] - self.total_power_consumption = t["TotalPowerConsumptTotal"] # in kWh - self.total_acceleration_power_consumption = t["TotalPowerConsumptMoter"] # in kWh - self.total_power_regenerated_in_braking = t["TotalPowerConsumptMinus"] # in kWh - self.total_travel_distance_km = float(t["TotalTravelDistance"]) / 1000 # assumed to be in meters - - self.total_electric_mileage = t["TotalElectricMileage"] - self.total_co2_reduction = t["TotalCO2Reductiont"] # (yep, extra 't' at the end) - - self.electricity_rate = r["ElectricPrice"] - self.electric_bill = r["ElectricBill"] - self.electric_cost_scale = r["ElectricCostScale"] # e.g. "miles/kWh" - - -class CarwingsMyCarFinderResponse(CarwingsResponse): - """ - { - "Location": { - "Country": "", - "Home": "OUTSIDE", - "LatitudeDeg": "69", - "LatitudeMin": "41", - "LatitudeMode": "NORTH", - "LatitudeSec": "5540", - "LocationType": "WGS84", - "LongitudeDeg": "18", - "LongitudeMin": "38", - "LongitudeMode": "EAST", - "LongitudeSec": "2506", - "Position": "UNAVAILABLE" - }, - "TargetDate": "2017/11/29 20:02", - "lat": "69.698722222222", - "lng": "18.640294444444", - "receivedDate": "2017/11/29 20:02", - "responseFlag": "1", - "resultCode": "1", - "status": 200, - "timeStamp": "2017-11-29 20:02:45" - } - """ - def __init__(self, status): - CarwingsResponse.__init__(self, status) - - self.latitude = status["lat"] - self.longitude = status["lng"] From fbef254da1619fa690e07b75d94b83bc5e596669 Mon Sep 17 00:00:00 2001 From: mekrapp <158028484+mekrapp@users.noreply.github.com> Date: Fri, 7 Mar 2025 20:22:58 +0100 Subject: [PATCH 28/55] Delete packages/modules/vehicles/leaf/soc.py soc.py using pycarwings2 removed --- packages/modules/vehicles/leaf/soc.py | 81 --------------------------- 1 file changed, 81 deletions(-) delete mode 100644 packages/modules/vehicles/leaf/soc.py diff --git a/packages/modules/vehicles/leaf/soc.py b/packages/modules/vehicles/leaf/soc.py deleted file mode 100644 index dc83465b19..0000000000 --- a/packages/modules/vehicles/leaf/soc.py +++ /dev/null @@ -1,81 +0,0 @@ -#!/usr/bin/env python3 -from typing import List - -import logging -import time - -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_vehicle import VehicleUpdateData -from modules.common.component_state import CarState -from modules.common.configurable_vehicle import ConfigurableVehicle -from modules.vehicles.leaf.config import LeafSoc, LeafConfiguration - -import pycarwings2 - -log = logging.getLogger(__name__) - - -def fetch_soc(username, password, chargepoint) -> CarState: - - region = "NE" - - def getNissanSession(): # open Https session with Nissan server - log.debug("LP%s: login = %s, region = %s" % (chargepoint, username, region)) - s = pycarwings2.Session(username, password, region) - leaf = s.get_leaf() - time.sleep(1) # give Nissan server some time - return leaf - - def readSoc(leaf): # get SoC from Nissan server - leaf_info = leaf.get_latest_battery_status() - bat_percent = int(leaf_info.battery_percent) - log.debug("LP%s: Battery status %s" % (chargepoint, bat_percent)) - return bat_percent - - def requestSoc(leaf): # request Nissan server to request last SoC from car - log.debug("LP%s: Request SoC Update" % (chargepoint)) - key = leaf.request_update() - status = leaf.get_status_from_update(key) - sleepsecs = 20 - for i in range(0, 9): - log.debug("Waiting {0} seconds".format(sleepsecs)) - time.sleep(sleepsecs) - status = leaf.get_status_from_update(key) - if status is not None: - break - log.debug("LP%s: Finished updating" % (chargepoint)) - - leaf = getNissanSession() # start Https session with Nissan Server - readSoc(leaf) # old SoC needs to be read from server before requesting new SoC from car - time.sleep(1) # give Nissan server some time - requestSoc(leaf) # Nissan server to request new SoC from car - time.sleep(1) # give Nissan server some time - soc = readSoc(leaf) # final read of SoC from server - return CarState(soc) - - -def create_vehicle(vehicle_config: LeafSoc, vehicle: int): - def updater(vehicle_update_data: VehicleUpdateData) -> CarState: - return fetch_soc( - vehicle_config.configuration.user_id, - vehicle_config.configuration.password, - vehicle) - return ConfigurableVehicle(vehicle_config=vehicle_config, component_updater=updater, vehicle=vehicle) - - -def leaf_update(user_id: str, password: str, charge_point: int): - log.debug("Leaf: user_id="+user_id+"charge_point="+str(charge_point)) - vehicle_config = LeafSoc(configuration=LeafConfiguration(charge_point, user_id, password)) - store.get_car_value_store(charge_point).store.set(fetch_soc( - vehicle_config.configuration.user_id, - vehicle_config.configuration.password, - charge_point)) - - -def main(argv: List[str]): - run_using_positional_cli_args(leaf_update, argv) - - -device_descriptor = DeviceDescriptor(configuration_factory=LeafSoc) From 10bc2b29d8a2b5a5970c4c9e8f0d93a73e8a1baf Mon Sep 17 00:00:00 2001 From: mekrapp <158028484+mekrapp@users.noreply.github.com> Date: Fri, 7 Mar 2025 20:44:09 +0100 Subject: [PATCH 29/55] 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 --- packages/modules/vehicles/leaf/api.py | 66 ++++++++++++++++++ .../modules/vehicles/leaf/api_wo_CarState.py | 67 +++++++++++++++++++ .../modules/vehicles/leaf/test_fetch_soc.py | 15 +++++ 3 files changed, 148 insertions(+) create mode 100644 packages/modules/vehicles/leaf/api.py create mode 100644 packages/modules/vehicles/leaf/api_wo_CarState.py create mode 100644 packages/modules/vehicles/leaf/test_fetch_soc.py diff --git a/packages/modules/vehicles/leaf/api.py b/packages/modules/vehicles/leaf/api.py new file mode 100644 index 0000000000..67487e4ee4 --- /dev/null +++ b/packages/modules/vehicles/leaf/api.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python3 +import asyncio +import logging + +from modules.vehicles.leaf.config import LeafSoc, LeafConfiguration + +import pycarwings3 + +log = logging.getLogger(__name__) + + +async def _fetch_soc(username, password, chargepoint): + + region = "NE" + + async def getNissanSession(): # open Https session with Nissan server + log.debug("LP%s: login = %s, region = %s" % (chargepoint, username, region)) + session = pycarwings3.Session(username, password, region) + leaf = await session.get_leaf() + await asyncio.sleep(1) # give Nissan server some time + return leaf + + async def readSoc(leaf): # get SoC from Nissan server + leaf_info = await leaf.get_latest_battery_status() + bat_percent = int(leaf_info.battery_percent) + log.debug("LP%s: Battery status %s" % (chargepoint, bat_percent)) + return bat_percent + + async def requestSoc(leaf: pycarwings3.Leaf): # request Nissan server to request last SoC from car + log.debug("LP%s: Request SoC Update from vehicle" % (chargepoint)) + key = await leaf.request_update() + sleepsecs = 20 + for _ in range(0, 3): + log.debug("Waiting {0} seconds".format(sleepsecs)) + await asyncio.sleep(sleepsecs) + status = await leaf.get_status_from_update(key) + if status is not None: + log.debug("LP%s: Update successful" % (chargepoint)) + return status + log.debug("LP%s: Update not successful" % (chargepoint)) + return status + + try: + leaf = await getNissanSession() # start HTTPS session with Nissan server + soc = await readSoc(leaf) # old SoC needs to be read from server before requesting new SoC from vehicle + await asyncio.sleep(1) # give Nissan server some time + status = await requestSoc(leaf) # Nissan server to request new SoC from vehicle + if status is not None: # was update of SoC successful? + await asyncio.sleep(1) # give Nissan server some time + soc = await readSoc(leaf) # final read of SoC from server + except pycarwings3.CarwingsError as e: + log.info("LP%s: SoC request not successful" % (chargepoint)) + log.info(e) + soc = 0 + return soc + +# main entry - _fetch_soc needs to be run async +def fetch_soc(user_id: str, password: str, vnum: int) -> CarState: + + loop = asyncio.new_event_loop() # prepare and call async method + asyncio.set_event_loop(loop) + + # get soc from vehicle via server + soc = loop.run_until_complete(_fetch_soc(user_id, password, vnum)) + + return CarState(soc) diff --git a/packages/modules/vehicles/leaf/api_wo_CarState.py b/packages/modules/vehicles/leaf/api_wo_CarState.py new file mode 100644 index 0000000000..7426191fe3 --- /dev/null +++ b/packages/modules/vehicles/leaf/api_wo_CarState.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python3 +import asyncio +import logging + +#from modules.vehicles.leaf.config import LeafSoc, LeafConfiguration + +import pycarwings3 + +log = logging.getLogger(__name__) + + +async def _fetch_soc(username, password, chargepoint): + + region = "NE" + + async def getNissanSession(): # open Https session with Nissan server + log.debug("LP%s: login = %s, region = %s" % (chargepoint, username, region)) + session = pycarwings3.Session(username, password, region) + leaf = await session.get_leaf() + await asyncio.sleep(1) # give Nissan server some time + return leaf + + async def readSoc(leaf): # get SoC from Nissan server + leaf_info = await leaf.get_latest_battery_status() + bat_percent = int(leaf_info.battery_percent) + log.debug("LP%s: Battery status %s" % (chargepoint, bat_percent)) + return bat_percent + + async def requestSoc(leaf: pycarwings3.Leaf): # request Nissan server to request last SoC from car + log.debug("LP%s: Request SoC Update from vehicle" % (chargepoint)) + key = await leaf.request_update() + sleepsecs = 20 + for _ in range(0, 3): + log.debug("Waiting {0} seconds".format(sleepsecs)) + await asyncio.sleep(sleepsecs) + status = await leaf.get_status_from_update(key) + if status is not None: + log.debug("LP%s: Update successful" % (chargepoint)) + return status + log.debug("LP%s: Update not successful" % (chargepoint)) + return status + + try: + leaf = await getNissanSession() # start HTTPS session with Nissan server + soc = await readSoc(leaf) # old SoC needs to be read from server before requesting new SoC from vehicle + await asyncio.sleep(1) # give Nissan server some time + status = await requestSoc(leaf) # Nissan server to request new SoC from vehicle + if status is not None: # was update of SoC successful? + await asyncio.sleep(1) # give Nissan server some time + soc = await readSoc(leaf) # final read of SoC from server + except pycarwings3.CarwingsError as e: + log.info("LP%s: SoC request not successful" % (chargepoint)) + log.info(e) + soc = 0 + return soc + +# main entry - _fetch_soc needs to be run async +def fetch_soc(user_id: str, password: str, vnum: int): #-> CarState: + + loop = asyncio.new_event_loop() # prepare and call async method + asyncio.set_event_loop(loop) + + # get soc from vehicle via server + soc = loop.run_until_complete(_fetch_soc(user_id, password, vnum)) + + return soc +# return CarState(soc) \ No newline at end of file diff --git a/packages/modules/vehicles/leaf/test_fetch_soc.py b/packages/modules/vehicles/leaf/test_fetch_soc.py new file mode 100644 index 0000000000..113ae42ff6 --- /dev/null +++ b/packages/modules/vehicles/leaf/test_fetch_soc.py @@ -0,0 +1,15 @@ +#!/usr/bin/env python3 + +import sys +import logging + +from api_wo_CarState import fetch_soc + +logging.basicConfig(stream=sys.stdout, level=5, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') # + +username = 'xyz@ab.de' #replace by your user ID +password = 'secretPassword' #replace by your password +chargepoint = 1 + +print("SoC: " + str(fetch_soc(username, password, chargepoint))) + From b59ce8463c836f1e823fd8326d36add8493714eb Mon Sep 17 00:00:00 2001 From: mekrapp <158028484+mekrapp@users.noreply.github.com> Date: Sat, 8 Mar 2025 10:30:07 +0100 Subject: [PATCH 30/55] Add files via upload --- packages/modules/vehicles/bmwbc/soc.py | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/packages/modules/vehicles/bmwbc/soc.py b/packages/modules/vehicles/bmwbc/soc.py index 4cd9dafd3c..be51a20f5d 100755 --- a/packages/modules/vehicles/bmwbc/soc.py +++ b/packages/modules/vehicles/bmwbc/soc.py @@ -8,38 +8,33 @@ from modules.common.abstract_vehicle import VehicleUpdateData from modules.common.component_state import CarState from modules.common.configurable_vehicle import ConfigurableVehicle -from modules.vehicles.bmwbc import api -from modules.vehicles.bmwbc.config import BMWbc, BMWbcConfiguration +from modules.vehicles.leaf import api +from modules.vehicles.leaf.config import LeafSoc, LeafConfiguration log = logging.getLogger(__name__) -def create_vehicle(vehicle_config: BMWbc, vehicle: int): +def create_vehicle(vehicle_config: LeafSoc, vehicle: int): def updater(vehicle_update_data: VehicleUpdateData) -> CarState: return api.fetch_soc( vehicle_config.configuration.user_id, vehicle_config.configuration.password, - vehicle_config.configuration.vin, vehicle) - return ConfigurableVehicle(vehicle_config=vehicle_config, - component_updater=updater, - vehicle=vehicle, - calc_while_charging=vehicle_config.configuration.calculate_soc) + return ConfigurableVehicle(vehicle_config=vehicle_config, component_updater=updater, vehicle=vehicle) -def bmwbc_update(user_id: str, password: str, vin: str, charge_point: int): - log.debug("bmwbc: user_id="+user_id+"vin="+vin+"charge_point="+str(charge_point)) - vehicle_config = BMWbc(configuration=BMWbcConfiguration(charge_point, user_id, password, vin)) +def leaf_update(user_id: str, password: str, charge_point: int): + log.debug("Leaf: user_id="+user_id+"charge_point="+str(charge_point)) + vehicle_config = LeafSoc(configuration=LeafConfiguration(charge_point, user_id, password)) store.get_car_value_store(charge_point).store.set(api.fetch_soc( vehicle_config.configuration.user_id, vehicle_config.configuration.password, - vehicle_config.configuration.vin, charge_point)) def main(argv: List[str]): - run_using_positional_cli_args(bmwbc_update, argv) + run_using_positional_cli_args(leaf_update, argv) -device_descriptor = DeviceDescriptor(configuration_factory=BMWbc) +device_descriptor = DeviceDescriptor(configuration_factory=LeafSoc) From d380e3d9dfb55a9a4859fd3049951524654a35b1 Mon Sep 17 00:00:00 2001 From: mekrapp <158028484+mekrapp@users.noreply.github.com> Date: Sat, 8 Mar 2025 11:02:42 +0100 Subject: [PATCH 31/55] Add files via upload --- packages/modules/vehicles/leaf/soc.py | 40 +++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 packages/modules/vehicles/leaf/soc.py diff --git a/packages/modules/vehicles/leaf/soc.py b/packages/modules/vehicles/leaf/soc.py new file mode 100644 index 0000000000..be51a20f5d --- /dev/null +++ b/packages/modules/vehicles/leaf/soc.py @@ -0,0 +1,40 @@ +from typing import List + +import logging + +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_vehicle import VehicleUpdateData +from modules.common.component_state import CarState +from modules.common.configurable_vehicle import ConfigurableVehicle +from modules.vehicles.leaf import api +from modules.vehicles.leaf.config import LeafSoc, LeafConfiguration + + +log = logging.getLogger(__name__) + + +def create_vehicle(vehicle_config: LeafSoc, vehicle: int): + def updater(vehicle_update_data: VehicleUpdateData) -> CarState: + return api.fetch_soc( + vehicle_config.configuration.user_id, + vehicle_config.configuration.password, + vehicle) + return ConfigurableVehicle(vehicle_config=vehicle_config, component_updater=updater, vehicle=vehicle) + + +def leaf_update(user_id: str, password: str, charge_point: int): + log.debug("Leaf: user_id="+user_id+"charge_point="+str(charge_point)) + vehicle_config = LeafSoc(configuration=LeafConfiguration(charge_point, user_id, password)) + store.get_car_value_store(charge_point).store.set(api.fetch_soc( + vehicle_config.configuration.user_id, + vehicle_config.configuration.password, + charge_point)) + + +def main(argv: List[str]): + run_using_positional_cli_args(leaf_update, argv) + + +device_descriptor = DeviceDescriptor(configuration_factory=LeafSoc) From e3aba8ff65045a11d6b15c57bc832c5b984846b2 Mon Sep 17 00:00:00 2001 From: mekrapp <158028484+mekrapp@users.noreply.github.com> Date: Sat, 8 Mar 2025 11:07:00 +0100 Subject: [PATCH 32/55] Add files via upload --- packages/modules/vehicles/bmwbc/soc.py | 34 +++++++++++++++++--------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/packages/modules/vehicles/bmwbc/soc.py b/packages/modules/vehicles/bmwbc/soc.py index be51a20f5d..796582f471 100755 --- a/packages/modules/vehicles/bmwbc/soc.py +++ b/packages/modules/vehicles/bmwbc/soc.py @@ -8,33 +8,45 @@ from modules.common.abstract_vehicle import VehicleUpdateData from modules.common.component_state import CarState from modules.common.configurable_vehicle import ConfigurableVehicle -from modules.vehicles.leaf import api -from modules.vehicles.leaf.config import LeafSoc, LeafConfiguration +from modules.vehicles.bmwbc import api +from modules.vehicles.bmwbc.config import BMWbc, BMWbcConfiguration log = logging.getLogger(__name__) -def create_vehicle(vehicle_config: LeafSoc, vehicle: int): +def create_vehicle(vehicle_config: BMWbc, vehicle: int): def updater(vehicle_update_data: VehicleUpdateData) -> CarState: return api.fetch_soc( vehicle_config.configuration.user_id, vehicle_config.configuration.password, + vehicle_config.configuration.vin, + vehicle_config.configuration.captcha_token, vehicle) - return ConfigurableVehicle(vehicle_config=vehicle_config, component_updater=updater, vehicle=vehicle) - - -def leaf_update(user_id: str, password: str, charge_point: int): - log.debug("Leaf: user_id="+user_id+"charge_point="+str(charge_point)) - vehicle_config = LeafSoc(configuration=LeafConfiguration(charge_point, user_id, password)) + return ConfigurableVehicle(vehicle_config=vehicle_config, + component_updater=updater, + vehicle=vehicle, + calc_while_charging=vehicle_config.configuration.calculate_soc) + + +def bmwbc_update(user_id: str, password: str, vin: str, captcha_token: str, charge_point: int): + log.debug("bmwbc: user_id="+user_id+"vin="+vin+"charge_point="+str(charge_point)) + log.debug("bmwbc: captcha_token="+captcha_token) + vehicle_config = BMWbc(configuration=BMWbcConfiguration(charge_point, + user_id, + password, + vin, + captcha_token)) store.get_car_value_store(charge_point).store.set(api.fetch_soc( vehicle_config.configuration.user_id, vehicle_config.configuration.password, + vehicle_config.configuration.vin, + vehicle_config.configuration.captcha_token, charge_point)) def main(argv: List[str]): - run_using_positional_cli_args(leaf_update, argv) + run_using_positional_cli_args(bmwbc_update, argv) -device_descriptor = DeviceDescriptor(configuration_factory=LeafSoc) +device_descriptor = DeviceDescriptor(configuration_factory=BMWbc) From 44aaa367fbcde7543b6467c42650ca97af6a7162 Mon Sep 17 00:00:00 2001 From: mekrapp <158028484+mekrapp@users.noreply.github.com> Date: Sat, 8 Mar 2025 12:01:37 +0100 Subject: [PATCH 33/55] Add files via upload replace requirements.txt with a version that includes pycarwings3. --- requirements.txt | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/requirements.txt b/requirements.txt index fce126f59f..687e6239ee 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,22 +5,22 @@ pymodbus==2.5.2 pytest==6.2.5 requests_mock==1.9.3 lxml==4.9.1 -aiohttp==3.9.4 +aiohttp==3.10.11 schedule==1.1.0 PyJWT==2.6.0 -ipparser==0.3.8 bs4==0.0.1 pkce==1.0.3 # skodaconnect==1.3.4 evdev==1.5.0 -#telnetlib3==2.0.2 -cryptography==42.0.4 -msal==1.27.0 +cryptography==44.0.1 +msal==1.31.1 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 -bimmer_connected==0.15.1 -pycarwings2==2.14 +bimmer_connected==0.17.2 +ocpp==1.0.0 +websockets==12.0 +pycarwings3==0.7.13 From b8fb70ae5dc6ab68c259ec6c3c40afbcf6155bd6 Mon Sep 17 00:00:00 2001 From: mekrapp <158028484+mekrapp@users.noreply.github.com> Date: Tue, 11 Mar 2025 22:58:45 +0100 Subject: [PATCH 34/55] Update api.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Definition/Import für Klasse CarState ergänzt. --- packages/modules/vehicles/leaf/api.py | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/modules/vehicles/leaf/api.py b/packages/modules/vehicles/leaf/api.py index 67487e4ee4..ad00fb1f26 100644 --- a/packages/modules/vehicles/leaf/api.py +++ b/packages/modules/vehicles/leaf/api.py @@ -3,6 +3,7 @@ import logging from modules.vehicles.leaf.config import LeafSoc, LeafConfiguration +from modules.common.component_state import CarState import pycarwings3 From 625779d25ba75d752d4ed13dc5d1fff897cf8d17 Mon Sep 17 00:00:00 2001 From: mekrapp <158028484+mekrapp@users.noreply.github.com> Date: Tue, 11 Mar 2025 23:04:58 +0100 Subject: [PATCH 35/55] Update api.py Name of parameter "vnum" within fetch_soc() changed to "charge_point". --- packages/modules/vehicles/leaf/api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/modules/vehicles/leaf/api.py b/packages/modules/vehicles/leaf/api.py index ad00fb1f26..da0e6fd223 100644 --- a/packages/modules/vehicles/leaf/api.py +++ b/packages/modules/vehicles/leaf/api.py @@ -56,12 +56,12 @@ async def requestSoc(leaf: pycarwings3.Leaf): # request Nissan server to r return soc # main entry - _fetch_soc needs to be run async -def fetch_soc(user_id: str, password: str, vnum: int) -> CarState: +def fetch_soc(user_id: str, password: str, charge_point: int) -> CarState: loop = asyncio.new_event_loop() # prepare and call async method asyncio.set_event_loop(loop) # get soc from vehicle via server - soc = loop.run_until_complete(_fetch_soc(user_id, password, vnum)) + soc = loop.run_until_complete(_fetch_soc(user_id, password, charge_point)) return CarState(soc) From 295c64a2500e3f81cc70fcc010e1a1344c05e6f7 Mon Sep 17 00:00:00 2001 From: mekrapp <158028484+mekrapp@users.noreply.github.com> Date: Tue, 11 Mar 2025 23:05:49 +0100 Subject: [PATCH 36/55] Update api.py chargepoint --- packages/modules/vehicles/leaf/api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/modules/vehicles/leaf/api.py b/packages/modules/vehicles/leaf/api.py index da0e6fd223..7b6190db1b 100644 --- a/packages/modules/vehicles/leaf/api.py +++ b/packages/modules/vehicles/leaf/api.py @@ -56,12 +56,12 @@ async def requestSoc(leaf: pycarwings3.Leaf): # request Nissan server to r return soc # main entry - _fetch_soc needs to be run async -def fetch_soc(user_id: str, password: str, charge_point: int) -> CarState: +def fetch_soc(user_id: str, password: str, chargepoint: int) -> CarState: loop = asyncio.new_event_loop() # prepare and call async method asyncio.set_event_loop(loop) # get soc from vehicle via server - soc = loop.run_until_complete(_fetch_soc(user_id, password, charge_point)) + soc = loop.run_until_complete(_fetch_soc(user_id, password, chargepoint)) return CarState(soc) From 02f627ec8b5408efc03cea19053450efd0d71372 Mon Sep 17 00:00:00 2001 From: mekrapp <158028484+mekrapp@users.noreply.github.com> Date: Fri, 14 Mar 2025 21:11:25 +0100 Subject: [PATCH 37/55] Update config.py variable "region" added. variable "name" changed to "Nissan Leaf/NV200 -05.2019 (experimental)". --- packages/modules/vehicles/leaf/config.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/modules/vehicles/leaf/config.py b/packages/modules/vehicles/leaf/config.py index 7af45d37e1..68370ac76b 100644 --- a/packages/modules/vehicles/leaf/config.py +++ b/packages/modules/vehicles/leaf/config.py @@ -5,11 +5,12 @@ class LeafConfiguration: def __init__(self, user_id: Optional[str] = None, password: Optional[str] = None): self.user_id = user_id self.password = password + self.region = region class LeafSoc: def __init__(self, - name: str = "Leaf", + name: str = "Nissan Leaf/NV200 -05.2019 (experimental)", type: str = "leaf", configuration: LeafConfiguration = None) -> None: self.name = name From 9d26c257511b3c1e54e80608da22e5d65ee5fd4e Mon Sep 17 00:00:00 2001 From: mekrapp <158028484+mekrapp@users.noreply.github.com> Date: Fri, 14 Mar 2025 22:25:21 +0100 Subject: [PATCH 38/55] Update soc.py variable "region" added parameter "calc_while_charging" added and preset to False --- packages/modules/vehicles/leaf/soc.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/modules/vehicles/leaf/soc.py b/packages/modules/vehicles/leaf/soc.py index be51a20f5d..709a7fd580 100644 --- a/packages/modules/vehicles/leaf/soc.py +++ b/packages/modules/vehicles/leaf/soc.py @@ -20,16 +20,21 @@ def updater(vehicle_update_data: VehicleUpdateData) -> CarState: return api.fetch_soc( vehicle_config.configuration.user_id, vehicle_config.configuration.password, + vehicle_config.configuration.region, vehicle) - return ConfigurableVehicle(vehicle_config=vehicle_config, component_updater=updater, vehicle=vehicle) + return ConfigurableVehicle(vehicle_config=vehicle_config, + component_updater=updater, + vehicle=vehicle, + calc_while_charging=False) -def leaf_update(user_id: str, password: str, charge_point: int): - log.debug("Leaf: user_id="+user_id+"charge_point="+str(charge_point)) - vehicle_config = LeafSoc(configuration=LeafConfiguration(charge_point, user_id, password)) +def leaf_update(user_id: str, password: str, region: str, charge_point: int): + log.debug("Leaf: user_id="+user_id+" region="+region+" charge_point="+str(charge_point)) + vehicle_config = LeafSoc(configuration=LeafConfiguration(charge_point, user_id, password, region)) store.get_car_value_store(charge_point).store.set(api.fetch_soc( vehicle_config.configuration.user_id, vehicle_config.configuration.password, + vehicle_config.configuration.region, charge_point)) From 5582f2ce25d4aee37ab359b89cb8706fb56d5de6 Mon Sep 17 00:00:00 2001 From: mekrapp <158028484+mekrapp@users.noreply.github.com> Date: Fri, 14 Mar 2025 22:46:35 +0100 Subject: [PATCH 39/55] Update api.py variable "region" added --- packages/modules/vehicles/leaf/api.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/modules/vehicles/leaf/api.py b/packages/modules/vehicles/leaf/api.py index 7b6190db1b..c811c27cc6 100644 --- a/packages/modules/vehicles/leaf/api.py +++ b/packages/modules/vehicles/leaf/api.py @@ -10,9 +10,7 @@ log = logging.getLogger(__name__) -async def _fetch_soc(username, password, chargepoint): - - region = "NE" +async def _fetch_soc(username, password, region = "NE", chargepoint): async def getNissanSession(): # open Https session with Nissan server log.debug("LP%s: login = %s, region = %s" % (chargepoint, username, region)) @@ -56,12 +54,12 @@ async def requestSoc(leaf: pycarwings3.Leaf): # request Nissan server to r return soc # main entry - _fetch_soc needs to be run async -def fetch_soc(user_id: str, password: str, chargepoint: int) -> CarState: +def fetch_soc(user_id: str, password: str, region: str, chargepoint: int) -> CarState: loop = asyncio.new_event_loop() # prepare and call async method asyncio.set_event_loop(loop) # get soc from vehicle via server - soc = loop.run_until_complete(_fetch_soc(user_id, password, chargepoint)) + soc = loop.run_until_complete(_fetch_soc(user_id, password, region, chargepoint)) return CarState(soc) From 455336593ea1bcae40b0b6ff7b3db591c0a03029 Mon Sep 17 00:00:00 2001 From: mekrapp <158028484+mekrapp@users.noreply.github.com> Date: Sun, 16 Mar 2025 09:49:11 +0100 Subject: [PATCH 40/55] Update config.py Variable "region" added in line 5. --- packages/modules/vehicles/leaf/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/modules/vehicles/leaf/config.py b/packages/modules/vehicles/leaf/config.py index 68370ac76b..93fa2159c7 100644 --- a/packages/modules/vehicles/leaf/config.py +++ b/packages/modules/vehicles/leaf/config.py @@ -2,7 +2,7 @@ class LeafConfiguration: - def __init__(self, user_id: Optional[str] = None, password: Optional[str] = None): + def __init__(self, user_id: Optional[str] = None, password: Optional[str] = None, region: Optional[str] = None): self.user_id = user_id self.password = password self.region = region From cd7afcb91d0b9145af37a8d14b89af0d96ef05f0 Mon Sep 17 00:00:00 2001 From: mekrapp <158028484+mekrapp@users.noreply.github.com> Date: Sun, 16 Mar 2025 10:13:40 +0100 Subject: [PATCH 41/55] Update api.py --- packages/modules/vehicles/leaf/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/modules/vehicles/leaf/api.py b/packages/modules/vehicles/leaf/api.py index c811c27cc6..a46067389d 100644 --- a/packages/modules/vehicles/leaf/api.py +++ b/packages/modules/vehicles/leaf/api.py @@ -10,7 +10,7 @@ log = logging.getLogger(__name__) -async def _fetch_soc(username, password, region = "NE", chargepoint): +async def _fetch_soc(username, password, region, chargepoint): async def getNissanSession(): # open Https session with Nissan server log.debug("LP%s: login = %s, region = %s" % (chargepoint, username, region)) From 8d95c005527201b65568cca28d17b60a9c98cbbb Mon Sep 17 00:00:00 2001 From: mekrapp <158028484+mekrapp@users.noreply.github.com> Date: Sun, 16 Mar 2025 12:52:00 +0100 Subject: [PATCH 42/55] Update api.py Parameter range added --- packages/modules/vehicles/leaf/api.py | 46 ++++++++++++++------------- 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/packages/modules/vehicles/leaf/api.py b/packages/modules/vehicles/leaf/api.py index a46067389d..af18b37012 100644 --- a/packages/modules/vehicles/leaf/api.py +++ b/packages/modules/vehicles/leaf/api.py @@ -2,7 +2,7 @@ import asyncio import logging -from modules.vehicles.leaf.config import LeafSoc, LeafConfiguration +from config import LeafSoc, LeafConfiguration from modules.common.component_state import CarState import pycarwings3 @@ -10,22 +10,24 @@ log = logging.getLogger(__name__) -async def _fetch_soc(username, password, region, chargepoint): +async def _fetch_soc(username, password, region, chargepoint) -> CarState: - async def getNissanSession(): # open Https session with Nissan server + async def getNissanSession(): # open HTTPS session with Nissan server log.debug("LP%s: login = %s, region = %s" % (chargepoint, username, region)) session = pycarwings3.Session(username, password, region) leaf = await session.get_leaf() - await asyncio.sleep(1) # give Nissan server some time + await asyncio.sleep(1) # give Nissan server some time return leaf - async def readSoc(leaf): # get SoC from Nissan server + async def readSoc(leaf) -> CarState: # get SoC & range from Nissan server leaf_info = await leaf.get_latest_battery_status() - bat_percent = int(leaf_info.battery_percent) - log.debug("LP%s: Battery status %s" % (chargepoint, bat_percent)) - return bat_percent + soc = float(leaf_info.battery_percent) + log.debug("LP%s: Battery State of Charge %s" % (chargepoint, soc)) + range = int(leaf_info.answer["BatteryStatusRecords"]["CruisingRangeAcOff"])/1000 + log.debug("LP%s: Cruising range AC Off %s" % (chargepoint, range)) + return CarState(soc, range) - async def requestSoc(leaf: pycarwings3.Leaf): # request Nissan server to request last SoC from car + async def requestSoc(leaf: pycarwings3.Leaf): # request Nissan server to request last SoC from car log.debug("LP%s: Request SoC Update from vehicle" % (chargepoint)) key = await leaf.request_update() sleepsecs = 20 @@ -40,18 +42,18 @@ async def requestSoc(leaf: pycarwings3.Leaf): # request Nissan server to r return status try: - leaf = await getNissanSession() # start HTTPS session with Nissan server - soc = await readSoc(leaf) # old SoC needs to be read from server before requesting new SoC from vehicle - await asyncio.sleep(1) # give Nissan server some time - status = await requestSoc(leaf) # Nissan server to request new SoC from vehicle - if status is not None: # was update of SoC successful? - await asyncio.sleep(1) # give Nissan server some time - soc = await readSoc(leaf) # final read of SoC from server + leaf = await getNissanSession() # start HTTPS session with Nissan server + soc_range = await readSoc(leaf) # old SoC & range need to be read from server before requesting new values from vehicle + await asyncio.sleep(1) # give Nissan server some time + status = await requestSoc(leaf) # Nissan server to request new values from vehicle + if status is not None: # was update of values successful? + await asyncio.sleep(1) # give Nissan server some time + soc_range = await readSoc(leaf) # final read of SoC & range from server except pycarwings3.CarwingsError as e: - log.info("LP%s: SoC request not successful" % (chargepoint)) + log.info("LP%s: SoC & range request not successful" % (chargepoint)) log.info(e) - soc = 0 - return soc + soc_range = CarState(0.0, 0.0) + return soc_range # main entry - _fetch_soc needs to be run async def fetch_soc(user_id: str, password: str, region: str, chargepoint: int) -> CarState: @@ -59,7 +61,7 @@ def fetch_soc(user_id: str, password: str, region: str, chargepoint: int) -> Car loop = asyncio.new_event_loop() # prepare and call async method asyncio.set_event_loop(loop) - # get soc from vehicle via server - soc = loop.run_until_complete(_fetch_soc(user_id, password, region, chargepoint)) + # get SoC and range from vehicle via server + soc_range = loop.run_until_complete(_fetch_soc(user_id, password, region, chargepoint)) - return CarState(soc) + return soc_range From c49fbadc1c828af4b57a928a7f0b004641d34858 Mon Sep 17 00:00:00 2001 From: mekrapp <158028484+mekrapp@users.noreply.github.com> Date: Sun, 16 Mar 2025 16:08:32 +0100 Subject: [PATCH 43/55] Update api.py Import-Pfad zur config.py korrigiert --- packages/modules/vehicles/leaf/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/modules/vehicles/leaf/api.py b/packages/modules/vehicles/leaf/api.py index af18b37012..26052ae506 100644 --- a/packages/modules/vehicles/leaf/api.py +++ b/packages/modules/vehicles/leaf/api.py @@ -2,7 +2,7 @@ import asyncio import logging -from config import LeafSoc, LeafConfiguration +from modules.vehicles.leaf.config import LeafSoc, LeafConfiguration from modules.common.component_state import CarState import pycarwings3 From 973b6bfecbd1ba7383169b472aa3aa4801edfdab Mon Sep 17 00:00:00 2001 From: mekrapp <158028484+mekrapp@users.noreply.github.com> Date: Mon, 17 Mar 2025 18:38:30 +0100 Subject: [PATCH 44/55] Update api.py fetching time stamp added --- packages/modules/vehicles/leaf/api.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/packages/modules/vehicles/leaf/api.py b/packages/modules/vehicles/leaf/api.py index 26052ae506..e2a4b1de19 100644 --- a/packages/modules/vehicles/leaf/api.py +++ b/packages/modules/vehicles/leaf/api.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 import asyncio import logging +from datetime import datetime, timezone from modules.vehicles.leaf.config import LeafSoc, LeafConfiguration from modules.common.component_state import CarState @@ -12,22 +13,31 @@ async def _fetch_soc(username, password, region, chargepoint) -> CarState: - async def getNissanSession(): # open HTTPS session with Nissan server + async def getNissanSession(): + # open HTTPS session with Nissan server log.debug("LP%s: login = %s, region = %s" % (chargepoint, username, region)) session = pycarwings3.Session(username, password, region) leaf = await session.get_leaf() - await asyncio.sleep(1) # give Nissan server some time + await asyncio.sleep(1) return leaf - async def readSoc(leaf) -> CarState: # get SoC & range from Nissan server + async def readSoc(leaf) -> CarState: + # get SoC & range & time stamp from Nissan server leaf_info = await leaf.get_latest_battery_status() soc = float(leaf_info.battery_percent) log.debug("LP%s: Battery State of Charge %s" % (chargepoint, soc)) range = int(leaf_info.answer["BatteryStatusRecords"]["CruisingRangeAcOff"])/1000 log.debug("LP%s: Cruising range AC Off %s" % (chargepoint, range)) - return CarState(soc, range) + time_stamp_str_utc = leaf_info.answer["BatteryStatusRecords"]["NotificationDateAndTime"] + soc_time = datetime.strptime(f"{time_stamp_str_utc}", "%Y/%m/%d %H:%M").replace(tzinfo=timezone.utc) + log.debug("LP%s: Date&Time of SoC (UTC) %s" % (chargepoint, soc_time)) + soc_timestamp = soc_time.timestamp() + log.debug("LP%s: soc_timestamp %s" % (chargepoint, soc_timestamp)) + log.debug("LP%s: local Date&Time of SoC %s" % (chargepoint, datetime.fromtimestamp(soc_timestamp))) + return CarState(soc, range, soc_timestamp) - async def requestSoc(leaf: pycarwings3.Leaf): # request Nissan server to request last SoC from car + async def requestSoc(leaf: pycarwings3.Leaf): + # request Nissan server to request last SoC from car log.debug("LP%s: Request SoC Update from vehicle" % (chargepoint)) key = await leaf.request_update() sleepsecs = 20 From 03cb6df0d8f2907a51b485dc6c1deed848d415e0 Mon Sep 17 00:00:00 2001 From: mekrapp <158028484+mekrapp@users.noreply.github.com> Date: Tue, 18 Mar 2025 14:51:52 +0100 Subject: [PATCH 45/55] Update soc.py "charge_point" renamed to "vehicle" (missunderstanding) --- packages/modules/vehicles/leaf/soc.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/modules/vehicles/leaf/soc.py b/packages/modules/vehicles/leaf/soc.py index 709a7fd580..6fab41ded5 100644 --- a/packages/modules/vehicles/leaf/soc.py +++ b/packages/modules/vehicles/leaf/soc.py @@ -28,14 +28,14 @@ def updater(vehicle_update_data: VehicleUpdateData) -> CarState: calc_while_charging=False) -def leaf_update(user_id: str, password: str, region: str, charge_point: int): - log.debug("Leaf: user_id="+user_id+" region="+region+" charge_point="+str(charge_point)) - vehicle_config = LeafSoc(configuration=LeafConfiguration(charge_point, user_id, password, region)) - store.get_car_value_store(charge_point).store.set(api.fetch_soc( +def leaf_update(user_id: str, password: str, region: str, vehicle: int): + log.debug("Leaf: user_id="+user_id+" region="+region+" vehicle="+str(vehicle)) + vehicle_config = LeafSoc(configuration=LeafConfiguration(vehicle, user_id, password, region)) + store.get_car_value_store(vehicle).store.set(api.fetch_soc( vehicle_config.configuration.user_id, vehicle_config.configuration.password, vehicle_config.configuration.region, - charge_point)) + vehicle)) def main(argv: List[str]): From 5b2978db1c4d21e6ecae5355534aadd51bf73216 Mon Sep 17 00:00:00 2001 From: mekrapp <158028484+mekrapp@users.noreply.github.com> Date: Tue, 18 Mar 2025 15:10:54 +0100 Subject: [PATCH 46/55] Update api.py "chargepoint" renamed to "vehicle" "LP" renamed to "vehicle" --- packages/modules/vehicles/leaf/api.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/packages/modules/vehicles/leaf/api.py b/packages/modules/vehicles/leaf/api.py index e2a4b1de19..5d33dcd15f 100644 --- a/packages/modules/vehicles/leaf/api.py +++ b/packages/modules/vehicles/leaf/api.py @@ -11,11 +11,11 @@ log = logging.getLogger(__name__) -async def _fetch_soc(username, password, region, chargepoint) -> CarState: +async def _fetch_soc(username, password, region, vehicle) -> CarState: async def getNissanSession(): # open HTTPS session with Nissan server - log.debug("LP%s: login = %s, region = %s" % (chargepoint, username, region)) + log.debug("vehicle%s: login = %s, region = %s" % (vehicle, username, region)) session = pycarwings3.Session(username, password, region) leaf = await session.get_leaf() await asyncio.sleep(1) @@ -25,20 +25,20 @@ async def readSoc(leaf) -> CarState: # get SoC & range & time stamp from Nissan server leaf_info = await leaf.get_latest_battery_status() soc = float(leaf_info.battery_percent) - log.debug("LP%s: Battery State of Charge %s" % (chargepoint, soc)) + log.debug("vehicle%s: Battery State of Charge %s" % (vehicle, soc)) range = int(leaf_info.answer["BatteryStatusRecords"]["CruisingRangeAcOff"])/1000 - log.debug("LP%s: Cruising range AC Off %s" % (chargepoint, range)) + log.debug("vehicle%s: Cruising range AC Off %s" % (vehicle, range)) time_stamp_str_utc = leaf_info.answer["BatteryStatusRecords"]["NotificationDateAndTime"] soc_time = datetime.strptime(f"{time_stamp_str_utc}", "%Y/%m/%d %H:%M").replace(tzinfo=timezone.utc) - log.debug("LP%s: Date&Time of SoC (UTC) %s" % (chargepoint, soc_time)) + log.debug("vehicle%s: Date&Time of SoC (UTC) %s" % (vehicle, soc_time)) soc_timestamp = soc_time.timestamp() - log.debug("LP%s: soc_timestamp %s" % (chargepoint, soc_timestamp)) - log.debug("LP%s: local Date&Time of SoC %s" % (chargepoint, datetime.fromtimestamp(soc_timestamp))) + log.debug("vehicle%s: soc_timestamp %s" % (vehicle, soc_timestamp)) + log.debug("vehicle%s: local Date&Time of SoC %s" % (vehicle, datetime.fromtimestamp(soc_timestamp))) return CarState(soc, range, soc_timestamp) async def requestSoc(leaf: pycarwings3.Leaf): # request Nissan server to request last SoC from car - log.debug("LP%s: Request SoC Update from vehicle" % (chargepoint)) + log.debug("vehicle%s: Request SoC Update from vehicle" % (vehicle)) key = await leaf.request_update() sleepsecs = 20 for _ in range(0, 3): @@ -46,9 +46,9 @@ async def requestSoc(leaf: pycarwings3.Leaf): await asyncio.sleep(sleepsecs) status = await leaf.get_status_from_update(key) if status is not None: - log.debug("LP%s: Update successful" % (chargepoint)) + log.debug("vehicle%s: Update successful" % (vehicle)) return status - log.debug("LP%s: Update not successful" % (chargepoint)) + log.debug("vehicle%s: Update not successful" % (vehicle)) return status try: @@ -60,18 +60,18 @@ async def requestSoc(leaf: pycarwings3.Leaf): await asyncio.sleep(1) # give Nissan server some time soc_range = await readSoc(leaf) # final read of SoC & range from server except pycarwings3.CarwingsError as e: - log.info("LP%s: SoC & range request not successful" % (chargepoint)) + log.info("vehicle%s: SoC & range request not successful" % (vehicle)) log.info(e) soc_range = CarState(0.0, 0.0) return soc_range # main entry - _fetch_soc needs to be run async -def fetch_soc(user_id: str, password: str, region: str, chargepoint: int) -> CarState: +def fetch_soc(user_id: str, password: str, region: str, vehicle: int) -> CarState: loop = asyncio.new_event_loop() # prepare and call async method asyncio.set_event_loop(loop) # get SoC and range from vehicle via server - soc_range = loop.run_until_complete(_fetch_soc(user_id, password, region, chargepoint)) + soc_range = loop.run_until_complete(_fetch_soc(user_id, password, region, vehicle)) return soc_range From cdcd2c7e97ab4618db556c0494d8467485d6c937 Mon Sep 17 00:00:00 2001 From: mekrapp <158028484+mekrapp@users.noreply.github.com> Date: Tue, 18 Mar 2025 15:42:00 +0100 Subject: [PATCH 47/55] Delete packages/modules/vehicles/leaf/api_wo_CarState.py needed for test purpose only --- .../modules/vehicles/leaf/api_wo_CarState.py | 67 ------------------- 1 file changed, 67 deletions(-) delete mode 100644 packages/modules/vehicles/leaf/api_wo_CarState.py diff --git a/packages/modules/vehicles/leaf/api_wo_CarState.py b/packages/modules/vehicles/leaf/api_wo_CarState.py deleted file mode 100644 index 7426191fe3..0000000000 --- a/packages/modules/vehicles/leaf/api_wo_CarState.py +++ /dev/null @@ -1,67 +0,0 @@ -#!/usr/bin/env python3 -import asyncio -import logging - -#from modules.vehicles.leaf.config import LeafSoc, LeafConfiguration - -import pycarwings3 - -log = logging.getLogger(__name__) - - -async def _fetch_soc(username, password, chargepoint): - - region = "NE" - - async def getNissanSession(): # open Https session with Nissan server - log.debug("LP%s: login = %s, region = %s" % (chargepoint, username, region)) - session = pycarwings3.Session(username, password, region) - leaf = await session.get_leaf() - await asyncio.sleep(1) # give Nissan server some time - return leaf - - async def readSoc(leaf): # get SoC from Nissan server - leaf_info = await leaf.get_latest_battery_status() - bat_percent = int(leaf_info.battery_percent) - log.debug("LP%s: Battery status %s" % (chargepoint, bat_percent)) - return bat_percent - - async def requestSoc(leaf: pycarwings3.Leaf): # request Nissan server to request last SoC from car - log.debug("LP%s: Request SoC Update from vehicle" % (chargepoint)) - key = await leaf.request_update() - sleepsecs = 20 - for _ in range(0, 3): - log.debug("Waiting {0} seconds".format(sleepsecs)) - await asyncio.sleep(sleepsecs) - status = await leaf.get_status_from_update(key) - if status is not None: - log.debug("LP%s: Update successful" % (chargepoint)) - return status - log.debug("LP%s: Update not successful" % (chargepoint)) - return status - - try: - leaf = await getNissanSession() # start HTTPS session with Nissan server - soc = await readSoc(leaf) # old SoC needs to be read from server before requesting new SoC from vehicle - await asyncio.sleep(1) # give Nissan server some time - status = await requestSoc(leaf) # Nissan server to request new SoC from vehicle - if status is not None: # was update of SoC successful? - await asyncio.sleep(1) # give Nissan server some time - soc = await readSoc(leaf) # final read of SoC from server - except pycarwings3.CarwingsError as e: - log.info("LP%s: SoC request not successful" % (chargepoint)) - log.info(e) - soc = 0 - return soc - -# main entry - _fetch_soc needs to be run async -def fetch_soc(user_id: str, password: str, vnum: int): #-> CarState: - - loop = asyncio.new_event_loop() # prepare and call async method - asyncio.set_event_loop(loop) - - # get soc from vehicle via server - soc = loop.run_until_complete(_fetch_soc(user_id, password, vnum)) - - return soc -# return CarState(soc) \ No newline at end of file From 5a6a90b0c7c8bb4bf2a6689fc69d6b21ba7aa786 Mon Sep 17 00:00:00 2001 From: mekrapp <158028484+mekrapp@users.noreply.github.com> Date: Tue, 18 Mar 2025 15:42:59 +0100 Subject: [PATCH 48/55] Delete packages/modules/vehicles/leaf/test_fetch_soc.py needed for test purpose only --- packages/modules/vehicles/leaf/test_fetch_soc.py | 15 --------------- 1 file changed, 15 deletions(-) delete mode 100644 packages/modules/vehicles/leaf/test_fetch_soc.py diff --git a/packages/modules/vehicles/leaf/test_fetch_soc.py b/packages/modules/vehicles/leaf/test_fetch_soc.py deleted file mode 100644 index 113ae42ff6..0000000000 --- a/packages/modules/vehicles/leaf/test_fetch_soc.py +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env python3 - -import sys -import logging - -from api_wo_CarState import fetch_soc - -logging.basicConfig(stream=sys.stdout, level=5, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') # - -username = 'xyz@ab.de' #replace by your user ID -password = 'secretPassword' #replace by your password -chargepoint = 1 - -print("SoC: " + str(fetch_soc(username, password, chargepoint))) - From b9e30cf02c0d571c43417c1a8a7669882515f90a Mon Sep 17 00:00:00 2001 From: mekrapp <158028484+mekrapp@users.noreply.github.com> Date: Wed, 19 Mar 2025 13:56:11 +0100 Subject: [PATCH 49/55] Update soc.py trailing whitespaces removed --- packages/modules/vehicles/leaf/soc.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/modules/vehicles/leaf/soc.py b/packages/modules/vehicles/leaf/soc.py index 6fab41ded5..aca9f4a187 100644 --- a/packages/modules/vehicles/leaf/soc.py +++ b/packages/modules/vehicles/leaf/soc.py @@ -22,8 +22,8 @@ def updater(vehicle_update_data: VehicleUpdateData) -> CarState: vehicle_config.configuration.password, vehicle_config.configuration.region, vehicle) - return ConfigurableVehicle(vehicle_config=vehicle_config, - component_updater=updater, + return ConfigurableVehicle(vehicle_config=vehicle_config, + component_updater=updater, vehicle=vehicle, calc_while_charging=False) From 5d280ea39c526a1c0af0a2b6034e532ecfc42196 Mon Sep 17 00:00:00 2001 From: mekrapp <158028484+mekrapp@users.noreply.github.com> Date: Wed, 19 Mar 2025 14:10:03 +0100 Subject: [PATCH 50/55] Update api.py according to flake8: trailing whitespace removed. import of LeafSoc und LeafConfiguration removed (not used). comment shortened. blank line added --- packages/modules/vehicles/leaf/api.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/modules/vehicles/leaf/api.py b/packages/modules/vehicles/leaf/api.py index 5d33dcd15f..6676f94d26 100644 --- a/packages/modules/vehicles/leaf/api.py +++ b/packages/modules/vehicles/leaf/api.py @@ -3,7 +3,6 @@ import logging from datetime import datetime, timezone -from modules.vehicles.leaf.config import LeafSoc, LeafConfiguration from modules.common.component_state import CarState import pycarwings3 @@ -27,7 +26,7 @@ async def readSoc(leaf) -> CarState: soc = float(leaf_info.battery_percent) log.debug("vehicle%s: Battery State of Charge %s" % (vehicle, soc)) range = int(leaf_info.answer["BatteryStatusRecords"]["CruisingRangeAcOff"])/1000 - log.debug("vehicle%s: Cruising range AC Off %s" % (vehicle, range)) + log.debug("vehicle%s: Cruising range AC Off %s" % (vehicle, range)) time_stamp_str_utc = leaf_info.answer["BatteryStatusRecords"]["NotificationDateAndTime"] soc_time = datetime.strptime(f"{time_stamp_str_utc}", "%Y/%m/%d %H:%M").replace(tzinfo=timezone.utc) log.debug("vehicle%s: Date&Time of SoC (UTC) %s" % (vehicle, soc_time)) @@ -53,7 +52,7 @@ async def requestSoc(leaf: pycarwings3.Leaf): try: leaf = await getNissanSession() # start HTTPS session with Nissan server - soc_range = await readSoc(leaf) # old SoC & range need to be read from server before requesting new values from vehicle + soc_range = await readSoc(leaf) # read old SoC & range values from server await asyncio.sleep(1) # give Nissan server some time status = await requestSoc(leaf) # Nissan server to request new values from vehicle if status is not None: # was update of values successful? @@ -64,6 +63,7 @@ async def requestSoc(leaf: pycarwings3.Leaf): log.info(e) soc_range = CarState(0.0, 0.0) return soc_range + # main entry - _fetch_soc needs to be run async def fetch_soc(user_id: str, password: str, region: str, vehicle: int) -> CarState: From dc9c8d42a7403239a9a5921ad314b577715fb7fa Mon Sep 17 00:00:00 2001 From: mekrapp <158028484+mekrapp@users.noreply.github.com> Date: Wed, 19 Mar 2025 14:13:26 +0100 Subject: [PATCH 51/55] Update api.py whitespaces removed --- packages/modules/vehicles/leaf/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/modules/vehicles/leaf/api.py b/packages/modules/vehicles/leaf/api.py index 6676f94d26..ff06bd96dd 100644 --- a/packages/modules/vehicles/leaf/api.py +++ b/packages/modules/vehicles/leaf/api.py @@ -63,7 +63,7 @@ async def requestSoc(leaf: pycarwings3.Leaf): log.info(e) soc_range = CarState(0.0, 0.0) return soc_range - + # main entry - _fetch_soc needs to be run async def fetch_soc(user_id: str, password: str, region: str, vehicle: int) -> CarState: From 1b93125cdf6388b2e5d6a90ff0282d375865b640 Mon Sep 17 00:00:00 2001 From: mekrapp <158028484+mekrapp@users.noreply.github.com> Date: Thu, 10 Apr 2025 13:43:10 +0200 Subject: [PATCH 52/55] Update soc.py calc_while_charging removed --- packages/modules/vehicles/leaf/soc.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/modules/vehicles/leaf/soc.py b/packages/modules/vehicles/leaf/soc.py index aca9f4a187..ecb5474579 100644 --- a/packages/modules/vehicles/leaf/soc.py +++ b/packages/modules/vehicles/leaf/soc.py @@ -24,8 +24,8 @@ def updater(vehicle_update_data: VehicleUpdateData) -> CarState: vehicle) return ConfigurableVehicle(vehicle_config=vehicle_config, component_updater=updater, - vehicle=vehicle, - calc_while_charging=False) + vehicle=vehicle) +# calc_while_charging=False) def leaf_update(user_id: str, password: str, region: str, vehicle: int): From b1d8012468cda809a1aaac2cc01c347f2e0dd9f5 Mon Sep 17 00:00:00 2001 From: mekrapp <158028484+mekrapp@users.noreply.github.com> Date: Thu, 10 Apr 2025 15:00:54 +0200 Subject: [PATCH 53/55] Update soc.py variable "vehicle" renamed to "charge_point" --- packages/modules/vehicles/leaf/soc.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/modules/vehicles/leaf/soc.py b/packages/modules/vehicles/leaf/soc.py index ecb5474579..b1374e13c7 100644 --- a/packages/modules/vehicles/leaf/soc.py +++ b/packages/modules/vehicles/leaf/soc.py @@ -28,14 +28,17 @@ def updater(vehicle_update_data: VehicleUpdateData) -> CarState: # calc_while_charging=False) -def leaf_update(user_id: str, password: str, region: str, vehicle: int): - log.debug("Leaf: user_id="+user_id+" region="+region+" vehicle="+str(vehicle)) - vehicle_config = LeafSoc(configuration=LeafConfiguration(vehicle, user_id, password, region)) - store.get_car_value_store(vehicle).store.set(api.fetch_soc( +def leaf_update(user_id: str, password: str, region: str, charge_point: int): + log.debug("leaf: user_id="+user_id+" region="+region+" charge_point="+str(charge_point)) + vehicle_config = LeafSoc(configuration=LeafConfiguration(charge_point, + user_id, + password, + region)) + store.get_car_value_store(charge_point).store.set(api.fetch_soc( vehicle_config.configuration.user_id, vehicle_config.configuration.password, vehicle_config.configuration.region, - vehicle)) + charge_point)) def main(argv: List[str]): From 5639687f3f8fd581d347fe6ad15a4e463f7d1313 Mon Sep 17 00:00:00 2001 From: mekrapp <158028484+mekrapp@users.noreply.github.com> Date: Thu, 10 Apr 2025 15:04:54 +0200 Subject: [PATCH 54/55] Update soc.py trailing whitespaces removed --- packages/modules/vehicles/leaf/soc.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/modules/vehicles/leaf/soc.py b/packages/modules/vehicles/leaf/soc.py index b1374e13c7..e260630264 100644 --- a/packages/modules/vehicles/leaf/soc.py +++ b/packages/modules/vehicles/leaf/soc.py @@ -25,14 +25,13 @@ def updater(vehicle_update_data: VehicleUpdateData) -> CarState: return ConfigurableVehicle(vehicle_config=vehicle_config, component_updater=updater, vehicle=vehicle) -# calc_while_charging=False) def leaf_update(user_id: str, password: str, region: str, charge_point: int): log.debug("leaf: user_id="+user_id+" region="+region+" charge_point="+str(charge_point)) - vehicle_config = LeafSoc(configuration=LeafConfiguration(charge_point, - user_id, - password, + vehicle_config = LeafSoc(configuration=LeafConfiguration(charge_point, + user_id, + password, region)) store.get_car_value_store(charge_point).store.set(api.fetch_soc( vehicle_config.configuration.user_id, From 3ec4c0719bb181e6974a9b087d91757665b21a26 Mon Sep 17 00:00:00 2001 From: LKuemmel <76958050+LKuemmel@users.noreply.github.com> Date: Fri, 11 Apr 2025 14:31:26 +0200 Subject: [PATCH 55/55] Mock pycarwings3 for pytest --- packages/modules/conftest.py | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/modules/conftest.py b/packages/modules/conftest.py index f83ea44a66..06b423dcdf 100644 --- a/packages/modules/conftest.py +++ b/packages/modules/conftest.py @@ -17,6 +17,7 @@ sys.modules['skodaconnect.Connection'] = type(sys)('skodaconnect.Connection') sys.modules['socketserver'] = type(sys)('socketserver') sys.modules['grpc'] = type(sys)('grpc') +sys.modules['pycarwings3'] = type(sys)('pycarwings3') # sys.modules['telnetlib3'] = type(sys)('telnetlib3')