From 8b3ce10f100ee133387795646e07bf9323bc5473 Mon Sep 17 00:00:00 2001 From: steve Date: Thu, 24 Aug 2023 09:40:35 +1000 Subject: [PATCH 01/17] added gitlab CI --- custom_components/fordpass/.gitlab-ci.yml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 custom_components/fordpass/.gitlab-ci.yml diff --git a/custom_components/fordpass/.gitlab-ci.yml b/custom_components/fordpass/.gitlab-ci.yml new file mode 100644 index 0000000..eb7deae --- /dev/null +++ b/custom_components/fordpass/.gitlab-ci.yml @@ -0,0 +1,22 @@ +image: "python:3.11" + +before_script: + #- cp /etc/gitlab-runner/certs/ca.crt /usr/local/share/ca-certificates/ca.crt + #- update-ca-certificates + - python3 --version + - pip install -r requirements.txt + - pip install bandit + +stages: + - Static Analysis + + + + +static_analysis: + stage: Static Analysis + tags: ["dev","python"] + allow_failure: true + script: + - flake8 --max-line-length=120 + - pylint . --recursive=y --function-naming-style=camelCase --method-naming-style=camelCase --disable=W0613,C0115 \ No newline at end of file From c51d93a7844a51694a4c92518d6114d7a3df7acc Mon Sep 17 00:00:00 2001 From: steve Date: Thu, 24 Aug 2023 09:42:13 +1000 Subject: [PATCH 02/17] gitlab ci --- custom_components/fordpass/.gitlab-ci.yml => .gitlab-ci.yml | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename custom_components/fordpass/.gitlab-ci.yml => .gitlab-ci.yml (100%) diff --git a/custom_components/fordpass/.gitlab-ci.yml b/.gitlab-ci.yml similarity index 100% rename from custom_components/fordpass/.gitlab-ci.yml rename to .gitlab-ci.yml From 3a4dac65394dc6e60d0a161b51d9d69f123c4dfb Mon Sep 17 00:00:00 2001 From: steve Date: Thu, 24 Aug 2023 09:42:52 +1000 Subject: [PATCH 03/17] remove requirements --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index eb7deae..c8eed43 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -4,7 +4,7 @@ before_script: #- cp /etc/gitlab-runner/certs/ca.crt /usr/local/share/ca-certificates/ca.crt #- update-ca-certificates - python3 --version - - pip install -r requirements.txt + #- pip install -r requirements.txt - pip install bandit stages: From d99215087149191d286ed35959179af246b89247 Mon Sep 17 00:00:00 2001 From: steve Date: Thu, 24 Aug 2023 09:43:41 +1000 Subject: [PATCH 04/17] missing deps --- .gitlab-ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index c8eed43..5187a3c 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -5,6 +5,8 @@ before_script: #- update-ca-certificates - python3 --version #- pip install -r requirements.txt + - pip install flake8 + - pip install pylint - pip install bandit stages: From 802fff20dc18f9495d282be1d22af4c227f0154b Mon Sep 17 00:00:00 2001 From: steve Date: Thu, 24 Aug 2023 17:07:44 +1000 Subject: [PATCH 05/17] refactor pt1 --- custom_components/fordpass/__init__.py | 8 ++++---- custom_components/fordpass/config_flow.py | 8 -------- custom_components/fordpass/device_tracker.py | 3 +-- custom_components/fordpass/fordpass_new.py | 15 ++++++--------- 4 files changed, 11 insertions(+), 23 deletions(-) diff --git a/custom_components/fordpass/__init__.py b/custom_components/fordpass/__init__.py index 1603da3..ec1ab35 100644 --- a/custom_components/fordpass/__init__.py +++ b/custom_components/fordpass/__init__.py @@ -38,7 +38,6 @@ _LOGGER = logging.getLogger(__name__) - async def async_setup(hass: HomeAssistant, config: dict): """Set up the FordPass component.""" hass.data.setdefault(DOMAIN, {}) @@ -76,7 +75,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): raise ConfigEntryNotReady hass.data[DOMAIN][entry.entry_id] = { - COORDINATOR : coordinator, + COORDINATOR: coordinator, "fordpass_options_listener": fordpass_options_listener } @@ -96,7 +95,6 @@ async def async_clear_tokens_service(service_call): async def poll_api_service(service_call): await coordinator.async_request_refresh() - async def handle_reload(service): """Handle reload service call.""" _LOGGER.debug("Reloading Integration") @@ -146,12 +144,14 @@ async def async_update_options(hass, config_entry): ) hass.config_entries.async_update_entry(config_entry, options=options) + async def options_update_listener( - hass: HomeAssistant, entry: ConfigEntry + hass: HomeAssistant, entry: ConfigEntry ): _LOGGER.debug("OPTIONS CHANGE") await hass.config_entries.async_reload(entry.entry_id) + def refresh_status(hass, service, coordinator): _LOGGER.debug("Running Service") vin = service.data.get("vin", "") diff --git a/custom_components/fordpass/config_flow.py b/custom_components/fordpass/config_flow.py index 66d6292..662979b 100644 --- a/custom_components/fordpass/config_flow.py +++ b/custom_components/fordpass/config_flow.py @@ -50,14 +50,6 @@ async def validate_input(hass: core.HomeAssistant, data): except Exception as ex: raise InvalidAuth from ex - #result3 = await hass.async_add_executor_job(vehicle.vehicles) - # Disabled due to API change - #vinfound = False - #for car in result3: - # if car["vin"] == data[VIN]: - # vinfound = True - #if vinfound == False: - # _LOGGER.debug("Vin not found in account, Is your VIN valid?") if not result: _LOGGER.error("Failed to authenticate with fordpass") raise CannotConnect diff --git a/custom_components/fordpass/device_tracker.py b/custom_components/fordpass/device_tracker.py index 3985f8f..746b81d 100644 --- a/custom_components/fordpass/device_tracker.py +++ b/custom_components/fordpass/device_tracker.py @@ -1,5 +1,4 @@ import logging -from datetime import timedelta from homeassistant.components.device_tracker import SOURCE_TYPE_GPS from homeassistant.components.device_tracker.config_entry import TrackerEntity @@ -15,7 +14,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): entry = hass.data[DOMAIN][config_entry.entry_id][COORDINATOR] # Added a check to see if the car supports GPS - if entry.data["gps"] != None: + if entry.data["gps"] is not None: async_add_entities([CarTracker(entry, "gps")], True) else: _LOGGER.debug("Vehicle does not support GPS") diff --git a/custom_components/fordpass/fordpass_new.py b/custom_components/fordpass/fordpass_new.py index 02aa803..81cc445 100644 --- a/custom_components/fordpass/fordpass_new.py +++ b/custom_components/fordpass/fordpass_new.py @@ -8,7 +8,7 @@ import string import time -from base64 import urlsafe_b64encode, urlsafe_b64decode +from base64 import urlsafe_b64encode from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry @@ -64,7 +64,7 @@ def __init__( _LOGGER.debug(configLocation) self.token_location = configLocation - def base64UrlEncode(self,data): + def base64UrlEncode(self, data): return urlsafe_b64encode(data).rstrip(b'=') def generate_hash(self, code): @@ -72,7 +72,6 @@ def generate_hash(self, code): m.update(code.encode('utf-8')) return self.base64UrlEncode(m.digest()).decode('utf-8') - def auth(self): _LOGGER.debug("New System") """New Authentication System """ @@ -92,7 +91,6 @@ def auth(self): test = re.findall('data-ibm-login-url="(.*)"\s', r.text)[0] nextUrl = ssoUrl + test - # Auth Step2 headers = { **defaultHeaders, @@ -101,20 +99,19 @@ def auth(self): data = { "operation": "verify", "login-form-type": "password", - "username" : self.username, - "password" : self.password + "username": self.username, + "password": self.password } r = session.post( nextUrl, headers=headers, - data = data, + data=data, allow_redirects=False - ) if r.status_code == 302: - nextUrl = r.headers["Location"] + nextUrl=r.headers["Location"] else: r.raise_for_status() From ffefdae6aae1463bd2b63b8d77f7aa200f0e100b Mon Sep 17 00:00:00 2001 From: steve Date: Thu, 24 Aug 2023 17:28:03 +1000 Subject: [PATCH 06/17] flake8 refactor --- .gitlab-ci.yml | 4 +- custom_components/fordpass/__init__.py | 8 +- custom_components/fordpass/config_flow.py | 2 +- custom_components/fordpass/fordpass_new.py | 22 ++---- custom_components/fordpass/sensor.py | 91 +++++++++------------- custom_components/fordpass/switch.py | 3 +- 6 files changed, 53 insertions(+), 77 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 5187a3c..0c6040b 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -20,5 +20,5 @@ static_analysis: tags: ["dev","python"] allow_failure: true script: - - flake8 --max-line-length=120 - - pylint . --recursive=y --function-naming-style=camelCase --method-naming-style=camelCase --disable=W0613,C0115 \ No newline at end of file + - flake8 --max-line-length=120 --disable=w605 + - pylint . --recursive=y --function-naming-style=camelCase --method-naming-style=camelCase --disable=W0613,C0115,w605 \ No newline at end of file diff --git a/custom_components/fordpass/__init__.py b/custom_components/fordpass/__init__.py index ec1ab35..1bfe6fd 100644 --- a/custom_components/fordpass/__init__.py +++ b/custom_components/fordpass/__init__.py @@ -145,11 +145,9 @@ async def async_update_options(hass, config_entry): hass.config_entries.async_update_entry(config_entry, options=options) -async def options_update_listener( - hass: HomeAssistant, entry: ConfigEntry - ): - _LOGGER.debug("OPTIONS CHANGE") - await hass.config_entries.async_reload(entry.entry_id) +async def options_update_listener(hass: HomeAssistant, entry: ConfigEntry): + _LOGGER.debug("OPTIONS CHANGE") + await hass.config_entries.async_reload(entry.entry_id) def refresh_status(hass, service, coordinator): diff --git a/custom_components/fordpass/config_flow.py b/custom_components/fordpass/config_flow.py index 662979b..0105e83 100644 --- a/custom_components/fordpass/config_flow.py +++ b/custom_components/fordpass/config_flow.py @@ -116,7 +116,7 @@ async def async_step_init(self, user_input=None): ): vol.In(DISTANCE_UNITS), vol.Optional( DISTANCE_CONVERSION_DISABLED, - default = self.config_entry.options.get( + default=self.config_entry.options.get( DISTANCE_CONVERSION_DISABLED, DISTANCE_CONVERSION_DISABLED_DEFAULT ), ): bool, diff --git a/custom_components/fordpass/fordpass_new.py b/custom_components/fordpass/fordpass_new.py index 81cc445..ce80426 100644 --- a/custom_components/fordpass/fordpass_new.py +++ b/custom_components/fordpass/fordpass_new.py @@ -111,7 +111,7 @@ def auth(self): ) if r.status_code == 302: - nextUrl=r.headers["Location"] + nextUrl = r.headers["Location"] else: r.raise_for_status() @@ -123,13 +123,10 @@ def auth(self): r = session.get( nextUrl, - headers = headers, + headers=headers, allow_redirects=False ) - - - if r.status_code == 302: nextUrl = r.headers["Location"] query = requests.utils.urlparse(nextUrl).query @@ -139,7 +136,7 @@ def auth(self): else: r.raise_for_status() - # Auth Step4 + # Auth Step4 headers = { **defaultHeaders, "Content-Type": "application/x-www-form-urlencoded", @@ -152,16 +149,15 @@ def auth(self): "grant_id": grant_id, "code": code, "code_verifier": code1 - } + } r = session.post( f"{ssoUrl}/oidc/endpoint/default/token", - headers = headers, - data = data + headers=headers, + data=data ) - if r.status_code == 200: result = r.json() if result["access_token"]: @@ -169,7 +165,6 @@ def auth(self): else: r.raise_for_status() - # Auth Step5 data = {"ciToken": access_token} headers = {**apiHeaders, "Application-Id": self.region} @@ -238,7 +233,7 @@ def __acquireToken(self): _LOGGER.debug("No token, or has expired, requesting new token") self.refreshToken(data) # self.auth() - if self.token == None: + if self.token is None: # No existing token exists so refreshing library self.auth() else: @@ -353,9 +348,8 @@ def vehicles(self): "Locale": "EN-US" } - data = { - "dashboardRefreshRequest":"All" + "dashboardRefreshRequest": "All" } r = session.post( guardUrl + "/expdashboard/v1/details/", diff --git a/custom_components/fordpass/sensor.py b/custom_components/fordpass/sensor.py index ff36f3d..9de133f 100644 --- a/custom_components/fordpass/sensor.py +++ b/custom_components/fordpass/sensor.py @@ -1,8 +1,7 @@ import logging -from datetime import datetime, timedelta +from datetime import datetime -from homeassistant.helpers.entity import Entity -from homeassistant.util import Throttle, dt +from homeassistant.util import dt from homeassistant.components.sensor import ( SensorEntity, @@ -28,7 +27,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): if "zoneLighting" in sensor.coordinator.data: sensors.append(sensor) elif key == "elVeh": - if sensor.coordinator.data["elVehDTE"] != None: + if sensor.coordinator.data["elVehDTE"] is not None: sensors.append(sensor) elif key == "dieselSystemStatus": if sensor.coordinator.data.get("dieselSystemStatus", {}): @@ -42,6 +41,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): sensors.append(sensor) async_add_entities(sensors, True) + class CarSensor( FordPassEntity, SensorEntity, @@ -59,21 +59,20 @@ def __init__(self, coordinator, sensor, options): def get_value(self, ftype): if ftype == "state": if self.sensor == "odometer": - if self.fordoptions[CONF_DISTANCE_UNIT] != None: + if self.fordoptions[CONF_DISTANCE_UNIT] is not None: if self.fordoptions[CONF_DISTANCE_UNIT] == "mi": - if DISTANCE_CONVERSION_DISABLED in self.fordoptions and self.fordoptions[DISTANCE_CONVERSION_DISABLED] == True: + if DISTANCE_CONVERSION_DISABLED in self.fordoptions and self.fordoptions[DISTANCE_CONVERSION_DISABLED] is True: return self.coordinator.data[self.sensor]["value"] else: return round( float(self.coordinator.data[self.sensor]["value"]) / 1.60934 ) - else: return self.coordinator.data[self.sensor]["value"] else: return self.coordinator.data[self.sensor]["value"] elif self.sensor == "fuel": - if self.coordinator.data[self.sensor] == None: + if self.coordinator.data[self.sensor] is None: return None return round(self.coordinator.data[self.sensor]["fuelLevel"]) elif self.sensor == "battery": @@ -83,7 +82,7 @@ def get_value(self, ftype): elif self.sensor == "tirePressure": return self.coordinator.data[self.sensor]["value"] elif self.sensor == "gps": - if self.coordinator.data[self.sensor] == None: + if self.coordinator.data[self.sensor] is None: return "Unsupported" return self.coordinator.data[self.sensor]["gpsState"] elif self.sensor == "alarm": @@ -102,7 +101,7 @@ def get_value(self, ftype): return "Open" return "Closed" elif self.sensor == "windowPosition": - if self.coordinator.data[self.sensor] == None: + if self.coordinator.data[self.sensor] is None: return "Unsupported" status = "Closed" for key, value in self.coordinator.data[self.sensor].items(): @@ -116,12 +115,11 @@ def get_value(self, ftype): ) ) elif self.sensor == "elVeh": - if self.coordinator.data["elVehDTE"] != None: - if self.fordoptions[CONF_DISTANCE_UNIT] != None: + if self.coordinator.data["elVehDTE"] is not None: + if self.fordoptions[CONF_DISTANCE_UNIT] is not None: if self.fordoptions[CONF_DISTANCE_UNIT] == "mi": return round( - float(self.coordinator.data["elVehDTE"]["value"]) - / 1.60934 + float(self.coordinator.data["elVehDTE"]["value"]) / 1.60934 ) else: return float(self.coordinator.data["elVehDTE"]["value"]) @@ -133,8 +131,7 @@ def get_value(self, ftype): if "zoneLighting" not in self.coordinator.data: return "Unsupported" if ( - self.coordinator.data["zoneLighting"] != None - and self.coordinator.data["zoneLighting"]["activationData"] != None + self.coordinator.data["zoneLighting"] is not None and self.coordinator.data["zoneLighting"]["activationData"] is not None ): return self.coordinator.data["zoneLighting"]["activationData"][ "value" @@ -142,7 +139,7 @@ def get_value(self, ftype): else: return "Unsupported" elif self.sensor == "remoteStartStatus": - if self.coordinator.data["remoteStartStatus"] == None: + if self.coordinator.data["remoteStartStatus"] is not None: return None else: if self.coordinator.data["remoteStartStatus"]["value"] == 1: @@ -150,12 +147,12 @@ def get_value(self, ftype): else: return "Inactive" elif self.sensor == "messages": - if self.coordinator.data["messages"] == None: + if self.coordinator.data["messages"] is None: return None else: return len(self.coordinator.data["messages"]) elif self.sensor == "dieselSystemStatus": - if self.coordinator.data["dieselSystemStatus"]["filterRegenerationStatus"] != None: + if self.coordinator.data["dieselSystemStatus"]["filterRegenerationStatus"] is not None: return self.coordinator.data["dieselSystemStatus"]["filterRegenerationStatus"] else: return "Not Supported" @@ -211,12 +208,11 @@ def get_value(self, ftype): if self.sensor == "odometer": return self.coordinator.data[self.sensor].items() elif self.sensor == "fuel": - if self.coordinator.data[self.sensor] == None: + if self.coordinator.data[self.sensor] is None: return None if self.fordoptions[CONF_DISTANCE_UNIT] == "mi": self.coordinator.data["fuel"]["distanceToEmpty"] = round( - float(self.coordinator.data["fuel"]["distanceToEmpty"]) - / 1.60934 + float(self.coordinator.data["fuel"]["distanceToEmpty"]) / 1.60934 ) return self.coordinator.data[self.sensor].items() elif self.sensor == "battery": @@ -228,7 +224,7 @@ def get_value(self, ftype): elif self.sensor == "oil": return self.coordinator.data[self.sensor].items() elif self.sensor == "tirePressure": - if self.coordinator.data["TPMS"] != None: + if self.coordinator.data["TPMS"] is not None: if self.fordoptions[CONF_PRESSURE_UNIT] == "PSI": sval = 0.1450377377 rval = 1 @@ -255,7 +251,7 @@ def get_value(self, ftype): return tirepress return None elif self.sensor == "gps": - if self.coordinator.data[self.sensor] == None: + if self.coordinator.data[self.sensor] is None: return None return self.coordinator.data[self.sensor].items() elif self.sensor == "alarm": @@ -272,7 +268,7 @@ def get_value(self, ftype): doors[key] = value["value"] return doors elif self.sensor == "windowPosition": - if self.coordinator.data[self.sensor] == None: + if self.coordinator.data[self.sensor] is None: return None windows = dict() for key, value in self.coordinator.data[self.sensor].items(): @@ -288,75 +284,65 @@ def get_value(self, ftype): elif self.sensor == "lastRefresh": return None elif self.sensor == "elVeh": - if self.coordinator.data["elVehDTE"] == None: + if self.coordinator.data["elVehDTE"] is None: return None else: elecs = dict() if ( - self.coordinator.data["elVehDTE"] != None - and self.coordinator.data["elVehDTE"]["value"] != None + self.coordinator.data["elVehDTE"] is not None and self.coordinator.data["elVehDTE"]["value"] is not None ): elecs["elVehDTE"] = self.coordinator.data["elVehDTE"]["value"] if ( - self.coordinator.data["plugStatus"] != None - and self.coordinator.data["plugStatus"]["value"] != None + self.coordinator.data["plugStatus"] is not None and self.coordinator.data["plugStatus"]["value"] is not None ): elecs["Plug Status"] = self.coordinator.data["plugStatus"][ "value" ] if ( - self.coordinator.data["chargingStatus"] != None - and self.coordinator.data["chargingStatus"]["value"] != None + self.coordinator.data["chargingStatus"] is not None and self.coordinator.data["chargingStatus"]["value"] is not None ): elecs["Charging Status"] = self.coordinator.data[ "chargingStatus" ]["value"] if ( - self.coordinator.data["chargeStartTime"] != None - and self.coordinator.data["chargeStartTime"]["value"] != None + self.coordinator.data["chargeStartTime"] is not None and self.coordinator.data["chargeStartTime"]["value"] is not None ): elecs["Charge Start Time"] = self.coordinator.data[ "chargeStartTime" ]["value"] if ( - self.coordinator.data["chargeEndTime"] != None - and self.coordinator.data["chargeEndTime"]["value"] != None + self.coordinator.data["chargeEndTime"] is not None and self.coordinator.data["chargeEndTime"]["value"] is not None ): elecs["Charge End Time"] = self.coordinator.data[ "chargeEndTime" ]["value"] if ( - self.coordinator.data["batteryFillLevel"] != None - and self.coordinator.data["batteryFillLevel"]["value"] != None + self.coordinator.data["batteryFillLevel"] is not None and self.coordinator.data["batteryFillLevel"]["value"] is not None ): elecs["Battery Fill Level"] = int(self.coordinator.data[ "batteryFillLevel" ]["value"]) if ( - self.coordinator.data["chargerPowertype"] != None - and self.coordinator.data["chargerPowertype"]["value"] != None + self.coordinator.data["chargerPowertype"] is not None and self.coordinator.data["chargerPowertype"]["value"] is not None ): elecs["Charger Power Type"] = self.coordinator.data[ "chargerPowertype" ]["value"] if ( - self.coordinator.data["batteryChargeStatus"] != None - and self.coordinator.data["batteryChargeStatus"]["value"] - != None + self.coordinator.data["batteryChargeStatus"] is not None and self.coordinator.data["batteryChargeStatus"]["value"] is not None ): elecs["Battery Charge Status"] = self.coordinator.data[ "batteryChargeStatus" ]["value"] if ( - self.coordinator.data["batteryPerfStatus"] != None - and self.coordinator.data["batteryPerfStatus"]["value"] != None + self.coordinator.data["batteryPerfStatus"] is not None and self.coordinator.data["batteryPerfStatus"]["value"] is not None ): elecs["Battery Performance Status"] = self.coordinator.data[ "batteryPerfStatus" @@ -367,11 +353,10 @@ def get_value(self, ftype): if "zoneLighting" not in self.coordinator.data: return None if ( - self.coordinator.data[self.sensor] != None - and self.coordinator.data[self.sensor]["zoneStatusData"] != None + self.coordinator.data[self.sensor] is not None and self.coordinator.data[self.sensor]["zoneStatusData"] is not None ): zone = dict() - if self.coordinator.data[self.sensor]["zoneStatusData"] != None: + if self.coordinator.data[self.sensor]["zoneStatusData"] is not None: for key, value in self.coordinator.data[self.sensor][ "zoneStatusData" ].items(): @@ -379,7 +364,7 @@ def get_value(self, ftype): if ( self.coordinator.data[self.sensor]["lightSwitchStatusData"] - != None + is not None ): for key, value in self.coordinator.data[self.sensor][ "lightSwitchStatusData" @@ -389,7 +374,7 @@ def get_value(self, ftype): if ( self.coordinator.data[self.sensor]["zoneLightingFaultStatus"] - != None + is not None ): zone["zoneLightingFaultStatus"] = self.coordinator.data[ self.sensor @@ -398,7 +383,7 @@ def get_value(self, ftype): self.coordinator.data[self.sensor][ "zoneLightingShutDownWarning" ] - != None + is not None ): zone["zoneLightingShutDownWarning"] = self.coordinator.data[ self.sensor @@ -407,12 +392,12 @@ def get_value(self, ftype): else: return None elif self.sensor == "remoteStartStatus": - if self.coordinator.data["remoteStart"] == None: + if self.coordinator.data["remoteStart"] is None: return None else: return self.coordinator.data["remoteStart"].items() elif self.sensor == "messages": - if self.coordinator.data["messages"] == None: + if self.coordinator.data["messages"] is None: return None else: messages = dict() diff --git a/custom_components/fordpass/switch.py b/custom_components/fordpass/switch.py index aa5e762..7c6bc7f 100644 --- a/custom_components/fordpass/switch.py +++ b/custom_components/fordpass/switch.py @@ -75,8 +75,7 @@ def is_on(self): if self.switch == "ignition": """Determine if the vehicle is started.""" if ( - self.coordinator.data is None - or self.coordinator.data["remoteStartStatus"] is None + self.coordinator.data is None or self.coordinator.data["remoteStartStatus"] is None ): return None return self.coordinator.data["remoteStartStatus"]["value"] From 04aa2c3faa84ea7bd7bba494c147c045b5a0d6ed Mon Sep 17 00:00:00 2001 From: steve Date: Thu, 24 Aug 2023 17:28:45 +1000 Subject: [PATCH 07/17] fix flake8 ci --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 0c6040b..a5c4f76 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -20,5 +20,5 @@ static_analysis: tags: ["dev","python"] allow_failure: true script: - - flake8 --max-line-length=120 --disable=w605 + - flake8 --max-line-length=280 --ignore=w605 - pylint . --recursive=y --function-naming-style=camelCase --method-naming-style=camelCase --disable=W0613,C0115,w605 \ No newline at end of file From b817745265223d172c5e491633f098d34806887d Mon Sep 17 00:00:00 2001 From: steve Date: Thu, 24 Aug 2023 17:37:05 +1000 Subject: [PATCH 08/17] ci fix --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index a5c4f76..1e77040 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -20,5 +20,5 @@ static_analysis: tags: ["dev","python"] allow_failure: true script: - - flake8 --max-line-length=280 --ignore=w605 + - flake8 --max-line-length=280 --ignore=W605 - pylint . --recursive=y --function-naming-style=camelCase --method-naming-style=camelCase --disable=W0613,C0115,w605 \ No newline at end of file From 0aac20557d09c9378b9948f3dcc75f278cae9e20 Mon Sep 17 00:00:00 2001 From: steve Date: Thu, 24 Aug 2023 18:43:47 +1000 Subject: [PATCH 09/17] refactor sensor.py --- .gitlab-ci.yml | 2 +- custom_components/fordpass/__init__.py | 14 +- custom_components/fordpass/config_flow.py | 5 +- custom_components/fordpass/lock.py | 1 + custom_components/fordpass/sensor.py | 320 +++++++++++----------- 5 files changed, 174 insertions(+), 168 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 1e77040..cada14e 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -21,4 +21,4 @@ static_analysis: allow_failure: true script: - flake8 --max-line-length=280 --ignore=W605 - - pylint . --recursive=y --function-naming-style=camelCase --method-naming-style=camelCase --disable=W0613,C0115,w605 \ No newline at end of file + - pylint . --recursive=y --function-naming-style=camelCase --method-naming-style=camelCase --disable=W0613,C0115 \ No newline at end of file diff --git a/custom_components/fordpass/__init__.py b/custom_components/fordpass/__init__.py index 1bfe6fd..6d7933e 100644 --- a/custom_components/fordpass/__init__.py +++ b/custom_components/fordpass/__init__.py @@ -54,8 +54,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): else: update_interval = UPDATE_INTERVAL_DEFAULT _LOGGER.debug(update_interval) - for ar in entry.data: - _LOGGER.debug(ar) + for ar_entry in entry.data: + _LOGGER.debug(ar_entry) if REGION in entry.data.keys(): _LOGGER.debug(entry.data[REGION]) region = entry.data[REGION] @@ -134,6 +134,7 @@ async def handle_reload(service): async def async_update_options(hass, config_entry): + """Update options entries on change""" options = { CONF_PRESSURE_UNIT: config_entry.data.get( CONF_PRESSURE_UNIT, DEFAULT_PRESSURE_UNIT @@ -146,11 +147,13 @@ async def async_update_options(hass, config_entry): async def options_update_listener(hass: HomeAssistant, entry: ConfigEntry): + """Options listener to refresh config entries on option change""" _LOGGER.debug("OPTIONS CHANGE") await hass.config_entries.async_reload(entry.entry_id) def refresh_status(hass, service, coordinator): + """Get latest vehicle status from vehicle, actively polls the car""" _LOGGER.debug("Running Service") vin = service.data.get("vin", "") status = coordinator.vehicle.requestUpdate(vin) @@ -161,6 +164,7 @@ def refresh_status(hass, service, coordinator): def clear_tokens(hass, service, coordinator): + """Clear the token file in config directory, only use in emergency""" _LOGGER.debug("Clearing Tokens") coordinator.vehicle.clearToken() @@ -185,12 +189,12 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): class FordPassDataUpdateCoordinator(DataUpdateCoordinator): """DataUpdateCoordinator to handle fetching new data about the vehicle.""" - def __init__(self, hass, user, password, vin, region, update_interval, saveToken=False): + def __init__(self, hass, user, password, vin, region, update_interval, save_token=False): """Initialize the coordinator and set up the Vehicle object.""" self._hass = hass self.vin = vin - configPath = hass.config.path("custom_components/fordpass/" + user + "_fordpass_token.txt") - self.vehicle = Vehicle(user, password, vin, region, saveToken, configPath) + config_path = hass.config.path("custom_components/fordpass/" + user + "_fordpass_token.txt") + self.vehicle = Vehicle(user, password, vin, region, save_token, config_path) self._available = True super().__init__( diff --git a/custom_components/fordpass/config_flow.py b/custom_components/fordpass/config_flow.py index 0105e83..47a8ef6 100644 --- a/custom_components/fordpass/config_flow.py +++ b/custom_components/fordpass/config_flow.py @@ -42,8 +42,8 @@ async def validate_input(hass: core.HomeAssistant, data): Data has the keys from DATA_SCHEMA with values provided by the user. """ _LOGGER.debug(data[REGION]) - configPath = hass.config.path("custom_components/fordpass/" + data[CONF_USERNAME] + "_fordpass_token.txt") - vehicle = Vehicle(data[CONF_USERNAME], data[CONF_PASSWORD], data[VIN], data[REGION], 1, configPath) + config_path = hass.config.path("custom_components/fordpass/" + data[CONF_USERNAME] + "_fordpass_token.txt") + vehicle = Vehicle(data[CONF_USERNAME], data[CONF_PASSWORD], data[VIN], data[REGION], 1, config_path) try: result = await hass.async_add_executor_job(vehicle.auth) @@ -99,6 +99,7 @@ def __init__(self, config_entry: config_entries.ConfigEntry): self.config_entry = config_entry async def async_step_init(self, user_input=None): + """Options Flow steps""" if user_input is not None: return self.async_create_entry(title="", data=user_input) options = { diff --git a/custom_components/fordpass/lock.py b/custom_components/fordpass/lock.py index 45a0b81..ff0f55c 100644 --- a/custom_components/fordpass/lock.py +++ b/custom_components/fordpass/lock.py @@ -64,4 +64,5 @@ def is_locked(self): @property def icon(self): + """Return MDI Icon""" return "mdi:car-door-lock" diff --git a/custom_components/fordpass/sensor.py b/custom_components/fordpass/sensor.py index 9de133f..16172aa 100644 --- a/custom_components/fordpass/sensor.py +++ b/custom_components/fordpass/sensor.py @@ -1,3 +1,4 @@ +"""All vehicle sensors from the accessible by the API""" import logging from datetime import datetime @@ -20,7 +21,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Add the Entities from the config.""" entry = hass.data[DOMAIN][config_entry.entry_id][COORDINATOR] sensors = [] - for key, value in SENSORS.items(): + for key in SENSORS: sensor = CarSensor(entry, key, config_entry.options) # Add support for only adding compatible sensors for the given vehicle if key == "zoneLighting": @@ -48,6 +49,12 @@ class CarSensor( ): def __init__(self, coordinator, sensor, options): + super().__init__( + device_id="fordpass_" + sensor, + name="fordpass_" + sensor, + coordinator=coordinator + ) + self.sensor = sensor self.fordoptions = options self._attr = {} @@ -57,50 +64,48 @@ def __init__(self, coordinator, sensor, options): self.coordinator_context = object() def get_value(self, ftype): + """Get sensor value and attributes from coordinator data""" if ftype == "state": if self.sensor == "odometer": if self.fordoptions[CONF_DISTANCE_UNIT] is not None: if self.fordoptions[CONF_DISTANCE_UNIT] == "mi": if DISTANCE_CONVERSION_DISABLED in self.fordoptions and self.fordoptions[DISTANCE_CONVERSION_DISABLED] is True: return self.coordinator.data[self.sensor]["value"] - else: - return round( - float(self.coordinator.data[self.sensor]["value"]) / 1.60934 - ) - else: - return self.coordinator.data[self.sensor]["value"] - else: + return round( + float(self.coordinator.data[self.sensor]["value"]) / 1.60934 + ) return self.coordinator.data[self.sensor]["value"] - elif self.sensor == "fuel": + return self.coordinator.data[self.sensor]["value"] + if self.sensor == "fuel": if self.coordinator.data[self.sensor] is None: return None return round(self.coordinator.data[self.sensor]["fuelLevel"]) - elif self.sensor == "battery": + if self.sensor == "battery": return self.coordinator.data[self.sensor]["batteryHealth"]["value"] - elif self.sensor == "oil": + if self.sensor == "oil": return self.coordinator.data[self.sensor]["oilLife"] - elif self.sensor == "tirePressure": + if self.sensor == "tirePressure": return self.coordinator.data[self.sensor]["value"] - elif self.sensor == "gps": + if self.sensor == "gps": if self.coordinator.data[self.sensor] is None: return "Unsupported" return self.coordinator.data[self.sensor]["gpsState"] - elif self.sensor == "alarm": + if self.sensor == "alarm": return self.coordinator.data[self.sensor]["value"] - elif self.sensor == "ignitionStatus": + if self.sensor == "ignitionStatus": return self.coordinator.data[self.sensor]["value"] - elif self.sensor == "firmwareUpgInProgress": + if self.sensor == "firmwareUpgInProgress": return self.coordinator.data[self.sensor]["value"] - elif self.sensor == "deepSleepInProgress": + if self.sensor == "deepSleepInProgress": return self.coordinator.data[self.sensor]["value"] - elif self.sensor == "doorStatus": + if self.sensor == "doorStatus": for key, value in self.coordinator.data[self.sensor].items(): if value["value"] == "Invalid": continue if value["value"] != "Closed": return "Open" return "Closed" - elif self.sensor == "windowPosition": + if self.sensor == "windowPosition": if self.coordinator.data[self.sensor] is None: return "Unsupported" status = "Closed" @@ -108,26 +113,23 @@ def get_value(self, ftype): if "open" in value["value"].lower(): status = "Open" return status - elif self.sensor == "lastRefresh": + if self.sensor == "lastRefresh": return dt.as_local( datetime.strptime( self.coordinator.data[self.sensor] + "+0000", "%m-%d-%Y %H:%M:%S%z" ) ) - elif self.sensor == "elVeh": + if self.sensor == "elVeh": if self.coordinator.data["elVehDTE"] is not None: if self.fordoptions[CONF_DISTANCE_UNIT] is not None: if self.fordoptions[CONF_DISTANCE_UNIT] == "mi": return round( float(self.coordinator.data["elVehDTE"]["value"]) / 1.60934 ) - else: - return float(self.coordinator.data["elVehDTE"]["value"]) - else: return float(self.coordinator.data["elVehDTE"]["value"]) - else: - return "Unsupported" - elif self.sensor == "zoneLighting": + return float(self.coordinator.data["elVehDTE"]["value"]) + return "Unsupported" + if self.sensor == "zoneLighting": if "zoneLighting" not in self.coordinator.data: return "Unsupported" if ( @@ -136,78 +138,72 @@ def get_value(self, ftype): return self.coordinator.data["zoneLighting"]["activationData"][ "value" ] - else: - return "Unsupported" - elif self.sensor == "remoteStartStatus": - if self.coordinator.data["remoteStartStatus"] is not None: + return "Unsupported" + if self.sensor == "remoteStartStatus": + if self.coordinator.data["remoteStartStatus"] is None: return None - else: - if self.coordinator.data["remoteStartStatus"]["value"] == 1: - return "Active" - else: - return "Inactive" - elif self.sensor == "messages": + if self.coordinator.data["remoteStartStatus"]["value"] == 1: + return "Active" + return "Inactive" + if self.sensor == "messages": if self.coordinator.data["messages"] is None: return None - else: - return len(self.coordinator.data["messages"]) - elif self.sensor == "dieselSystemStatus": + return len(self.coordinator.data["messages"]) + if self.sensor == "dieselSystemStatus": if self.coordinator.data["dieselSystemStatus"]["filterRegenerationStatus"] is not None: return self.coordinator.data["dieselSystemStatus"]["filterRegenerationStatus"] - else: - return "Not Supported" - elif self.sensor == "exhaustFluidLevel": + return "Not Supported" + if self.sensor == "exhaustFluidLevel": if "value" in self.coordinator.data["dieselSystemStatus"]["exhaustFluidLevel"]: return self.coordinator.data["dieselSystemStatus"]["exhaustFluidLevel"]["value"] - else: - return "Not Supported" - elif ftype == "measurement": + return "Not Supported" + return None + if ftype == "measurement": if self.sensor == "odometer": if self.fordoptions[CONF_DISTANCE_UNIT] == "mi": return "mi" - else: - return "km" - elif self.sensor == "fuel": + return "km" + if self.sensor == "fuel": return "%" - elif self.sensor == "battery": + if self.sensor == "battery": return None - elif self.sensor == "oil": + if self.sensor == "oil": return None - elif self.sensor == "tirePressure": + if self.sensor == "tirePressure": return None - elif self.sensor == "gps": + if self.sensor == "gps": return None - elif self.sensor == "alarm": + if self.sensor == "alarm": return None - elif self.sensor == "ignitionStatus": + if self.sensor == "ignitionStatus": return None - elif self.sensor == "firmwareUpgInProgress": + if self.sensor == "firmwareUpgInProgress": return None - elif self.sensor == "deepSleepInProgress": + if self.sensor == "deepSleepInProgress": return None - elif self.sensor == "doorStatus": + if self.sensor == "doorStatus": return None - elif self.sensor == "windowsPosition": + if self.sensor == "windowsPosition": return None - elif self.sensor == "lastRefresh": + if self.sensor == "lastRefresh": return None - elif self.sensor == "zoneLighting": + if self.sensor == "zoneLighting": return None - elif self.sensor == "remoteStartStatus": + if self.sensor == "remoteStartStatus": return None - elif self.sensor == "messages": + if self.sensor == "messages": return "Messages" - elif self.sensor == "elVeh": + if self.sensor == "elVeh": if self.fordoptions[CONF_DISTANCE_UNIT] == "mi": return "mi" - else: - return "km" - elif self.sensor == "exhaustFluidLevel": + return "km" + if self.sensor == "exhaustFluidLevel": return "%" - elif ftype == "attribute": + return None + if ftype == "attribute": if self.sensor == "odometer": return self.coordinator.data[self.sensor].items() - elif self.sensor == "fuel": + if self.sensor == "fuel": if self.coordinator.data[self.sensor] is None: return None if self.fordoptions[CONF_DISTANCE_UNIT] == "mi": @@ -215,25 +211,25 @@ def get_value(self, ftype): float(self.coordinator.data["fuel"]["distanceToEmpty"]) / 1.60934 ) return self.coordinator.data[self.sensor].items() - elif self.sensor == "battery": + if self.sensor == "battery": return { "Battery Voltage": self.coordinator.data[self.sensor][ "batteryStatusActual" ]["value"] } - elif self.sensor == "oil": + if self.sensor == "oil": return self.coordinator.data[self.sensor].items() - elif self.sensor == "tirePressure": + if self.sensor == "tirePressure": if self.coordinator.data["TPMS"] is not None: if self.fordoptions[CONF_PRESSURE_UNIT] == "PSI": sval = 0.1450377377 rval = 1 decimal = 0 - elif self.fordoptions[CONF_PRESSURE_UNIT] == "BAR": + if self.fordoptions[CONF_PRESSURE_UNIT] == "BAR": sval = 0.01 rval = 0.0689475729 decimal = 2 - elif self.fordoptions[CONF_PRESSURE_UNIT] == "kPa": + if self.fordoptions[CONF_PRESSURE_UNIT] == "kPa": sval = 1 rval = 6.8947572932 decimal = 0 @@ -250,27 +246,27 @@ def get_value(self, ftype): tirepress[key] = round(float(value["value"]) * sval, decimal) return tirepress return None - elif self.sensor == "gps": + if self.sensor == "gps": if self.coordinator.data[self.sensor] is None: return None return self.coordinator.data[self.sensor].items() - elif self.sensor == "alarm": + if self.sensor == "alarm": return self.coordinator.data[self.sensor].items() - elif self.sensor == "ignitionStatus": + if self.sensor == "ignitionStatus": return self.coordinator.data[self.sensor].items() - elif self.sensor == "firmwareUpgInProgress": + if self.sensor == "firmwareUpgInProgress": return self.coordinator.data[self.sensor].items() - elif self.sensor == "deepSleepInProgress": + if self.sensor == "deepSleepInProgress": return self.coordinator.data[self.sensor].items() - elif self.sensor == "doorStatus": - doors = dict() + if self.sensor == "doorStatus": + doors = {} for key, value in self.coordinator.data[self.sensor].items(): doors[key] = value["value"] return doors - elif self.sensor == "windowPosition": + if self.sensor == "windowPosition": if self.coordinator.data[self.sensor] is None: return None - windows = dict() + windows = {} for key, value in self.coordinator.data[self.sensor].items(): windows[key] = value["value"] if "open" in value["value"].lower(): @@ -281,81 +277,80 @@ def get_value(self, ftype): elif "closed" in value["value"].lower(): windows[key] = "Closed" return windows - elif self.sensor == "lastRefresh": + if self.sensor == "lastRefresh": return None - elif self.sensor == "elVeh": + if self.sensor == "elVeh": if self.coordinator.data["elVehDTE"] is None: return None - else: - elecs = dict() - if ( - self.coordinator.data["elVehDTE"] is not None and self.coordinator.data["elVehDTE"]["value"] is not None - ): - elecs["elVehDTE"] = self.coordinator.data["elVehDTE"]["value"] - if ( - self.coordinator.data["plugStatus"] is not None and self.coordinator.data["plugStatus"]["value"] is not None - ): - elecs["Plug Status"] = self.coordinator.data["plugStatus"][ - "value" - ] + elecs = {} + if ( + self.coordinator.data["elVehDTE"] is not None and self.coordinator.data["elVehDTE"]["value"] is not None + ): + elecs["elVehDTE"] = self.coordinator.data["elVehDTE"]["value"] + if ( + self.coordinator.data["plugStatus"] is not None and self.coordinator.data["plugStatus"]["value"] is not None + ): + elecs["Plug Status"] = self.coordinator.data["plugStatus"][ + "value" + ] - if ( - self.coordinator.data["chargingStatus"] is not None and self.coordinator.data["chargingStatus"]["value"] is not None - ): - elecs["Charging Status"] = self.coordinator.data[ - "chargingStatus" - ]["value"] + if ( + self.coordinator.data["chargingStatus"] is not None and self.coordinator.data["chargingStatus"]["value"] is not None + ): + elecs["Charging Status"] = self.coordinator.data[ + "chargingStatus" + ]["value"] - if ( - self.coordinator.data["chargeStartTime"] is not None and self.coordinator.data["chargeStartTime"]["value"] is not None - ): - elecs["Charge Start Time"] = self.coordinator.data[ - "chargeStartTime" - ]["value"] + if ( + self.coordinator.data["chargeStartTime"] is not None and self.coordinator.data["chargeStartTime"]["value"] is not None + ): + elecs["Charge Start Time"] = self.coordinator.data[ + "chargeStartTime" + ]["value"] - if ( - self.coordinator.data["chargeEndTime"] is not None and self.coordinator.data["chargeEndTime"]["value"] is not None - ): - elecs["Charge End Time"] = self.coordinator.data[ - "chargeEndTime" - ]["value"] + if ( + self.coordinator.data["chargeEndTime"] is not None and self.coordinator.data["chargeEndTime"]["value"] is not None + ): + elecs["Charge End Time"] = self.coordinator.data[ + "chargeEndTime" + ]["value"] - if ( - self.coordinator.data["batteryFillLevel"] is not None and self.coordinator.data["batteryFillLevel"]["value"] is not None - ): - elecs["Battery Fill Level"] = int(self.coordinator.data[ - "batteryFillLevel" - ]["value"]) + if ( + self.coordinator.data["batteryFillLevel"] is not None and self.coordinator.data["batteryFillLevel"]["value"] is not None + ): + elecs["Battery Fill Level"] = int(self.coordinator.data[ + "batteryFillLevel" + ]["value"]) - if ( - self.coordinator.data["chargerPowertype"] is not None and self.coordinator.data["chargerPowertype"]["value"] is not None - ): - elecs["Charger Power Type"] = self.coordinator.data[ - "chargerPowertype" - ]["value"] + if ( + self.coordinator.data["chargerPowertype"] is not None and self.coordinator.data["chargerPowertype"]["value"] is not None + ): + elecs["Charger Power Type"] = self.coordinator.data[ + "chargerPowertype" + ]["value"] - if ( - self.coordinator.data["batteryChargeStatus"] is not None and self.coordinator.data["batteryChargeStatus"]["value"] is not None - ): - elecs["Battery Charge Status"] = self.coordinator.data[ - "batteryChargeStatus" - ]["value"] + if ( + self.coordinator.data["batteryChargeStatus"] is not None and self.coordinator.data["batteryChargeStatus"]["value"] is not None + ): + elecs["Battery Charge Status"] = self.coordinator.data[ + "batteryChargeStatus" + ]["value"] - if ( - self.coordinator.data["batteryPerfStatus"] is not None and self.coordinator.data["batteryPerfStatus"]["value"] is not None - ): - elecs["Battery Performance Status"] = self.coordinator.data[ - "batteryPerfStatus" - ]["value"] + if ( + self.coordinator.data["batteryPerfStatus"] is not None and self.coordinator.data["batteryPerfStatus"]["value"] is not None + ): + elecs["Battery Performance Status"] = self.coordinator.data[ + "batteryPerfStatus" + ]["value"] - return elecs - elif self.sensor == "zoneLighting": + return elecs + if self.sensor == "zoneLighting": if "zoneLighting" not in self.coordinator.data: return None if ( self.coordinator.data[self.sensor] is not None and self.coordinator.data[self.sensor]["zoneStatusData"] is not None ): - zone = dict() + zone = {} if self.coordinator.data[self.sensor]["zoneStatusData"] is not None: for key, value in self.coordinator.data[self.sensor][ "zoneStatusData" @@ -389,68 +384,73 @@ def get_value(self, ftype): self.sensor ]["zoneLightingShutDownWarning"]["value"] return zone - else: - return None - elif self.sensor == "remoteStartStatus": + return None + if self.sensor == "remoteStartStatus": if self.coordinator.data["remoteStart"] is None: return None - else: - return self.coordinator.data["remoteStart"].items() - elif self.sensor == "messages": + return self.coordinator.data["remoteStart"].items() + if self.sensor == "messages": if self.coordinator.data["messages"] is None: return None - else: - messages = dict() - for value in self.coordinator.data["messages"]: + messages = {} + for value in self.coordinator.data["messages"]: - messages[value["messageSubject"]] = value["createdDate"] - return messages - elif self.sensor == "dieselSystemStatus": + messages[value["messageSubject"]] = value["createdDate"] + return messages + if self.sensor == "dieselSystemStatus": return self.coordinator.data["dieselSystemStatus"] - elif self.sensor == "exhaustFluidLevel": + if self.sensor == "exhaustFluidLevel": return self.coordinator.data["dieselSystemStatus"] + return None + return None @property def name(self): + """Return Sensor Name""" return "fordpass_" + self.sensor @property def state(self): + """Return Sensor State""" return self.get_value("state") @property def device_id(self): + """Return Sensor Device ID""" return self.device_id @property def extra_state_attributes(self): + """Return sensor attributes""" return self.get_value("attribute") @property def native_unit_of_measurement(self): + """Return sensor measurement""" return self.get_value("measurement") @property def icon(self): + """Return sensor icon""" return SENSORS[self.sensor]["icon"] @property def state_class(self): + """Return sensor state_class for statistics""" if "state_class" in SENSORS[self.sensor]: if SENSORS[self.sensor]["state_class"] == "total": return SensorStateClass.TOTAL - elif SENSORS[self.sensor]["state_class"] == "measurement": + if SENSORS[self.sensor]["state_class"] == "measurement": return SensorStateClass.MEASUREMENT - else: - return None - else: return None + return None @property def device_class(self): + """Return sensor device class for statistics""" if "device_class" in SENSORS[self.sensor]: if SENSORS[self.sensor]["device_class"] == "distance": return SensorDeviceClass.DISTANCE if SENSORS[self.sensor]["device_class"] == "timestamp": return SensorDeviceClass.TIMESTAMP - return None + return None From d2d2132b7c41c5ed480dc9b53434a8de40ceb1c4 Mon Sep 17 00:00:00 2001 From: steve Date: Thu, 24 Aug 2023 19:44:09 +1000 Subject: [PATCH 10/17] refactor_complete --- custom_components/fordpass/__init__.py | 4 +- custom_components/fordpass/device_tracker.py | 14 + custom_components/fordpass/fordpass_new.py | 319 ++++++++++--------- custom_components/fordpass/sensor.py | 2 +- custom_components/fordpass/switch.py | 34 +- 5 files changed, 200 insertions(+), 173 deletions(-) diff --git a/custom_components/fordpass/__init__.py b/custom_components/fordpass/__init__.py index 6d7933e..11bc344 100644 --- a/custom_components/fordpass/__init__.py +++ b/custom_components/fordpass/__init__.py @@ -156,7 +156,7 @@ def refresh_status(hass, service, coordinator): """Get latest vehicle status from vehicle, actively polls the car""" _LOGGER.debug("Running Service") vin = service.data.get("vin", "") - status = coordinator.vehicle.requestUpdate(vin) + status = coordinator.vehicle.request_update(vin) if status == 401: _LOGGER.debug("Invalid VIN") elif status == 200: @@ -166,7 +166,7 @@ def refresh_status(hass, service, coordinator): def clear_tokens(hass, service, coordinator): """Clear the token file in config directory, only use in emergency""" _LOGGER.debug("Clearing Tokens") - coordinator.vehicle.clearToken() + coordinator.vehicle.clear_token() async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): diff --git a/custom_components/fordpass/device_tracker.py b/custom_components/fordpass/device_tracker.py index 746b81d..5202358 100644 --- a/custom_components/fordpass/device_tracker.py +++ b/custom_components/fordpass/device_tracker.py @@ -1,3 +1,4 @@ +"""Vehicle Tracker Sensor""" import logging from homeassistant.components.device_tracker import SOURCE_TYPE_GPS @@ -23,6 +24,12 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class CarTracker(FordPassEntity, TrackerEntity): def __init__(self, coordinator, sensor): + super().__init__( + device_id="fordpass_" + sensor, + name="fordpass_" + sensor, + coordinator=coordinator + ) + self._attr = {} self.sensor = sensor self.coordinator = coordinator @@ -32,28 +39,35 @@ def __init__(self, coordinator, sensor): @property def latitude(self): + """Return latitude from Vehicle GPS""" return float(self.coordinator.data[self.sensor]["latitude"]) @property def longitude(self): + """Return longitude from Vehicle GPS""" return float(self.coordinator.data[self.sensor]["longitude"]) @property def source_type(self): + """Set source type to GPS""" return SOURCE_TYPE_GPS @property def name(self): + """Return device tracker entity name""" return "fordpass_tracker" @property def device_id(self): + """Return device tracker id""" return self.device_id @property def extra_state_attributes(self): + """No extra attributes to return""" return None @property def icon(self): + """Return device tracker icon""" return "mdi:radar" diff --git a/custom_components/fordpass/fordpass_new.py b/custom_components/fordpass/fordpass_new.py index ce80426..a027642 100644 --- a/custom_components/fordpass/fordpass_new.py +++ b/custom_components/fordpass/fordpass_new.py @@ -1,14 +1,15 @@ +"""Fordpass API Library""" import hashlib import json import logging import os import random import re -import requests import string import time - from base64 import urlsafe_b64encode +import requests + from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry @@ -31,50 +32,52 @@ "North America & Canada": "71A3AD0A-CF46-4CCF-B473-FC7FE5BC4592", } -baseUrl = "https://usapi.cv.ford.com/api" -guardUrl = "https://api.mps.ford.com/api" -ssoUrl = "https://sso.ci.ford.com" +BASE_URL = "https://usapi.cv.ford.com/api" +GUARD_URL = "https://api.mps.ford.com/api" +SSO_URL = "https://sso.ci.ford.com" session = requests.Session() -class Vehicle(object): +class Vehicle: # Represents a Ford vehicle, with methods for status and issuing commands def __init__( - self, username, password, vin, region, saveToken=False, configLocation="" + self, username, password, vin, region, save_token=False, config_location="" ): self.username = username self.password = password - self.saveToken = saveToken + self.save_token = save_token self.region = region_lookup[region] self.region2 = region self.vin = vin self.token = None self.expires = None - self.expiresAt = None + self.expires_at = None self.refresh_token = None retry = Retry(connect=3, backoff_factor=0.5) adapter = HTTPAdapter(max_retries=retry) session.mount("http://", adapter) session.mount("https://", adapter) - if configLocation == "": + if config_location == "": self.token_location = "custom_components/fordpass/fordpass_token.txt" else: - _LOGGER.debug(configLocation) - self.token_location = configLocation + _LOGGER.debug(config_location) + self.token_location = config_location - def base64UrlEncode(self, data): + def base64_url_encode(self, data): + """Encode string to base64""" return urlsafe_b64encode(data).rstrip(b'=') def generate_hash(self, code): - m = hashlib.sha256() - m.update(code.encode('utf-8')) - return self.base64UrlEncode(m.digest()).decode('utf-8') + """Generate hash for login""" + hashengine = hashlib.sha256() + hashengine.update(code.encode('utf-8')) + return self.base64_url_encode(hashengine.digest()).decode('utf-8') def auth(self): - _LOGGER.debug("New System") """New Authentication System """ + _LOGGER.debug("New System") # Auth Step1 headers = { **defaultHeaders, @@ -82,14 +85,14 @@ def auth(self): } code1 = ''.join(random.choice(string.ascii_lowercase) for i in range(43)) code_verifier = self.generate_hash(code1) - url1 = f"{ssoUrl}/v1.0/endpoint/default/authorize?redirect_uri=fordapp://userauthorized&response_type=code&scope=openid&max_age=3600&client_id=9fb503e0-715b-47e8-adfd-ad4b7770f73b&code_challenge={code_verifier}&code_challenge_method=S256" - r = session.get( + url1 = f"{SSO_URL}/v1.0/endpoint/default/authorize?redirect_uri=fordapp://userauthorized&response_type=code&scope=openid&max_age=3600&client_id=9fb503e0-715b-47e8-adfd-ad4b7770f73b&code_challenge={code_verifier}&code_challenge_method=S256" + response = session.get( url1, headers=headers, ) - test = re.findall('data-ibm-login-url="(.*)"\s', r.text)[0] - nextUrl = ssoUrl + test + test = re.findall('data-ibm-login-url="(.*)"\s', response.text)[0] + next_url = SSO_URL + test # Auth Step2 headers = { @@ -103,17 +106,17 @@ def auth(self): "password": self.password } - r = session.post( - nextUrl, + response = session.post( + next_url, headers=headers, data=data, allow_redirects=False ) - if r.status_code == 302: - nextUrl = r.headers["Location"] + if response.status_code == 302: + next_url = response.headers["Location"] else: - r.raise_for_status() + response.raise_for_status() # Auth Step3 headers = { @@ -121,20 +124,20 @@ def auth(self): 'Content-Type': 'application/json', } - r = session.get( - nextUrl, + response = session.get( + next_url, headers=headers, allow_redirects=False ) - if r.status_code == 302: - nextUrl = r.headers["Location"] - query = requests.utils.urlparse(nextUrl).query + if response.status_code == 302: + next_url = response.headers["Location"] + query = requests.utils.urlparse(next_url).query params = dict(x.split('=') for x in query.split('&')) code = params["code"] grant_id = params["grant_id"] else: - r.raise_for_status() + response.raise_for_status() # Auth Step4 headers = { @@ -151,116 +154,116 @@ def auth(self): "code_verifier": code1 } - r = session.post( - f"{ssoUrl}/oidc/endpoint/default/token", + response = session.post( + f"{SSO_URL}/oidc/endpoint/default/token", headers=headers, data=data ) - if r.status_code == 200: - result = r.json() + if response.status_code == 200: + result = response.json() if result["access_token"]: access_token = result["access_token"] else: - r.raise_for_status() + response.raise_for_status() # Auth Step5 data = {"ciToken": access_token} headers = {**apiHeaders, "Application-Id": self.region} - r = session.post( - f"{guardUrl}/token/v2/cat-with-ci-access-token", + response = session.post( + f"{GUARD_URL}/token/v2/cat-with-ci-access-token", data=json.dumps(data), headers=headers, ) - if r.status_code == 200: - result = r.json() + if response.status_code == 200: + result = response.json() self.token = result["access_token"] self.refresh_token = result["refresh_token"] - self.expiresAt = time.time() + result["expires_in"] - if self.saveToken: + self.expires_at = time.time() + result["expires_in"] + if self.save_token: result["expiry_date"] = time.time() + result["expires_in"] - self.writeToken(result) + self.write_token(result) session.cookies.clear() return True - else: - r.raise_for_status() + response.raise_for_status() + return False - def refreshToken(self, token): - # Token is invalid so let's try refreshing it + def refresh_token_func(self, token): + """Refresh token if still valid""" data = {"refresh_token": token["refresh_token"]} headers = {**apiHeaders, "Application-Id": self.region} - r = session.post( - f"https://{guardUrl}/token/v2/cat-with-refresh-token", + response = session.post( + f"https://{GUARD_URL}/token/v2/cat-with-refresh-token", data=json.dumps(data), headers=headers, ) - if r.status_code == 200: - result = r.json() - if self.saveToken: + if response.status_code == 200: + result = response.json() + if self.save_token: result["expiry_date"] = time.time() + result["expires_in"] - self.writeToken(result) + self.write_token(result) self.token = result["access_token"] self.refresh_token = result["refresh_token"] - self.expiresAt = time.time() + result["expires_in"] - if r.status_code == 401: + self.expires_at = time.time() + result["expires_in"] + if response.status_code == 401: _LOGGER.debug("401 response stage 2: refresh stage 1 token") self.auth() - def __acquireToken(self): + def __acquire_token(self): # Fetch and refresh token as needed # If file exists read in token file and check it's valid - if self.saveToken: + if self.save_token: if os.path.isfile(self.token_location): - data = self.readToken() + data = self.read_token() else: - data = dict() + data = {} data["access_token"] = self.token data["refresh_token"] = self.refresh_token - data["expiry_date"] = self.expiresAt + data["expiry_date"] = self.expires_at else: - data = dict() + data = {} data["access_token"] = self.token data["refresh_token"] = self.refresh_token - data["expiry_date"] = self.expiresAt + data["expiry_date"] = self.expires_at self.token = data["access_token"] - self.expiresAt = data["expiry_date"] - if self.expiresAt: - if time.time() >= self.expiresAt: + self.expires_at = data["expiry_date"] + if self.expires_at: + if time.time() >= self.expires_at: _LOGGER.debug("No token, or has expired, requesting new token") - self.refreshToken(data) + self.refresh_token_func(data) # self.auth() if self.token is None: # No existing token exists so refreshing library self.auth() else: _LOGGER.debug("Token is valid, continuing") - pass - def writeToken(self, token): - # Save token to file to be reused - with open(self.token_location, "w") as outfile: + def write_token(self, token): + """Save token to file for reuse""" + with open(self.token_location, "w", encoding="utf-8") as outfile: token["expiry_date"] = time.time() + token["expires_in"] _LOGGER.debug(token) json.dump(token, outfile) - def readToken(self): - # Get saved token from file + def read_token(self): + """Read saved token from file""" try: - with open(self.token_location) as token_file: + with open(self.token_location, encoding="utf-8") as token_file: token = json.load(token_file) return token except ValueError: _LOGGER.debug("Fixing malformed token") self.auth() - with open(self.token_location) as token_file: + with open(self.token_location, encoding="utf-8") as token_file: token = json.load(token_file) return token - def clearToken(self): + def clear_token(self): + """Clear tokens from config directory""" if os.path.isfile("/tmp/fordpass_token.txt"): os.remove("/tmp/fordpass_token.txt") if os.path.isfile("/tmp/token.txt"): @@ -269,9 +272,9 @@ def clearToken(self): os.remove(self.token_location) def status(self): - # Get the status of the vehicle + """Get Vehicle status from API""" - self.__acquireToken() + self.__acquire_token() params = {"lrdt": "01-01-1970 00:00:00"} @@ -281,62 +284,64 @@ def status(self): "Application-Id": self.region, } - r = session.get( - f"{baseUrl}/vehicles/v5/{self.vin}/status", params=params, headers=headers + response = session.get( + f"{BASE_URL}/vehicles/v5/{self.vin}/status", params=params, headers=headers ) - if r.status_code == 200: - result = r.json() + if response.status_code == 200: + result = response.json() if result["status"] == 402: - r.raise_for_status() + response.raise_for_status() return result["vehiclestatus"] - if r.status_code == 401: + if response.status_code == 401: _LOGGER.debug("401 with status request: start token refresh") - data = dict() + data = {} data["access_token"] = self.token data["refresh_token"] = self.refresh_token - data["expiry_date"] = self.expiresAt - self.refreshToken(data) - self.__acquireToken() + data["expiry_date"] = self.expires_at + self.refresh_token_func(data) + self.__acquire_token() headers = { **apiHeaders, "auth-token": self.token, "Application-Id": self.region, } - r = session.get( - f"{baseUrl}/vehicles/v4/{self.vin}/status", + response = session.get( + f"{BASE_URL}/vehicles/v4/{self.vin}/status", params=params, headers=headers, ) - if r.status_code == 200: - result = r.json() + if response.status_code == 200: + result = response.json() return result["vehiclestatus"] - else: - r.raise_for_status() + response.raise_for_status() + return None def messages(self): - self.__acquireToken() + """Get Vehicle messages from API""" + self.__acquire_token() headers = { **apiHeaders, "Auth-Token": self.token, "Application-Id": self.region, } - r = session.get(f"{guardUrl}/messagecenter/v3/messages?", headers=headers) - if r.status_code == 200: - result = r.json() + response = session.get(f"{GUARD_URL}/messagecenter/v3/messages?", headers=headers) + if response.status_code == 200: + result = response.json() return result["result"]["messages"] # _LOGGER.debug(result) - else: - _LOGGER.debug(r.text) - r.raise_for_status() + _LOGGER.debug(response.text) + response.raise_for_status() + return None def vehicles(self): - self.__acquireToken() + """Get vehicle list from account""" + self.__acquire_token() - if (self.region2 == "Australia"): + if self.region2 == "Australia": countryheader = "AUS" - elif (self.region2 == "North America & Canada"): + elif self.region2 == "North America & Canada": countryheader = "USA" - elif (self.region2 == "UK&Europe"): + elif self.region2 == "UK&Europe": countryheader = "GBR" else: countryheader = "USA" @@ -351,23 +356,23 @@ def vehicles(self): data = { "dashboardRefreshRequest": "All" } - r = session.post( - guardUrl + "/expdashboard/v1/details/", + response = session.post( + f"{GUARD_URL}/expdashboard/v1/details/", headers=headers, data=json.dumps(data) ) - if r.status_code == 207: - result = r.json() + if response.status_code == 207: + result = response.json() _LOGGER.debug(result) return result - else: - _LOGGER.debug(r.text) - r.raise_for_status() + _LOGGER.debug(response.text) + response.raise_for_status() + return None - def guardStatus(self): - # WIP current being tested - self.__acquireToken() + def guard_status(self): + """Retrieve guard status from API""" + self.__acquire_token() params = {"lrdt": "01-01-1970 00:00:00"} @@ -377,81 +382,81 @@ def guardStatus(self): "Application-Id": self.region, } - r = session.get( - f"{guardUrl}/guardmode/v1/{self.vin}/session", + response = session.get( + f"{GUARD_URL}/guardmode/v1/{self.vin}/session", params=params, headers=headers, ) - return r.json() + return response.json() def start(self): """ Issue a start command to the engine """ - return self.__requestAndPoll( - "PUT", f"{baseUrl}/vehicles/v2/{self.vin}/engine/start" + return self.__request_and_poll( + "PUT", f"{BASE_URL}/vehicles/v2/{self.vin}/engine/start" ) def stop(self): """ Issue a stop command to the engine """ - return self.__requestAndPoll( - "DELETE", f"{baseUrl}/vehicles/v2/{self.vin}/engine/start" + return self.__request_and_poll( + "DELETE", f"{BASE_URL}/vehicles/v2/{self.vin}/engine/start" ) def lock(self): """ Issue a lock command to the doors """ - return self.__requestAndPoll( - "PUT", f"{baseUrl}/vehicles/v2/{self.vin}/doors/lock" + return self.__request_and_poll( + "PUT", f"{BASE_URL}/vehicles/v2/{self.vin}/doors/lock" ) def unlock(self): """ Issue an unlock command to the doors """ - return self.__requestAndPoll( - "DELETE", f"{baseUrl}/vehicles/v2/{self.vin}/doors/lock" + return self.__request_and_poll( + "DELETE", f"{BASE_URL}/vehicles/v2/{self.vin}/doors/lock" ) - def enableGuard(self): + def enable_guard(self): """ Enable Guard mode on supported models """ - self.__acquireToken() + self.__acquire_token() - r = self.__makeRequest( - "PUT", f"{guardUrl}/guardmode/v1/{self.vin}/session", None, None + response = self.__make_request( + "PUT", f"{GUARD_URL}/guardmode/v1/{self.vin}/session", None, None ) - _LOGGER.debug(r.text) - return r + _LOGGER.debug(response.text) + return response - def disableGuard(self): + def disable_guard(self): """ Disable Guard mode on supported models """ - self.__acquireToken() - r = self.__makeRequest( - "DELETE", f"{guardUrl}/guardmode/v1/{self.vin}/session", None, None + self.__acquire_token() + response = self.__make_request( + "DELETE", f"{GUARD_URL}/guardmode/v1/{self.vin}/session", None, None ) - _LOGGER.debug(r.text) - return r + _LOGGER.debug(response.text) + return response - def requestUpdate(self, vin=""): - # Send request to refresh data from the cars module - self.__acquireToken() + def request_update(self, vin=""): + """Send request to vehicle for update""" + self.__acquire_token() if vin: vinnum = vin else: vinnum = self.vin - status = self.__makeRequest( - "PUT", f"{baseUrl}/vehicles/v2/{vinnum}/status", None, None + status = self.__make_request( + "PUT", f"{BASE_URL}/vehicles/v2/{vinnum}/status", None, None ) return status.json()["status"] - def __makeRequest(self, method, url, data, params): + def __make_request(self, method, url, data, params): """ Make a request to the given URL, passing data/params as needed """ @@ -466,32 +471,30 @@ def __makeRequest(self, method, url, data, params): url, headers=headers, data=data, params=params ) - def __pollStatus(self, url, id): + def __poll_status(self, url, command_id): """ Poll the given URL with the given command ID until the command is completed """ - status = self.__makeRequest("GET", f"{url}/{id}", None, None) + status = self.__make_request("GET", f"{url}/{command_id}", None, None) result = status.json() if result["status"] == 552: _LOGGER.debug("Command is pending") time.sleep(5) - return self.__pollStatus(url, id) # retry after 5s - elif result["status"] == 200: + return self.__poll_status(url, command_id) # retry after 5s + if result["status"] == 200: _LOGGER.debug("Command completed succesfully") return True - else: - _LOGGER.debug("Command failed") - return False + _LOGGER.debug("Command failed") + return False - def __requestAndPoll(self, method, url): - self.__acquireToken() - command = self.__makeRequest(method, url, None, None) + def __request_and_poll(self, method, url): + """Poll API until status code is reached, locking + remote start""" + self.__acquire_token() + command = self.__make_request(method, url, None, None) if command.status_code == 200: result = command.json() if "commandId" in result: - return self.__pollStatus(url, result["commandId"]) - else: - return False - else: + return self.__poll_status(url, result["commandId"]) return False + return False diff --git a/custom_components/fordpass/sensor.py b/custom_components/fordpass/sensor.py index 16172aa..9850949 100644 --- a/custom_components/fordpass/sensor.py +++ b/custom_components/fordpass/sensor.py @@ -53,7 +53,7 @@ def __init__(self, coordinator, sensor, options): device_id="fordpass_" + sensor, name="fordpass_" + sensor, coordinator=coordinator - ) + ) self.sensor = sensor self.fordoptions = options diff --git a/custom_components/fordpass/switch.py b/custom_components/fordpass/switch.py index 7c6bc7f..84aee9c 100644 --- a/custom_components/fordpass/switch.py +++ b/custom_components/fordpass/switch.py @@ -1,3 +1,4 @@ +"""Fordpass Switch Entities""" import logging from homeassistant.components.switch import SwitchEntity @@ -14,23 +15,29 @@ async def async_setup_entry(hass, config_entry, async_add_entities): # switches = [Switch(entry)] # async_add_entities(switches, False) - for key, value in SWITCHES.items(): - sw = Switch(entry, key, config_entry.options) + for key in SWITCHES: + switch = Switch(entry, key, config_entry.options) # Only add guard entity if supported by the car if key == "guardmode": - if "guardstatus" in sw.coordinator.data: - if sw.coordinator.data["guardstatus"]["returnCode"] == 200: - async_add_entities([sw], False) + if "guardstatus" in switch.coordinator.data: + if switch.coordinator.data["guardstatus"]["returnCode"] == 200: + async_add_entities([switch], False) else: _LOGGER.debug("Guard mode not supported on this vehicle") else: - async_add_entities([sw], False) + async_add_entities([switch], False) class Switch(FordPassEntity, SwitchEntity): """Define the Switch for turning ignition off/on""" def __init__(self, coordinator, switch, options): + """Initialize""" + super().__init__( + device_id="fordpass_doorlock", + name="fordpass_doorlock", + coordinator=coordinator, + ) self._device_id = "fordpass_" + switch self.switch = switch @@ -39,6 +46,7 @@ def __init__(self, coordinator, switch, options): self.coordinator_context = object() async def async_turn_on(self, **kwargs): + """Send request to vehicle on switch status on""" if self.switch == "ignition": await self.coordinator.hass.async_add_executor_job( self.coordinator.vehicle.start @@ -51,6 +59,7 @@ async def async_turn_on(self, **kwargs): await self.coordinator.async_request_refresh() async def async_turn_off(self, **kwargs): + """Send request to vehicle on switch status off""" if self.switch == "ignition": await self.coordinator.hass.async_add_executor_job( self.coordinator.vehicle.stop @@ -64,22 +73,24 @@ async def async_turn_off(self, **kwargs): @property def name(self): + """return switch name""" return "fordpass_" + self.switch + "_Switch" @property def device_id(self): + """return switch device id""" return self.device_id @property def is_on(self): + """Check status of switch""" if self.switch == "ignition": - """Determine if the vehicle is started.""" if ( self.coordinator.data is None or self.coordinator.data["remoteStartStatus"] is None ): return None return self.coordinator.data["remoteStartStatus"]["value"] - elif self.switch == "guardmode": + if self.switch == "guardmode": # Need to find the correct response for enabled vs disabled so this may be spotty at the moment guardstatus = self.coordinator.data["guardstatus"] @@ -88,13 +99,12 @@ def is_on(self): if "gmStatus" in guardstatus: if guardstatus["session"]["gmStatus"] == "enable": return True - else: - return False - else: return False - else: return False + return False + return False @property def icon(self): + """Return icon for switch""" return SWITCHES[self.switch]["icon"] From 8bb61ddaaeb6ca120a13b7acc739751da6104dd8 Mon Sep 17 00:00:00 2001 From: steve Date: Thu, 24 Aug 2023 19:49:43 +1000 Subject: [PATCH 11/17] updated ci --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index cada14e..dbf45d9 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -21,4 +21,4 @@ static_analysis: allow_failure: true script: - flake8 --max-line-length=280 --ignore=W605 - - pylint . --recursive=y --function-naming-style=camelCase --method-naming-style=camelCase --disable=W0613,C0115 \ No newline at end of file + - pylint . --recursive=y --disable=W0613,C0115,E0401,R0911,R0912,R0913,R0915,R0903,W0201,R1702,W1401,R0902,R0914 --max-line-length=280 \ No newline at end of file From f2cbee47be1eb8024f68d4267e408c373f74cedb Mon Sep 17 00:00:00 2001 From: steve Date: Thu, 24 Aug 2023 20:00:52 +1000 Subject: [PATCH 12/17] Fix guard url --- custom_components/fordpass/fordpass_new.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/fordpass/fordpass_new.py b/custom_components/fordpass/fordpass_new.py index a027642..5df8622 100644 --- a/custom_components/fordpass/fordpass_new.py +++ b/custom_components/fordpass/fordpass_new.py @@ -197,7 +197,7 @@ def refresh_token_func(self, token): headers = {**apiHeaders, "Application-Id": self.region} response = session.post( - f"https://{GUARD_URL}/token/v2/cat-with-refresh-token", + f"{GUARD_URL}/token/v2/cat-with-refresh-token", data=json.dumps(data), headers=headers, ) From be6b9d128f60621b9890281e39cd268a8349999f Mon Sep 17 00:00:00 2001 From: steve Date: Thu, 24 Aug 2023 20:17:04 +1000 Subject: [PATCH 13/17] added strings --- custom_components/fordpass/strings.json | 7 +++++++ custom_components/fordpass/translations/en.json | 9 ++++++++- custom_components/fordpass/translations/fr.json | 9 ++++++++- 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/custom_components/fordpass/strings.json b/custom_components/fordpass/strings.json index 8e053e7..828ab0b 100644 --- a/custom_components/fordpass/strings.json +++ b/custom_components/fordpass/strings.json @@ -11,6 +11,13 @@ } } }, + "vehicle": { + "title": "Select vehicle to add", + "description": "Select the vehicle to add", + "data": { + "vin": "VIN" + } + }, "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", diff --git a/custom_components/fordpass/translations/en.json b/custom_components/fordpass/translations/en.json index c72a057..69c0e3a 100644 --- a/custom_components/fordpass/translations/en.json +++ b/custom_components/fordpass/translations/en.json @@ -18,7 +18,14 @@ "vin": "VIN", "region" : "FordPass Region" } - } + }, + "vehicle": { + "title": "Select vehicle to add", + "description": "Only vehicles not currently added will be shown", + "data": { + "vin": "VIN" + } + } } }, "options": { diff --git a/custom_components/fordpass/translations/fr.json b/custom_components/fordpass/translations/fr.json index b6f79a7..0e15efd 100644 --- a/custom_components/fordpass/translations/fr.json +++ b/custom_components/fordpass/translations/fr.json @@ -18,7 +18,14 @@ "vin": "NIV", "region" : "Région FordPass" } - } + }, + "vehicle": { + "title": "Sélectionnez le véhicule à ajouter", + "description": "Seuls les véhicules non ajoutés actuellement seront affichés", + "data": { + "vin": "VIN" + } + } } }, "options": { From c64a410dff86251906af08c2af4bfe2ee5565ca1 Mon Sep 17 00:00:00 2001 From: steve Date: Thu, 24 Aug 2023 20:42:51 +1000 Subject: [PATCH 14/17] Added new login --- custom_components/fordpass/__init__.py | 29 ++++++----- custom_components/fordpass/config_flow.py | 51 +++++++++++++++++-- .../fordpass/translations/de.json | 9 +++- .../fordpass/translations/it.json | 9 +++- .../fordpass/translations/nl.json | 9 +++- 5 files changed, 86 insertions(+), 21 deletions(-) diff --git a/custom_components/fordpass/__init__.py b/custom_components/fordpass/__init__.py index 64016d1..41eaa72 100644 --- a/custom_components/fordpass/__init__.py +++ b/custom_components/fordpass/__init__.py @@ -161,7 +161,6 @@ def refresh_status(hass, service, coordinator): _LOGGER.debug("Invalid VIN") elif status == 200: _LOGGER.debug("Refresh Sent") - def clear_tokens(hass, service, coordinator): @@ -170,21 +169,13 @@ def clear_tokens(hass, service, coordinator): coordinator.vehicle.clear_token() -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, component) - for component in PLATFORMS - ] - ) - ) - hass.data[DOMAIN][entry.entry_id]["fordpass_options_listener"]() - if unload_ok: - hass.data[DOMAIN].pop(entry.entry_id) - return unload_ok + if await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + hass.data[DOMAIN].pop(entry.entry_id) + return True + return False class FordPassDataUpdateCoordinator(DataUpdateCoordinator): @@ -221,6 +212,9 @@ async def _async_update_data(self): data["messages"] = await self._hass.async_add_executor_job( self.vehicle.messages ) + data["vehicles"] = await self._hass.async_add_executor_job( + self.vehicle.vehicles + ) _LOGGER.debug(data) # If data has now been fetched but was previously unavailable, log and reset if not self._available: @@ -264,8 +258,15 @@ def device_info(self): if self._device_id is None: return None + model = "unknown" + if self.coordinator.data["vehicles"] is not None: + for vehicle in self.coordinator.data["vehicles"]["vehicleProfile"]: + if vehicle["VIN"] == self.coordinator.vin: + model = f"{vehicle['year']} {vehicle['model']}" + return { "identifiers": {(DOMAIN, self.coordinator.vin)}, "name": f"{VEHICLE} ({self.coordinator.vin})", + "model": f"{model}", "manufacturer": MANUFACTURER, } diff --git a/custom_components/fordpass/config_flow.py b/custom_components/fordpass/config_flow.py index 47a8ef6..17e3cd4 100644 --- a/custom_components/fordpass/config_flow.py +++ b/custom_components/fordpass/config_flow.py @@ -30,12 +30,20 @@ { vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str, - vol.Required(VIN): vol.All(str, vol.Length(min=17, max=17)), vol.Required(REGION): vol.In(REGION_OPTIONS), } ) +@callback +def configured_vehicles(hass): + """Return a list of configured vehicles""" + return { + entry.data[VIN] + for entry in hass.config_entries.async_entries(DOMAIN) + } + + async def validate_input(hass: core.HomeAssistant, data): """Validate the user input allows us to connect. @@ -43,19 +51,22 @@ async def validate_input(hass: core.HomeAssistant, data): """ _LOGGER.debug(data[REGION]) config_path = hass.config.path("custom_components/fordpass/" + data[CONF_USERNAME] + "_fordpass_token.txt") - vehicle = Vehicle(data[CONF_USERNAME], data[CONF_PASSWORD], data[VIN], data[REGION], 1, config_path) + vehicle = Vehicle(data[CONF_USERNAME], data[CONF_PASSWORD], "", data[REGION], 1, config_path) try: result = await hass.async_add_executor_job(vehicle.auth) except Exception as ex: raise InvalidAuth from ex + if result: + vehicles = await(hass.async_add_executor_job(vehicle.vehicles)) + if not result: _LOGGER.error("Failed to authenticate with fordpass") raise CannotConnect # Return info that you want to store in the config entry. - return {"title": f"Vehicle ({data[VIN]})"} + return vehicles class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @@ -70,7 +81,10 @@ async def async_step_user(self, user_input=None): if user_input is not None: try: info = await validate_input(self.hass, user_input) - return self.async_create_entry(title=info["title"], data=user_input) + self.login_input = user_input + self.vehicles = info["userVehicles"]["vehicleDetails"] + return await self.async_step_vehicle() + # return self.async_create_entry(title=info["title"], data=user_input) except CannotConnect: print("EXCEPT") errors["base"] = "cannot_connect" @@ -86,6 +100,35 @@ async def async_step_user(self, user_input=None): step_id="user", data_schema=DATA_SCHEMA, errors=errors ) + async def async_step_vehicle(self, user_input=None): + """Show user vehicle selection form""" + if user_input is not None: + _LOGGER.debug("Checking Vehicle is accessible") + self.login_input[VIN] = user_input["vin"] + _LOGGER.debug(self.login_input) + return self.async_create_entry(title=f"Vehicle ({user_input[VIN]})", data=self.login_input) + + _LOGGER.debug(self.vehicles) + + configured = configured_vehicles(self.hass) + _LOGGER.debug(configured) + avaliable_vehicles = {} + for vehicle in self.vehicles: + _LOGGER.debug(vehicle) + if vehicle["VIN"] not in configured: + avaliable_vehicles[vehicle["VIN"]] = vehicle["nickName"] + f" ({vehicle['VIN']})" + + if not avaliable_vehicles: + _LOGGER.debug("No Vehicles?") + return self.async_abort(reason="no_vehicles") + return self.async_show_form( + step_id="vehicle", + data_schema=vol.Schema( + {vol.Required(VIN): vol.In(avaliable_vehicles)} + ), + errors={} + ) + @staticmethod @callback def async_get_options_flow(config_entry): diff --git a/custom_components/fordpass/translations/de.json b/custom_components/fordpass/translations/de.json index 519119e..6c12ed5 100644 --- a/custom_components/fordpass/translations/de.json +++ b/custom_components/fordpass/translations/de.json @@ -17,7 +17,14 @@ "vin": "FIN", "region" : "FordPass Region" } - } + }, + "vehicle": { + "title": "Selecteer voertuig om toe te voegen", + "description": "Alleen voertuigen die momenteel niet zijn toegevoegd, worden weergegeven", + "data": { + "vin": "VIN" + } + } } }, "options": { diff --git a/custom_components/fordpass/translations/it.json b/custom_components/fordpass/translations/it.json index 4cc2ad6..3623844 100644 --- a/custom_components/fordpass/translations/it.json +++ b/custom_components/fordpass/translations/it.json @@ -17,7 +17,14 @@ "vin": "VIN", "region" : "FordPass Regione" } - } + }, + "vehicle": { + "title": "Seleziona il veicolo da aggiungere", + "description": "Verranno visualizzati solo i veicoli attualmente non aggiunti", + "data": { + "vin": "VIN" + } + } } }, "options": { diff --git a/custom_components/fordpass/translations/nl.json b/custom_components/fordpass/translations/nl.json index 7490075..3336fc6 100644 --- a/custom_components/fordpass/translations/nl.json +++ b/custom_components/fordpass/translations/nl.json @@ -18,7 +18,14 @@ "vin": "VIN", "region" : "FordPass regio" } - } + }, + "vehicle": { + "title": "Velg kjøretøy som skal legges til", + "description": "Kun kjøretøy som ikke er lagt til for øyeblikket, vises", + "data": { + "vin": "VIN" + } + } } }, "options": { From 8ea04294db610acae355782b574ec3e315603abc Mon Sep 17 00:00:00 2001 From: steve Date: Thu, 24 Aug 2023 20:47:55 +1000 Subject: [PATCH 15/17] Fix CI False pos --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index dbf45d9..68259bd 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -20,5 +20,5 @@ static_analysis: tags: ["dev","python"] allow_failure: true script: - - flake8 --max-line-length=280 --ignore=W605 + - flake8 --max-line-length=280 --ignore=W605,E275 - pylint . --recursive=y --disable=W0613,C0115,E0401,R0911,R0912,R0913,R0915,R0903,W0201,R1702,W1401,R0902,R0914 --max-line-length=280 \ No newline at end of file From 386ca417e704ae21d5329ae015b2776e1bd645c5 Mon Sep 17 00:00:00 2001 From: steve Date: Thu, 24 Aug 2023 20:53:02 +1000 Subject: [PATCH 16/17] Added info.md and manifest --- README.md | 5 ++++- custom_components/fordpass/manifest.json | 2 +- info.md | 5 +++++ 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f4ced0b..97733d2 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,9 @@ - https://github.com/JacobWasFramed - Updated unit conversions - https://github.com/heehoo59 - French Translation +## 1.50 Change +As of 1.50 VIN number is no longer required for setup. Integration should display a list of vehicles associated with your Fordpass account + ## 1.47 Change If you are experiencing issues with the odometer displaying wrong, please try enabling the checkbox in options for "Disable Distance Conversion" @@ -20,7 +23,7 @@ If you are experiencing issues with the odometer displaying wrong, please try en Use HACS and add as a custom repo. Once the integration is installed go to your integrations and follow the configuration options to specify the below: - Username (Fordpass App) - Password (Fordpass App) -- VIN Number +- VIN Number (Not required in 1.50) - Region (Where you are based, required for tokens to work correctly) ## Usage diff --git a/custom_components/fordpass/manifest.json b/custom_components/fordpass/manifest.json index a005fd5..f6202a2 100644 --- a/custom_components/fordpass/manifest.json +++ b/custom_components/fordpass/manifest.json @@ -12,6 +12,6 @@ "loggers": ["custom_components.fordpass"], "requirements": [], "ssdp": [], - "version": "0.1.48", + "version": "0.1.50", "zeroconf": [] } \ No newline at end of file diff --git a/info.md b/info.md index af6e1d1..da812dc 100644 --- a/info.md +++ b/info.md @@ -1,4 +1,9 @@ ## **Changelog** +### Version 1.50 +- Complete refactor of all code to make it more compliant +- Added new config flow to allow for choosing vehicles on setup instead of using VIN +### Version 1.49 +- Added German & Italian translations (@@lollo0296) ### Version 1.48 - Add translations for service strings - Fix error on odometer missing config From 2a82fa89de943df0a0397cf46d46f27b8d09550f Mon Sep 17 00:00:00 2001 From: steve Date: Thu, 24 Aug 2023 21:15:40 +1000 Subject: [PATCH 17/17] Fix string error --- custom_components/fordpass/strings.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/custom_components/fordpass/strings.json b/custom_components/fordpass/strings.json index 828ab0b..2144ecc 100644 --- a/custom_components/fordpass/strings.json +++ b/custom_components/fordpass/strings.json @@ -9,14 +9,14 @@ "password": "FordPass Password", "region": "FordPass Region" } - } - }, - "vehicle": { + }, + "vehicle": { "title": "Select vehicle to add", "description": "Select the vehicle to add", "data": { "vin": "VIN" } + } }, "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",