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/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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/17] Update __init__.py empty line removed at end of file From 917f5b1128d513d944d3b5f1d2b662d82e078f63 Mon Sep 17 00:00:00 2001 From: mekrapp <158028484+mekrapp@users.noreply.github.com> Date: Sat, 24 Aug 2024 10:56:02 +0200 Subject: [PATCH 17/17] Update pycarwings2.py wildcard * at import instruction replaced by the names of the classes to be imported --- packages/modules/vehicles/leaf/pycarwings2.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/modules/vehicles/leaf/pycarwings2.py b/packages/modules/vehicles/leaf/pycarwings2.py index 1265fe762e..6041ecb4d4 100644 --- a/packages/modules/vehicles/leaf/pycarwings2.py +++ b/packages/modules/vehicles/leaf/pycarwings2.py @@ -68,7 +68,9 @@ import json import logging from datetime import date -from responses import * +from responses import (CarwingsInitialAppResponse, CarwingsLoginResponse, CarwingsBatteryStatusResponse, + CarwingsStartClimateControlResponse, CarwingsStopClimateControlResponse, CarwingsClimateControlScheduleResponse, + CarwingsDrivingAnalysisResponse, CarwingsLatestBatteryStatusResponse, CarwingsLatestClimateControlStatusResponse) import base64 from Crypto.Cipher import Blowfish