diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..68259bd --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,24 @@ +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 flake8 + - pip install pylint + - pip install bandit + +stages: + - Static Analysis + + + + +static_analysis: + stage: Static Analysis + tags: ["dev","python"] + allow_failure: true + script: + - 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 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/__init__.py b/custom_components/fordpass/__init__.py index 6bed789..41eaa72 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, {}) @@ -55,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] @@ -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") @@ -136,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,54 +145,48 @@ 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): + """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) + status = coordinator.vehicle.request_update(vin) if status == 401: _LOGGER.debug("Invalid VIN") elif status == 200: _LOGGER.debug("Refresh Sent") - 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): +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): """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__( @@ -219,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: @@ -262,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 66d6292..17e3cd4 100644 --- a/custom_components/fordpass/config_flow.py +++ b/custom_components/fordpass/config_flow.py @@ -30,40 +30,43 @@ { 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. 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[REGION], 1, config_path) try: result = await hass.async_add_executor_job(vehicle.auth) 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 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): @@ -78,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" @@ -94,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): @@ -107,6 +142,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 = { @@ -124,7 +160,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/device_tracker.py b/custom_components/fordpass/device_tracker.py index 3985f8f..5202358 100644 --- a/custom_components/fordpass/device_tracker.py +++ b/custom_components/fordpass/device_tracker.py @@ -1,5 +1,5 @@ +"""Vehicle Tracker Sensor""" 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 +15,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") @@ -24,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 @@ -33,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 2c1dfc2..5df8622 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 base64 import urlsafe_b64encode, urlsafe_b64decode from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry @@ -31,51 +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, @@ -83,15 +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 = { @@ -101,22 +102,21 @@ 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, + response = session.post( + next_url, headers=headers, - data = data, + 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 = { @@ -124,25 +124,22 @@ def auth(self): 'Content-Type': 'application/json', } - r = session.get( - nextUrl, - headers = headers, + 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 + # Auth Step4 headers = { **defaultHeaders, "Content-Type": "application/x-www-form-urlencoded", @@ -155,120 +152,118 @@ 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 + 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"{guardUrl}/token/v2/cat-with-refresh-token", + response = session.post( + f"{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 == None: + 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"): @@ -277,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"} @@ -289,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" @@ -356,27 +353,26 @@ def vehicles(self): "Locale": "EN-US" } - data = { - "dashboardRefreshRequest":"All" + "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"} @@ -386,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 """ @@ -475,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/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/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/custom_components/fordpass/sensor.py b/custom_components/fordpass/sensor.py index ff36f3d..9850949 100644 --- a/custom_components/fordpass/sensor.py +++ b/custom_components/fordpass/sensor.py @@ -1,8 +1,8 @@ +"""All vehicle sensors from the accessible by the API""" 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, @@ -21,14 +21,14 @@ 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": 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,12 +42,19 @@ async def async_setup_entry(hass, config_entry, async_add_entities): sensors.append(sensor) async_add_entities(sensors, True) + class CarSensor( FordPassEntity, SensorEntity, ): 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,187 +64,172 @@ 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] != 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 round( + float(self.coordinator.data[self.sensor]["value"]) / 1.60934 + ) return self.coordinator.data[self.sensor]["value"] - elif self.sensor == "fuel": - if self.coordinator.data[self.sensor] == None: + 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.coordinator.data[self.sensor] == None: + 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.coordinator.data[self.sensor] == None: + if self.sensor == "windowPosition": + if self.coordinator.data[self.sensor] is None: return "Unsupported" status = "Closed" for key, value in self.coordinator.data[self.sensor].items(): 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.coordinator.data["elVehDTE"] != None: - if self.fordoptions[CONF_DISTANCE_UNIT] != None: + 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 + 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 ( - 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" ] - else: - return "Unsupported" - elif self.sensor == "remoteStartStatus": - if self.coordinator.data["remoteStartStatus"] == 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["messages"] == None: + 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": - if self.coordinator.data["dieselSystemStatus"]["filterRegenerationStatus"] != None: + 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.coordinator.data[self.sensor] == None: + if self.sensor == "fuel": + 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": + 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.coordinator.data["TPMS"] != None: + 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 @@ -254,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.coordinator.data[self.sensor] == None: + 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.coordinator.data[self.sensor] == None: + 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(): @@ -285,93 +277,81 @@ 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.coordinator.data["elVehDTE"] == None: + if self.sensor == "elVeh": + 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 - ): - elecs["elVehDTE"] = self.coordinator.data["elVehDTE"]["value"] - if ( - self.coordinator.data["plugStatus"] != None - and self.coordinator.data["plugStatus"]["value"] != 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"] != None - and self.coordinator.data["chargingStatus"]["value"] != 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"] != None - and self.coordinator.data["chargeStartTime"]["value"] != 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"] != None - and self.coordinator.data["chargeEndTime"]["value"] != 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"] != None - and self.coordinator.data["batteryFillLevel"]["value"] != 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"] != None - and self.coordinator.data["chargerPowertype"]["value"] != 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"] != None - and self.coordinator.data["batteryChargeStatus"]["value"] - != 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"] != None - and self.coordinator.data["batteryPerfStatus"]["value"] != 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] != 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: + zone = {} + if self.coordinator.data[self.sensor]["zoneStatusData"] is not None: for key, value in self.coordinator.data[self.sensor][ "zoneStatusData" ].items(): @@ -379,7 +359,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 +369,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,74 +378,79 @@ def get_value(self, ftype): self.coordinator.data[self.sensor][ "zoneLightingShutDownWarning" ] - != None + is not None ): zone["zoneLightingShutDownWarning"] = self.coordinator.data[ self.sensor ]["zoneLightingShutDownWarning"]["value"] return zone - else: - return None - elif self.sensor == "remoteStartStatus": - if self.coordinator.data["remoteStart"] == None: + 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": - if self.coordinator.data["messages"] == None: + 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 diff --git a/custom_components/fordpass/strings.json b/custom_components/fordpass/strings.json index 8e053e7..2144ecc 100644 --- a/custom_components/fordpass/strings.json +++ b/custom_components/fordpass/strings.json @@ -9,6 +9,13 @@ "password": "FordPass Password", "region": "FordPass Region" } + }, + "vehicle": { + "title": "Select vehicle to add", + "description": "Select the vehicle to add", + "data": { + "vin": "VIN" + } } }, "error": { diff --git a/custom_components/fordpass/switch.py b/custom_components/fordpass/switch.py index 78e6a21..f8ded13 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 @@ -52,6 +60,7 @@ async def async_turn_on(self, **kwargs): self.async_write_ha_state() 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 @@ -66,23 +75,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 + 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"] @@ -91,13 +101,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"] 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/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": { 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": { 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