diff --git a/README.md b/README.md index 7692b9b..cd250d8 100644 --- a/README.md +++ b/README.md @@ -12,9 +12,6 @@ - https://github.com/JacobWasFramed - Updated unit conversions - https://github.com/heehoo59 - French Translation -## As of 10/10/2023 Ford has switched to a new API! -This has caused the integration to stop working, I have started work on integrating the new API and there is a Beta but there is a lot of changes so it will take time to get it back and 100% operational again. - ## Account Warning (Sep 2023) A number of users have encountered their accounts being banned for containing "+" symbols in their email. It appears Ford thinks this is a disposable email. So if you have a + in your email I recommend changing it. diff --git a/custom_components/fordpass/autonomicData.py b/custom_components/fordpass/autonomicData.py new file mode 100644 index 0000000..ad83344 --- /dev/null +++ b/custom_components/fordpass/autonomicData.py @@ -0,0 +1,142 @@ +import json +import requests +import sys +import os +import re +from datetime import datetime + + +# Place this script in the /config/custom_components/fordpass folder on your HomeAssistant +# Add the details below +# run from a terminal in the /config/custom_components/fordpass folder: python3 autonomicData.py +# It will create username_status_timestamp.json in the same folder +# Script will automatically redact your VIN, VehicleID, and Geolocation details (lat, long) + +#GitHub username to append to the filename +gitHub_username = "" +#FordPass VIN for vehicle to get data from +fp_vin = "" +#Name of the file for the user_fordpass_token.txt from the fordpass-ha integration +fp_token = "_fordpass_token.txt" + +def get_autonomic_token(ford_access_token): + url = "https://accounts.autonomic.ai/v1/auth/oidc/token" + headers = { + "accept": "*/*", + "content-type": "application/x-www-form-urlencoded" + } + data = { + "subject_token": ford_access_token, + "subject_issuer": "fordpass", + "client_id": "fordpass-prod", + "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange", + "subject_token_type": "urn:ietf:params:oauth:token-type:jwt" + } + + try: + response = requests.post(url, headers=headers, data=data) + response.raise_for_status() + autonomic_token_data = response.json() + return autonomic_token_data + + except requests.exceptions.HTTPError as errh: + print(f"HTTP Error: {errh}") + print(f"Trying refresh token") + get_autonomic_token(ford_refresh_token) + except requests.exceptions.ConnectionError as errc: + print(f"Error Connecting: {errc}") + sys.exit() + except requests.exceptions.Timeout as errt: + print(f"Timeout Error: {errt}") + sys.exit() + except requests.exceptions.RequestException as err: + print(f"Something went wrong: {err}") + sys.exit() + + +def get_vehicle_status(vin, access_token): + BASE_URL = "https://api.autonomic.ai/" + endpoint = f"v1beta/telemetry/sources/fordpass/vehicles/{vin}:query" + url = f"{BASE_URL}{endpoint}" + headers = { + "Authorization": f"Bearer {access_token}", # Replace 'your_autonom_token' with the actual Autonomic API token + "Content-Type": "application/json", + "accept": "*/*" + } + redactionItems = ["lat", "lon", "vehicleId", "vin", "latitude", "longitude"] + + try: + response = requests.post(url, headers=headers, json={}) + response.raise_for_status() # Raise HTTPError for bad requests (4xx and 5xx status codes) + + # Parse the JSON response + vehicle_status_data = response.json() + + # Redact sensitive information + redact_json(vehicle_status_data, redactionItems) + return vehicle_status_data + + except requests.exceptions.HTTPError as errh: + print(f"HTTP Error: {errh}") + except requests.exceptions.ConnectionError as errc: + print(f"Error Connecting: {errc}") + except requests.exceptions.Timeout as errt: + print(f"Timeout Error: {errt}") + except requests.exceptions.RequestException as err: + print(f"Something went wrong: {err}") + +def redact_json(data, redaction): + # Regular expression to match GPS coordinates + gps_pattern = r'"gpsDegree":\s*-?\d+\.\d+,\s*"gpsFraction":\s*-?\d+\.\d+,\s*"gpsSign":\s*-?\d+\.\d+' + + if isinstance(data, dict): + for key in list(data.keys()): + if key in redaction: + data[key] = 'REDACTED' + else: + if isinstance(data[key], str): + # Redact GPS coordinates in string values + data[key] = re.sub(gps_pattern, '"gpsDegree": "REDACTED", "gpsFraction": "REDACTED", "gpsSign": "REDACTED"', data[key]) + else: + redact_json(data[key], redaction) + # Special handling for 'stringArrayValue' + if key == 'stringArrayValue': + for i in range(len(data[key])): + data[key][i] = re.sub(gps_pattern, '"gpsDegree": "REDACTED", "gpsFraction": "REDACTED", "gpsSign": "REDACTED"', data[key][i]) + elif isinstance(data, list): + for item in data: + redact_json(item, redaction) + + +if __name__ == "__main__": + workingDir = "/config/custom_components/fordpass" + if gitHub_username == "": + gitHub_username = 'my' + if fp_vin == "": + print("Please enter your VIN into the python script") + sys.exit() + if fp_token == "": + print("Please enter your FordPass token text file name into the python script") + sys.exit() + elif os.path.isfile(os.path.join(workingDir, fp_token)) == False: + print(f"Error finding FordPass token text file: {os.path.join(workingDir, fp_token)}") + sys.exit() + + fp_token = os.path.join(workingDir, fp_token) + # Get FordPass token + with open(fp_token, 'r') as file: + fp_token_data = json.load(file) + + ford_access_token = fp_token_data['access_token'] + ford_refresh_token = fp_token_data['refresh_token'] + # Exchange Fordpass token for Autonomic Token + autonomic_token = get_autonomic_token(ford_access_token) + vehicle_status = get_vehicle_status(fp_vin, autonomic_token["access_token"]) + + current_datetime = datetime.now().strftime("%Y-%m-%d_%H:%M:%S") + fileName = os.path.join(workingDir, f"{gitHub_username}_status_{current_datetime}.json") + + # Write the updated JSON data to the file + with open(fileName, 'w') as file: + json.dump(vehicle_status, file, indent=4) + print("done") \ No newline at end of file diff --git a/custom_components/fordpass/const.py b/custom_components/fordpass/const.py index b8ae300..40a3b45 100644 --- a/custom_components/fordpass/const.py +++ b/custom_components/fordpass/const.py @@ -42,19 +42,21 @@ "windowPosition": {"icon": "mdi:car-door"}, "lastRefresh": {"icon": "mdi:clock", "device_class": "timestamp"}, "elVeh": {"icon": "mdi:ev-station"}, - "deepSleepInProgress": { - "icon": "mdi:power-sleep", - "name": "Deep Sleep Mode Active", - }, - "firmwareUpgInProgress": { - "icon": "mdi:one-up", - "name": "Firmware Update In Progress", - }, + "elVehCharging": {"icon": "mdi:ev-station"}, + "speed": {"icon": "mdi:speedometer"}, + # "deepSleepInProgress": { + # "icon": "mdi:power-sleep", + # "name": "Deep Sleep Mode Active", + # }, + # "firmwareUpgInProgress": { + # "icon": "mdi:one-up", + # "name": "Firmware Update In Progress", + # }, "remoteStartStatus": {"icon": "mdi:remote"}, - "zoneLighting": {"icon": "mdi:spotlight-beam"}, + # "zoneLighting": {"icon": "mdi:spotlight-beam"}, "messages": {"icon": "mdi:message-text"}, - "dieselSystemStatus": {"icon": "mdi:smoking-pipe"}, - "exhaustFluidLevel": {"icon": "mdi:barrel"} + # "dieselSystemStatus": {"icon": "mdi:smoking-pipe"}, + # "exhaustFluidLevel": {"icon": "mdi:barrel"} } SWITCHES = {"ignition": {"icon": "hass:power"}, "guardmode": {"icon": "mdi:shield-key"}} @@ -72,4 +74,7 @@ }, } -SWITCHES = {"ignition": {"icon": "hass:power"}, "guardmode": {"icon": "mdi:shield-key"}} +SWITCHES = { + "ignition": {"icon": "hass:power"}, + # "guardmode": {"icon": "mdi:shield-key"} +} diff --git a/custom_components/fordpass/device_tracker.py b/custom_components/fordpass/device_tracker.py index 5202358..348661b 100644 --- a/custom_components/fordpass/device_tracker.py +++ b/custom_components/fordpass/device_tracker.py @@ -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"] is not None: + if entry.data["metrics"]["position"] is not None: async_add_entities([CarTracker(entry, "gps")], True) else: _LOGGER.debug("Vehicle does not support GPS") @@ -24,28 +24,23 @@ 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 + self.data = coordinator.data["metrics"] self._device_id = "fordpass_tracker" # Required for HA 2022.7 self.coordinator_context = object() @property def latitude(self): - """Return latitude from Vehicle GPS""" - return float(self.coordinator.data[self.sensor]["latitude"]) + """Return latitude""" + return float(self.coordinator.data["metrics"]["position"]["value"]["location"]["lat"]) @property def longitude(self): - """Return longitude from Vehicle GPS""" - return float(self.coordinator.data[self.sensor]["longitude"]) + """Return longtitude""" + return float(self.coordinator.data["metrics"]["position"]["value"]["location"]["lon"]) @property def source_type(self): diff --git a/custom_components/fordpass/fordpass_new.py b/custom_components/fordpass/fordpass_new.py index 22c3031..c63e736 100644 --- a/custom_components/fordpass/fordpass_new.py +++ b/custom_components/fordpass/fordpass_new.py @@ -32,9 +32,13 @@ "North America & Canada": "71A3AD0A-CF46-4CCF-B473-FC7FE5BC4592", } +NEW_API = True + BASE_URL = "https://usapi.cv.ford.com/api" GUARD_URL = "https://api.mps.ford.com/api" SSO_URL = "https://sso.ci.ford.com" +AUTONOMIC_URL = "https://api.autonomic.ai/v1" +AUTONOMIC_ACCOUNT_URL = "https://accounts.autonomic.ai/v1" session = requests.Session() @@ -55,6 +59,8 @@ def __init__( self.expires = None self.expires_at = None self.refresh_token = None + self.auto_token = None + self.auto_expires_at = None retry = Retry(connect=3, backoff_factor=0.5) adapter = HTTPAdapter(max_retries=retry) session.mount("http://", adapter) @@ -183,8 +189,15 @@ def auth(self): self.token = result["access_token"] self.refresh_token = result["refresh_token"] self.expires_at = time.time() + result["expires_in"] + auto_token = self.get_auto_token() + self.auto_token = auto_token["access_token"] + self.auto_expires_at = time.time() + result["expires_in"] if self.save_token: result["expiry_date"] = time.time() + result["expires_in"] + result["auto_token"] = auto_token["access_token"] + result["auto_refresh"] = auto_token["refresh_token"] + result["auto_expiry"] = time.time() + auto_token["expires_in"] + self.write_token(result) session.cookies.clear() return True @@ -213,9 +226,12 @@ def refresh_token_func(self, token): _LOGGER.debug("401 response stage 2: refresh stage 1 token") self.auth() + + def __acquire_token(self): # Fetch and refresh token as needed # If file exists read in token file and check it's valid + _LOGGER.debug("Fetching token") if self.save_token: if os.path.isfile(self.token_location): data = self.read_token() @@ -224,19 +240,35 @@ def __acquire_token(self): data["access_token"] = self.token data["refresh_token"] = self.refresh_token data["expiry_date"] = self.expires_at + data["auto_token"] = self.auto_token + data["auto_expiry"] = self.auto_expires_at else: data = {} data["access_token"] = self.token data["refresh_token"] = self.refresh_token data["expiry_date"] = self.expires_at + data["auto_token"] = self.auto_token + data["auto_expiry"] = self.auto_expires_at self.token = data["access_token"] self.expires_at = data["expiry_date"] + _LOGGER.debug(self.auto_token) + _LOGGER.debug(self.auto_expires_at) + if self.auto_token is None or self.auto_expires_at is None: + self.auth() + pass + self.auto_token = data["auto_token"] + self.auto_expires_at = data["auto_expiry"] if self.expires_at: if time.time() >= self.expires_at: _LOGGER.debug("No token, or has expired, requesting new token") self.refresh_token_func(data) # self.auth() + if self.auto_expires_at: + if time.time() >= self.auto_expires_at: + _LOGGER.debug("Autonomic token expired") + self.auth() if self.token is None: + _LOGGER.debug("Fetching token4") # No existing token exists so refreshing library self.auth() else: @@ -271,6 +303,37 @@ def clear_token(self): if os.path.isfile(self.token_location): os.remove(self.token_location) + def get_auto_token(self): + """Get token from new autonomic API""" + _LOGGER.debug("Getting Auto Token") + headers = { + "accept": "*/*", + "content-type": "application/x-www-form-urlencoded" + } + + data = { + "subject_token": self.token, + "subject_issuer": "fordpass", + "client_id": "fordpass-prod", + "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange", + "subject_token_type": "urn:ietf:params:oauth:token-type:jwt", + + } + + r = session.post( + f"{AUTONOMIC_ACCOUNT_URL}/auth/oidc/token", + data=data, + headers=headers + ) + + if r.status_code == 200: + result = r.json() + _LOGGER.debug(r.status_code) + _LOGGER.debug(r.text) + self.auto_token = result["access_token"] + return result + return False + def status(self): """Get Vehicle status from API""" @@ -283,38 +346,52 @@ def status(self): "auth-token": self.token, "Application-Id": self.region, } + _LOGGER.debug(self.auto_token) - response = session.get( - f"{BASE_URL}/vehicles/v5/{self.vin}/status", params=params, headers=headers - ) - if response.status_code == 200: - result = response.json() - if result["status"] == 402: - response.raise_for_status() - return result["vehiclestatus"] - if response.status_code == 401: - _LOGGER.debug("401 with status request: start token refresh") - data = {} - data["access_token"] = self.token - data["refresh_token"] = self.refresh_token - data["expiry_date"] = self.expires_at - self.refresh_token_func(data) - self.__acquire_token() + if NEW_API: headers = { **apiHeaders, - "auth-token": self.token, + "authorization": f"Bearer {self.auto_token}", "Application-Id": self.region, } + r = session.get( + f"{AUTONOMIC_URL}/telemetry/sources/fordpass/vehicles/{self.vin}", params=params, headers=headers + ) + if r.status_code == 200: + _LOGGER.debug(r.text) + result = r.json() + return result + else: response = session.get( - f"{BASE_URL}/vehicles/v5/{self.vin}/status", - params=params, - headers=headers, + f"{BASE_URL}/vehicles/v5/{self.vin}/status", params=params, headers=headers ) if response.status_code == 200: result = response.json() - return result["vehiclestatus"] - response.raise_for_status() - return None + if result["status"] == 402: + response.raise_for_status() + return result["vehiclestatus"] + if response.status_code == 401: + _LOGGER.debug("401 with status request: start token refresh") + data = {} + data["access_token"] = self.token + data["refresh_token"] = self.refresh_token + data["expiry_date"] = self.expires_at + self.refresh_token_func(data) + self.__acquire_token() + headers = { + **apiHeaders, + "auth-token": self.token, + "Application-Id": self.region, + } + response = session.get( + f"{BASE_URL}/vehicles/v5/{self.vin}/status", + params=params, + headers=headers, + ) + if response.status_code == 200: + result = response.json() + return result["vehiclestatus"] + response.raise_for_status() def messages(self): """Get Vehicle messages from API""" @@ -393,33 +470,25 @@ def start(self): """ Issue a start command to the engine """ - return self.__request_and_poll( - "PUT", f"{BASE_URL}/vehicles/v5/{self.vin}/engine/start" - ) + return self.__request_and_poll_command("remoteStart") def stop(self): """ Issue a stop command to the engine """ - return self.__request_and_poll( - "DELETE", f"{BASE_URL}/vehicles/v5/{self.vin}/engine/start" - ) + return self.__request_and_poll_command("cancelRemoteStart") def lock(self): """ Issue a lock command to the doors """ - return self.__request_and_poll( - "PUT", f"{BASE_URL}/vehicles/v5/{self.vin}/doors/lock" - ) + return self.__request_and_poll_command("lock") def unlock(self): """ Issue an unlock command to the doors """ - return self.__request_and_poll( - "DELETE", f"{BASE_URL}/vehicles/v5/{self.vin}/doors/lock" - ) + return self.__request_and_poll_command("unlock") def enable_guard(self): """ @@ -451,10 +520,8 @@ def request_update(self, vin=""): vinnum = vin else: vinnum = self.vin - status = self.__make_request( - "PUT", f"{BASE_URL}/vehicles/v5/{vinnum}/status", None, None - ) - return status.json()["status"] + status = self.__request_and_poll_command("statusRefresh", vinnum) + return status def __make_request(self, method, url, data, params): """ @@ -487,6 +554,40 @@ def __poll_status(self, url, command_id): _LOGGER.debug("Command failed") return False + def __request_and_poll_command(self, command, vin=None): + """Send command to the new Command endpoint""" + headers = { + **apiHeaders, + "Application-Id": self.region, + "authorization": f"Bearer {self.auto_token}" + } + + data = { + "properties": {}, + "tags": {}, + "type": command, + "wakeUp": True + } + if vin is None: + r = session.post( + f"{AUTONOMIC_URL}/command/vehicles/{self.vin}/commands", + data=json.dumps(data), + headers=headers + ) + else: + r = session.post( + f"{AUTONOMIC_URL}/command/vehicles/{self.vin}/commands", + data=json.dumps(data), + headers=headers + ) + + _LOGGER.debug("Testing command") + _LOGGER.debug(r.status_code) + _LOGGER.debug(r.text) + if r.status_code == 201: + return True + return False + def __request_and_poll(self, method, url): """Poll API until status code is reached, locking + remote start""" self.__acquire_token() diff --git a/custom_components/fordpass/lock.py b/custom_components/fordpass/lock.py index ff0f55c..1a500a4 100644 --- a/custom_components/fordpass/lock.py +++ b/custom_components/fordpass/lock.py @@ -14,7 +14,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): entry = hass.data[DOMAIN][config_entry.entry_id][COORDINATOR] lock = Lock(entry) - if lock.coordinator.data.get("lockStatus", {}) and lock.coordinator.data["lockStatus"]["value"] != "ERROR": + if lock.coordinator.data["metrics"]["doorLockStatus"] and lock.coordinator.data["metrics"]["doorLockStatus"][0]["value"] != "ERROR": async_add_entities([lock], False) else: _LOGGER.debug("Ford model doesn't support remote locking") @@ -22,47 +22,55 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class Lock(FordPassEntity, LockEntity): """Defines the vehicle's lock.""" - def __init__(self, coordinator): """Initialize.""" - super().__init__( - device_id="fordpass_doorlock", - name="fordpass_doorlock", - coordinator=coordinator, - ) + self._device_id = "fordpass_doorlock" + self.coordinator = coordinator + self.data = coordinator.data["metrics"] + + # Required for HA 2022.7 + self.coordinator_context = object() async def async_lock(self, **kwargs): """Locks the vehicle.""" - self._attr_is_locking = True + # self._attr_is_locking = True self.async_write_ha_state() _LOGGER.debug("Locking %s", self.coordinator.vin) - await self.coordinator.hass.async_add_executor_job( + status = await self.coordinator.hass.async_add_executor_job( self.coordinator.vehicle.lock ) + _LOGGER.debug(status) await self.coordinator.async_request_refresh() - self._attr_is_locking = False + _LOGGER.debug("Locking here") + # self._attr_is_locking = False self.async_write_ha_state() async def async_unlock(self, **kwargs): """Unlocks the vehicle.""" - self._attr_is_unlocking = True - self.async_write_ha_state() _LOGGER.debug("Unlocking %s", self.coordinator.vin) - await self.coordinator.hass.async_add_executor_job( + # self._attr_is_unlocking = True + self.async_write_ha_state() + status = await self.coordinator.hass.async_add_executor_job( self.coordinator.vehicle.unlock ) + _LOGGER.debug(status) await self.coordinator.async_request_refresh() - self._attr_is_unlocking = False + # self._attr_is_unlocking = False self.async_write_ha_state() @property def is_locked(self): """Determine if the lock is locked.""" - if self.coordinator.data is None or self.coordinator.data["lockStatus"] is None: + if self.coordinator.data["metrics"] is None or self.coordinator.data["metrics"]["doorLockStatus"] is None: return None - return self.coordinator.data["lockStatus"]["value"] == "LOCKED" + return self.coordinator.data["metrics"]["doorLockStatus"][0]["value"] == "LOCKED" @property def icon(self): """Return MDI Icon""" return "mdi:car-door-lock" + + @property + def name(self): + """Return Name""" + return "fordpass_doorlock" diff --git a/custom_components/fordpass/manifest.json b/custom_components/fordpass/manifest.json index 05f9bd8..a02b2cd 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.52", + "version": "0.1.53", "zeroconf": [] } \ No newline at end of file diff --git a/custom_components/fordpass/sensor.py b/custom_components/fordpass/sensor.py index 3a08c94..bb876f6 100644 --- a/custom_components/fordpass/sensor.py +++ b/custom_components/fordpass/sensor.py @@ -28,8 +28,12 @@ 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"] is not None: + if "xevBatteryRange" in sensor.coordinator.data["metrics"]: sensors.append(sensor) + ## SquidBytes: Added elVehCharging + elif key == "elVehCharging": + if "xevBatteryChargeEvent" in sensor.coordinator.data["events"]: + sensors.append(sensor) elif key == "dieselSystemStatus": if sensor.coordinator.data.get("dieselSystemStatus", {}): if sensor.coordinator.data.get("dieselSystemStatus", {}).get("filterRegenerationStatus"): @@ -59,90 +63,121 @@ def __init__(self, coordinator, sensor, options): self.fordoptions = options self._attr = {} self.coordinator = coordinator + self.data = coordinator.data["metrics"] self._device_id = "fordpass_" + sensor # Required for HA 2022.7 self.coordinator_context = object() def get_value(self, ftype): """Get sensor value and attributes from coordinator data""" + self.data = self.coordinator.data["metrics"] 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"] + return self.data[self.sensor]["value"] return round( - float(self.coordinator.data[self.sensor]["value"]) / 1.60934 + float(self.data[self.sensor]["value"]) / 1.60934 ) - return self.coordinator.data[self.sensor]["value"] - return self.coordinator.data[self.sensor]["value"] + return self.data[self.sensor]["value"] + return self.data["odometer"]["value"] if self.sensor == "fuel": - if self.coordinator.data[self.sensor] is None: + if "fuelLevel" in self.data: + if self.data["fuelLevel"] is None: + return None + return round(self.data["fuelLevel"]["value"]) + elif "xevBatteryStateOfCharge" in self.data: + return round(self.data["xevBatteryStateOfCharge"]["value"]) + else: return None - return round(self.coordinator.data[self.sensor]["fuelLevel"]) if self.sensor == "battery": - return self.coordinator.data[self.sensor]["batteryHealth"]["value"] + return round(self.data["batteryStateOfCharge"]["value"]) if self.sensor == "oil": - return self.coordinator.data[self.sensor]["oilLife"] + return round(self.data["oilLifeRemaining"]["value"]) if self.sensor == "tirePressure": - return self.coordinator.data[self.sensor]["value"] + if "tirePressureSystemStatus" in self.data: + return self.data["tirePressureSystemStatus"][0]["value"] + return "Not Supported" if self.sensor == "gps": - if self.coordinator.data[self.sensor] is None: + if self.data["position"] is None: return "Unsupported" - return self.coordinator.data[self.sensor]["gpsState"] + return self.data["position"]["value"] if self.sensor == "alarm": - return self.coordinator.data[self.sensor]["value"] + return self.data["alarmStatus"]["value"] if self.sensor == "ignitionStatus": - return self.coordinator.data[self.sensor]["value"] + return self.data[self.sensor]["value"] if self.sensor == "firmwareUpgInProgress": - return self.coordinator.data[self.sensor]["value"] + return self.data[self.sensor]["value"] if self.sensor == "deepSleepInProgress": - return self.coordinator.data[self.sensor]["value"] + return self.data[self.sensor]["value"] if self.sensor == "doorStatus": - for key, value in self.coordinator.data[self.sensor].items(): + for value in self.data["doorStatus"]: if value["value"] == "Invalid": continue - if value["value"] != "Closed": + if value["value"] != "CLOSED": return "Open" return "Closed" 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 + if "windowStatus" in self.data: + if self.data["windowStatus"] is None: + return "Unsupported" + status = "Closed" + for window in self.data["windowStatus"]: + windowrange = window["value"]["doubleRange"] + if windowrange["lowerBound"] != 0.0 and windowrange["upperBound"] != 0.0: + status = "Open" + return status + return "Unsupported" if self.sensor == "lastRefresh": - return dt.as_local( - datetime.strptime( - self.coordinator.data[self.sensor] + "+0000", "%m-%d-%Y %H:%M:%S%z" + try: + return dt.as_local( + datetime.strptime( + self.coordinator.data["updateTime"], "%Y-%m-%dT%H:%M:%S.%fz" + ) + ) + except: + _LOGGER.debug("%f conversion failed") + try: + return dt.as_local( + datetime.strptime( + self.coordinator.data["updateTime"], "%Y-%m-%dT%H:%M:%Sz" + ) ) - ) + except: + _LOGGER.debug("%s conversion failed") + refresh = "" + return refresh if self.sensor == "elVeh": - if self.coordinator.data["elVehDTE"] is not None: + if "xevBatteryRange" in self.data: 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.data["xevBatteryRange"]["value"]) / 1.60934 ) - return float(self.coordinator.data["elVehDTE"]["value"]) - return float(self.coordinator.data["elVehDTE"]["value"]) + return float(self.data["xevBatteryRange"]["value"]) + return float(self.data["xevBatteryRange"]["value"]) return "Unsupported" + ## SquidBytes: Added elVehCharging + if self.sensor == "elVehCharging": + if "xevBatteryChargeDisplayStatus" in self.data: + ## Default sensor type is the status of charge (might be better to have the kW as the value, but for now I'll do this) + return self.data["xevBatteryChargeDisplayStatus"]["value"] + return "Unsupported" if self.sensor == "zoneLighting": - if "zoneLighting" not in self.coordinator.data: + if "zoneLighting" not in self.data: return "Unsupported" if ( - self.coordinator.data["zoneLighting"] is not None and self.coordinator.data["zoneLighting"]["activationData"] is not None + self.data["zoneLighting"] is not None and self.data["zoneLighting"]["activationData"] is not None ): - return self.coordinator.data["zoneLighting"]["activationData"][ + return self.data["zoneLighting"]["activationData"][ "value" ] return "Unsupported" if self.sensor == "remoteStartStatus": - if self.coordinator.data["remoteStartStatus"] is None: + if self.data["remoteStartCountdownTimer"] is None: return None - if self.coordinator.data["remoteStartStatus"]["value"] == 1: + if self.data["remoteStartCountdownTimer"]["value"] > 0: return "Active" return "Inactive" if self.sensor == "messages": @@ -150,13 +185,15 @@ def get_value(self, ftype): return 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"] + if self.data["dieselSystemStatus"]["filterRegenerationStatus"] is not None: + return self.data["dieselSystemStatus"]["filterRegenerationStatus"] return "Not Supported" if self.sensor == "exhaustFluidLevel": - if "value" in self.coordinator.data["dieselSystemStatus"]["exhaustFluidLevel"]: - return self.coordinator.data["dieselSystemStatus"]["exhaustFluidLevel"]["value"] + if "value" in self.data["dieselSystemStatus"]["exhaustFluidLevel"]: + return self.data["dieselSystemStatus"]["exhaustFluidLevel"]["value"] return "Not Supported" + if self.sensor == "speed": + return self.data[self.sensor]["value"] return None if ftype == "measurement": if self.sensor == "odometer": @@ -166,9 +203,9 @@ def get_value(self, ftype): if self.sensor == "fuel": return "%" if self.sensor == "battery": - return None + return "%" if self.sensor == "oil": - return None + return "%" if self.sensor == "tirePressure": return None if self.sensor == "gps": @@ -197,31 +234,40 @@ def get_value(self, ftype): if self.fordoptions[CONF_DISTANCE_UNIT] == "mi": return "mi" return "km" + if self.sensor == "speed": + if self.fordoptions[CONF_DISTANCE_UNIT] == "mi": + return "mph" + return "km/h" if self.sensor == "exhaustFluidLevel": return "%" return None if ftype == "attribute": if self.sensor == "odometer": - return self.coordinator.data[self.sensor].items() + return self.data[self.sensor].items() 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 - ) - return self.coordinator.data[self.sensor].items() + if "fuelRange" in self.data: + if self.fordoptions[CONF_DISTANCE_UNIT] == "mi": + return {"fuelRange": round( + float(self.data["fuelRange"]["value"]) / 1.60934 + )} + return {"fuelRange": self.data["fuelRange"]["value"]} + elif "xevBatteryRange" in self.data: + if self.fordoptions[CONF_DISTANCE_UNIT] == "mi": + return {"batteryRange": round( + float(self.data["xevBatteryRange"]["value"]) / 1.60934 + )} + return {"batteryRange": self.data["xevBatteryRange"]["value"]} if self.sensor == "battery": return { - "Battery Voltage": self.coordinator.data[self.sensor][ - "batteryStatusActual" - ]["value"] + "Battery Voltage": self.data["batteryVoltage"]["value"] } if self.sensor == "oil": - return self.coordinator.data[self.sensor].items() + return self.data["oilLifeRemaining"].items() if self.sensor == "tirePressure": - if self.coordinator.data["TPMS"] is not None: + if "tirePressure" in self.data : + _LOGGER.debug(self.fordoptions[CONF_PRESSURE_UNIT]) if self.fordoptions[CONF_PRESSURE_UNIT] == "PSI": + _LOGGER.debug("PSIIIII") sval = 0.1450377377 rval = 1 decimal = 0 @@ -234,161 +280,190 @@ def get_value(self, ftype): rval = 6.8947572932 decimal = 0 else: + _LOGGER.debug("HITT") sval = 1 rval = 1 decimal = 0 tirepress = {} - for key, value in self.coordinator.data["TPMS"].items(): - if "TirePressure" in key and value is not None and value != '': - if "recommended" in key: - tirepress[key] = round(float(value["value"]) * rval, decimal) - else: - tirepress[key] = round(float(value["value"]) * sval, decimal) + for value in self.data["tirePressure"]: + # if "recommended" in key: + # tirepress[key] = round(float(value["value"]) * rval, decimal) + # else: + tirepress[value["vehicleWheel"]] = round(float(value["value"]) * sval, decimal) return tirepress return None if self.sensor == "gps": - if self.coordinator.data[self.sensor] is None: + if self.data["position"] is None: return None - return self.coordinator.data[self.sensor].items() + return self.data["position"].items() if self.sensor == "alarm": - return self.coordinator.data[self.sensor].items() + return self.data["alarmStatus"].items() if self.sensor == "ignitionStatus": - return self.coordinator.data[self.sensor].items() + return self.data[self.sensor].items() if self.sensor == "firmwareUpgInProgress": - return self.coordinator.data[self.sensor].items() + return self.data[self.sensor].items() if self.sensor == "deepSleepInProgress": - return self.coordinator.data[self.sensor].items() + return self.data[self.sensor].items() if self.sensor == "doorStatus": doors = {} - for key, value in self.coordinator.data[self.sensor].items(): - doors[key] = value["value"] + for value in self.data[self.sensor]: + _LOGGER.debug(value) + if "vehicleSide" in value: + doors[f"{value['vehicleSide']} : {value['vehicleDoor']}"] = value['value'] + else: + doors[value["vehicleDoor"]] = value['value'] return doors if self.sensor == "windowPosition": - if self.coordinator.data[self.sensor] is None: + if "windowStatus" not in self.data: return None windows = {} - for key, value in self.coordinator.data[self.sensor].items(): - windows[key] = value["value"] - if "open" in value["value"].lower(): - if "btwn" in value["value"].lower(): - windows[key] = "Open-Partial" - else: - windows[key] = "Open" - elif "closed" in value["value"].lower(): - windows[key] = "Closed" + for window in self.data["windowStatus"]: + windows[window["vehicleWindow"]] = window return windows if self.sensor == "lastRefresh": return None if self.sensor == "elVeh": - if self.coordinator.data["elVehDTE"] is None: + if "xevBatteryRange" not in self.data: return None elecs = {} if ( - self.coordinator.data["elVehDTE"] is not None and self.coordinator.data["elVehDTE"]["value"] is not None + "xevBatteryCapacity" in self.data and self.data["xevBatteryCapacity"] is not None and self.data["xevBatteryCapacity"]["value"] is not None ): - elecs["elVehDTE"] = self.coordinator.data["elVehDTE"]["value"] + elecs["xevBatteryCapacity"] = self.data["xevBatteryCapacity"]["value"] if ( - self.coordinator.data["plugStatus"] is not None and self.coordinator.data["plugStatus"]["value"] is not None + "xevPlugChargerStatus" in self.data and self.data["xevPlugChargerStatus"] is not None and self.data["xevPlugChargerStatus"]["value"] is not None ): - elecs["Plug Status"] = self.coordinator.data["plugStatus"][ + elecs["Plug Status"] = self.data["xevPlugChargerStatus"][ "value" ] if ( - self.coordinator.data["chargingStatus"] is not None and self.coordinator.data["chargingStatus"]["value"] is not None + "xevBatteryChargeDisplayStatus" in self.data and self.data["xevBatteryChargeDisplayStatus"] is not None and self.data["xevBatteryChargeDisplayStatus"]["value"] is not None ): - elecs["Charging Status"] = self.coordinator.data[ - "chargingStatus" + elecs["Charging Status"] = self.data[ + "xevBatteryChargeDisplayStatus" ]["value"] if ( - self.coordinator.data["chargeStartTime"] is not None and self.coordinator.data["chargeStartTime"]["value"] is not None + "xevChargeStationPowerType" in self.data and self.data["xevChargeStationPowerType"] is not None and self.data["xevChargeStationPowerType"]["value"] is not None ): - elecs["Charge Start Time"] = self.coordinator.data[ - "chargeStartTime" + elecs["Charger Power Type"] = self.data[ + "xevChargeStationPowerType" ]["value"] if ( - self.coordinator.data["chargeEndTime"] is not None and self.coordinator.data["chargeEndTime"]["value"] is not None + "xevChargeStationCommunicationStatus" in self.data and self.data["xevChargeStationCommunicationStatus"] is not None and self.data["xevChargeStationCommunicationStatus"]["value"] is not None ): - elecs["Charge End Time"] = self.coordinator.data[ - "chargeEndTime" + elecs["Battery Charge Status"] = self.data[ + "xevChargeStationCommunicationStatus" ]["value"] if ( - self.coordinator.data["batteryFillLevel"] is not None and self.coordinator.data["batteryFillLevel"]["value"] is not None + "xevBatteryPerformanceStatus" in self.data and self.data["xevBatteryPerformanceStatus"] is not None and self.data["xevBatteryPerformanceStatus"]["value"] is not None ): - elecs["Battery Fill Level"] = int(self.coordinator.data[ - "batteryFillLevel" - ]["value"]) + elecs["Battery Performance Status"] = self.data[ + "xevBatteryPerformanceStatus" + ]["value"] if ( - self.coordinator.data["chargerPowertype"] is not None and self.coordinator.data["chargerPowertype"]["value"] is not None + "xevBatteryStateOfCharge" in self.data and self.data["xevBatteryStateOfCharge"] is not None and self.data["xevBatteryStateOfCharge"]["value"] is not None ): - elecs["Charger Power Type"] = self.coordinator.data[ - "chargerPowertype" + elecs["Battery Charge"] = self.data[ + "xevBatteryStateOfCharge" ]["value"] + return elecs + ## SquidBytes: Added elVehCharging + if self.sensor == "elVehCharging": + if "xevPlugChargerStatus" not in self.data: + return None + + cs = {} if ( - self.coordinator.data["batteryChargeStatus"] is not None and self.coordinator.data["batteryChargeStatus"]["value"] is not None + "xevBatteryStateOfCharge" in self.data and self.data["xevBatteryStateOfCharge"] is not None and self.data["xevBatteryStateOfCharge"]["value"] is not None ): - elecs["Battery Charge Status"] = self.coordinator.data[ - "batteryChargeStatus" - ]["value"] - + cs["Charging State of Charge"] = self.data["xevBatteryStateOfCharge"]["value"] + if ("xevBatteryChargeDisplayStatus" in self.data and self.data["xevBatteryChargeDisplayStatus"] is not None and self.data["xevBatteryChargeDisplayStatus"]["value"] is not None + ): + cs["Charging Status"] = self.data["xevBatteryChargeDisplayStatus"]["value"] if ( - self.coordinator.data["batteryPerfStatus"] is not None and self.coordinator.data["batteryPerfStatus"]["value"] is not None + "xevChargeStationPowerType" in self.data and self.data["xevChargeStationPowerType"] is not None and self.data["xevChargeStationPowerType"]["value"] is not None ): - elecs["Battery Performance Status"] = self.coordinator.data[ - "batteryPerfStatus" - ]["value"] + cs["Charging Type"] = self.data["xevChargeStationPowerType"]["value"] + if ( + "xevChargeStationCommunicationStatus" in self.data and self.data["xevChargeStationCommunicationStatus"] is not None and self.data["xevChargeStationCommunicationStatus"]["value"] is not None + ): + cs["Charge Station Status"] = self.data["xevChargeStationCommunicationStatus"]["value"] + if ( + "xevBatteryTemperature" in self.data and self.data["xevBatteryTemperature"] is not None and self.data["xevBatteryTemperature"]["value"] is not None + ): + cs["Battery Temperature"] = self.data["xevBatteryTemperature"]["value"] + if ( + "xevBatteryChargerVoltageOutput" in self.data and self.data["xevBatteryChargerVoltageOutput"] is not None and self.data["xevBatteryChargerVoltageOutput"]["value"] is not None + ): + cs["Charging Voltage"] = float(self.data["xevBatteryChargerVoltageOutput"]["value"]) + chVolt = cs["Charging Voltage"] + if ( + "xevBatteryChargerCurrentOutput" in self.data and self.data["xevBatteryChargerCurrentOutput"] is not None and self.data["xevBatteryChargerCurrentOutput"]["value"] is not None + ): + cs["Charging Amperage"] = float(self.data["xevBatteryChargerCurrentOutput"]["value"]) + chAmps = cs["Charging Amperage"] + if ( + "xevBatteryChargerCurrentOutput" in self.data and self.data["xevBatteryChargerCurrentOutput"]["value"] is not None and self.data["xevBatteryChargerVoltageOutput"]["value"] is not None + ): + cs["Charging kW"] = round((chVolt * chAmps) / 1000, 2) + + if ( + "xevBatteryTimeToFullCharge" in self.data and self.data["xevBatteryTimeToFullCharge"] is not None and self.data["xevBatteryTimeToFullCharge"]["value"] is not None + ): + cs["Time To Full Charge"] = self.data["xevBatteryTimeToFullCharge"]["value"] - return elecs + return cs if self.sensor == "zoneLighting": - if "zoneLighting" not in self.coordinator.data: + if "zoneLighting" not in self.data: return None if ( - self.coordinator.data[self.sensor] is not None and self.coordinator.data[self.sensor]["zoneStatusData"] is not None + self.data[self.sensor] is not None and self.data[self.sensor]["zoneStatusData"] is not None ): zone = {} - if self.coordinator.data[self.sensor]["zoneStatusData"] is not None: - for key, value in self.coordinator.data[self.sensor][ + if self.data[self.sensor]["zoneStatusData"] is not None: + for key, value in self.data[self.sensor][ "zoneStatusData" ].items(): zone["zone_" + key] = value["value"] if ( - self.coordinator.data[self.sensor]["lightSwitchStatusData"] + self.data[self.sensor]["lightSwitchStatusData"] is not None ): - for key, value in self.coordinator.data[self.sensor][ + for key, value in self.data[self.sensor][ "lightSwitchStatusData" ].items(): if value is not None: zone[key] = value["value"] if ( - self.coordinator.data[self.sensor]["zoneLightingFaultStatus"] + self.data[self.sensor]["zoneLightingFaultStatus"] is not None ): - zone["zoneLightingFaultStatus"] = self.coordinator.data[ + zone["zoneLightingFaultStatus"] = self.data[ self.sensor ]["zoneLightingFaultStatus"]["value"] if ( - self.coordinator.data[self.sensor][ + self.data[self.sensor][ "zoneLightingShutDownWarning" ] is not None ): - zone["zoneLightingShutDownWarning"] = self.coordinator.data[ + zone["zoneLightingShutDownWarning"] = self.data[ self.sensor ]["zoneLightingShutDownWarning"]["value"] return zone return None if self.sensor == "remoteStartStatus": - if self.coordinator.data["remoteStart"] is None: + if self.data["remoteStartCountdownTimer"] is None: return None - return self.coordinator.data["remoteStart"].items() + return { "Countdown": self.data["remoteStartCountdownTimer"]["value"] } if self.sensor == "messages": if self.coordinator.data["messages"] is None: return None @@ -398,9 +473,11 @@ def get_value(self, ftype): messages[value["messageSubject"]] = value["createdDate"] return messages if self.sensor == "dieselSystemStatus": - return self.coordinator.data["dieselSystemStatus"] + return self.data["dieselSystemStatus"] if self.sensor == "exhaustFluidLevel": - return self.coordinator.data["dieselSystemStatus"] + return self.data["dieselSystemStatus"] + if self.sensor == "speed": + return None return None return None diff --git a/custom_components/fordpass/switch.py b/custom_components/fordpass/switch.py index f8ded13..9b80621 100644 --- a/custom_components/fordpass/switch.py +++ b/custom_components/fordpass/switch.py @@ -15,17 +15,17 @@ async def async_setup_entry(hass, config_entry, async_add_entities): # switches = [Switch(entry)] # async_add_entities(switches, False) - for key in SWITCHES: - switch = Switch(entry, key, config_entry.options) + for key, value in SWITCHES.items(): + sw = Switch(entry, key, config_entry.options) # Only add guard entity if supported by the car if key == "guardmode": - if "guardstatus" in switch.coordinator.data: - if switch.coordinator.data["guardstatus"]["returnCode"] == 200: - async_add_entities([switch], False) + if "guardstatus" in sw.coordinator.data: + if sw.coordinator.data["guardstatus"]["returnCode"] == 200: + async_add_entities([sw], False) else: _LOGGER.debug("Guard mode not supported on this vehicle") else: - async_add_entities([switch], False) + async_add_entities([sw], False) class Switch(FordPassEntity, SwitchEntity): @@ -33,15 +33,10 @@ class Switch(FordPassEntity, SwitchEntity): 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 self.coordinator = coordinator + self.data = coordinator.data["metrics"] # Required for HA 2022.7 self.coordinator_context = object() @@ -88,10 +83,11 @@ def is_on(self): """Check status of switch""" if self.switch == "ignition": if ( - self.coordinator.data is None or self.coordinator.data["remoteStartStatus"] is None + self.coordinator.data["metrics"] is None or self.coordinator.data["metrics"]["ignitionStatus"] is None ): return None - return self.coordinator.data["remoteStartStatus"]["value"] + if self.coordinator.data["metrics"]["ignitionStatus"]["value"] == "OFF": + return False 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"] diff --git a/info.md b/info.md index adc5f13..1261184 100644 --- a/info.md +++ b/info.md @@ -1,4 +1,15 @@ ## **Changelog** +### Version 1.53 +- Updated vehicle endpoint to use new Autonomics API +- Added secondary Autonomic token +- Remapped commands to use new "command" API endpoint +- Remapped existing sensors to new json variables (Some are missinge) +- Added charge status sensor (Thanks @SquidBytes) +- Added new speed sensor (Will be adding more attributes to this like pedal position and torque settings soon) + +*Please report any bugs as a separate issue so I can keep track easier* + +There is a LOT more coming soon as the new API exposes an excessive amount of information including speed, pedal position, crash sensors and way more. ### Version 1.52 - Update for discontinued API endpoints (Update, lock, remote start) ### Version 1.51