diff --git a/.github/workflows/combined.yaml b/.github/workflows/combined.yaml new file mode 100644 index 0000000..5d4d046 --- /dev/null +++ b/.github/workflows/combined.yaml @@ -0,0 +1,30 @@ +name: "Validation And Formatting" +on: + push: + pull_request: + schedule: + - cron: '0 0 * * *' +jobs: + ci: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + name: Download repo + with: + fetch-depth: 0 + - uses: actions/setup-python@v2 + name: Setup Python + - uses: actions/cache@v2 + name: Cache + with: + path: | + ~/.cache/pip + key: custom-component-ci + - uses: hacs/integration/action@main + with: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + CATEGORY: integration + - uses: KTibow/ha-blueprint@stable + name: CI + with: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/validate.yaml b/.github/workflows/validate.yaml deleted file mode 100644 index f518c1a..0000000 --- a/.github/workflows/validate.yaml +++ /dev/null @@ -1,18 +0,0 @@ -name: Validate - -on: - push: - pull_request: - schedule: - - cron: "0 0 * * *" - -jobs: - validate: - runs-on: "ubuntu-latest" - steps: - - uses: "actions/checkout@v2" - - name: HACS validation - uses: "hacs/integration/action@main" - with: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - CATEGORY: integration diff --git a/custom_components/fordpass/__init__.py b/custom_components/fordpass/__init__.py index 82d2695..b6b756e 100644 --- a/custom_components/fordpass/__init__.py +++ b/custom_components/fordpass/__init__.py @@ -1,13 +1,11 @@ """The FordPass integration.""" import asyncio -from datetime import timedelta import logging +from datetime import timedelta import async_timeout -from dotted.collection import DottedDict -from .fordpass_new import Vehicle import voluptuous as vol - +from dotted.collection import DottedDict from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant @@ -19,10 +17,11 @@ ) from .const import DOMAIN, MANUFACTURER, VEHICLE, VIN +from .fordpass_new import Vehicle CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA) -PLATFORMS = ["lock","sensor", "switch"] +PLATFORMS = ["lock", "sensor", "switch"] _LOGGER = logging.getLogger(__name__) diff --git a/custom_components/fordpass/config_flow.py b/custom_components/fordpass/config_flow.py index cd098af..3370abf 100644 --- a/custom_components/fordpass/config_flow.py +++ b/custom_components/fordpass/config_flow.py @@ -1,9 +1,8 @@ """Config flow for FordPass integration.""" import logging -from fordpass import Vehicle import voluptuous as vol - +from fordpass import Vehicle from homeassistant import config_entries, core, exceptions from homeassistant.const import CONF_PASSWORD, CONF_USERNAME diff --git a/custom_components/fordpass/const.py b/custom_components/fordpass/const.py index ae11eae..b235fb6 100644 --- a/custom_components/fordpass/const.py +++ b/custom_components/fordpass/const.py @@ -6,4 +6,4 @@ MANUFACTURER = "Ford Motor Company" -VEHICLE = "Ford Vehicle" \ No newline at end of file +VEHICLE = "Ford Vehicle" diff --git a/custom_components/fordpass/fordpass_new.py b/custom_components/fordpass/fordpass_new.py index fe34fac..2998b5f 100644 --- a/custom_components/fordpass/fordpass_new.py +++ b/custom_components/fordpass/fordpass_new.py @@ -1,26 +1,28 @@ -import requests -import logging -import time import json +import logging import os.path +import time + +import requests defaultHeaders = { - 'Accept': '*/*', - 'Accept-Language': 'en-us', - 'User-Agent': 'fordpass-ap/93 CFNetwork/1197 Darwin/20.0.0', - 'Accept-Encoding': 'gzip, deflate, br', + "Accept": "*/*", + "Accept-Language": "en-us", + "User-Agent": "fordpass-ap/93 CFNetwork/1197 Darwin/20.0.0", + "Accept-Encoding": "gzip, deflate, br", } apiHeaders = { **defaultHeaders, - 'Application-Id': '5C80A6BB-CF0D-4A30-BDBF-FC804B5C1A98', - 'Content-Type': 'application/json', + "Application-Id": "5C80A6BB-CF0D-4A30-BDBF-FC804B5C1A98", + "Content-Type": "application/json", } -baseUrl = 'https://usapi.cv.ford.com/api' +baseUrl = "https://usapi.cv.ford.com/api" + class Vehicle(object): - #Represents a Ford vehicle, with methods for status and issuing commands + # Represents a Ford vehicle, with methods for status and issuing commands def __init__(self, username, password, vin, saveToken=False): self.username = username @@ -31,71 +33,75 @@ def __init__(self, username, password, vin, saveToken=False): self.expires = None self.expiresAt = None self.refresh_token = None + def auth(self): - '''Authenticate and store the token''' + """Authenticate and store the token""" data = { - 'client_id': '9fb503e0-715b-47e8-adfd-ad4b7770f73b', - 'grant_type': 'password', - 'username': self.username, - 'password': self.password + "client_id": "9fb503e0-715b-47e8-adfd-ad4b7770f73b", + "grant_type": "password", + "username": self.username, + "password": self.password, } headers = { **defaultHeaders, - 'Content-Type': 'application/x-www-form-urlencoded' + "Content-Type": "application/x-www-form-urlencoded", } - # Fetch OAUTH token stage 1 - r = requests.post('https://sso.ci.ford.com/oidc/endpoint/default/token', data=data, headers=headers) + # Fetch OAUTH token stage 1 + r = requests.post( + "https://sso.ci.ford.com/oidc/endpoint/default/token", + data=data, + headers=headers, + ) if r.status_code == 200: - logging.info('Succesfully fetched token Stage1') + logging.info("Succesfully fetched token Stage1") result = r.json() - data = { - "code": result["access_token"] - } - headers = { - **apiHeaders - } - #Fetch OAUTH token stage 2 and refresh token - r = requests.put('https://api.mps.ford.com/api/oauth2/v1/token', data=json.dumps(data), headers=headers) + data = {"code": result["access_token"]} + headers = {**apiHeaders} + # Fetch OAUTH token stage 2 and refresh token + r = requests.put( + "https://api.mps.ford.com/api/oauth2/v1/token", + data=json.dumps(data), + headers=headers, + ) if r.status_code == 200: - result = r.json() - self.token = result['access_token'] - self.refresh_token = result["refresh_token"] - self.expiresAt = time.time() + result['expires_in'] - if self.saveToken: - result["expiry_date"] = time.time() + result['expires_in'] - self.writeToken(result) - return True + result = r.json() + self.token = result["access_token"] + self.refresh_token = result["refresh_token"] + self.expiresAt = time.time() + result["expires_in"] + if self.saveToken: + result["expiry_date"] = time.time() + result["expires_in"] + self.writeToken(result) + return True else: r.raise_for_status() - def refreshToken(self, token): - #Token is invalid so let's try refreshing it - data = { - "refresh_token": token["refresh_token"] - } - headers = { - **apiHeaders - } - - r = requests.put('https://api.mps.ford.com/api/oauth2/v1/refresh', data=json.dumps(data), headers=headers) + # Token is invalid so let's try refreshing it + data = {"refresh_token": token["refresh_token"]} + headers = {**apiHeaders} + + r = requests.put( + "https://api.mps.ford.com/api/oauth2/v1/refresh", + data=json.dumps(data), + headers=headers, + ) if r.status_code == 200: result = r.json() if self.saveToken: - result["expiry_date"] = time.time() + result['expires_in'] + result["expiry_date"] = time.time() + result["expires_in"] self.writeToken(result) - self.token = result['access_token'] + self.token = result["access_token"] self.refresh_token = result["refresh_token"] - self.expiresAt = time.time() + result['expires_in'] + self.expiresAt = time.time() + result["expires_in"] def __acquireToken(self): - #Fetch and refresh token as needed - #If file exists read in token file and check it's valid + # Fetch and refresh token as needed + # If file exists read in token file and check it's valid if self.saveToken: - if os.path.isfile('/tmp/token.txt'): + if os.path.isfile("/tmp/token.txt"): data = self.readToken() else: data = dict() @@ -107,114 +113,117 @@ def __acquireToken(self): data["access_token"] = self.token data["refresh_token"] = self.refresh_token data["expiry_date"] = self.expiresAt - self.token=data["access_token"] + self.token = data["access_token"] self.expiresAt = data["expiry_date"] if self.expiresAt: if time.time() >= self.expiresAt: - logging.info('No token, or has expired, requesting new token') - self.refreshToken(data) - #self.auth() + logging.info("No token, or has expired, requesting new token") + self.refreshToken(data) + # self.auth() if self.token == None: - #No existing token exists so refreshing library + # No existing token exists so refreshing library self.auth() else: - logging.info('Token is valid, continuing') + logging.info("Token is valid, continuing") pass def writeToken(self, token): - #Save token to file to be reused - with open('/tmp/token.txt', 'w') as outfile: - token["expiry_date"] = time.time() + token['expires_in'] + # Save token to file to be reused + with open("/tmp/token.txt", "w") as outfile: + token["expiry_date"] = time.time() + token["expires_in"] json.dump(token, outfile) def readToken(self): - #Get saved token from file - with open('/tmp/token.txt') as token_file: + # Get saved token from file + with open("/tmp/token.txt") as token_file: return json.load(token_file) def status(self): - #Get the status of the vehicle + # Get the status of the vehicle self.__acquireToken() - params = { - 'lrdt': '01-01-1970 00:00:00' - } + params = {"lrdt": "01-01-1970 00:00:00"} - headers = { - **apiHeaders, - 'auth-token': self.token - } + headers = {**apiHeaders, "auth-token": self.token} - r = requests.get(f'{baseUrl}/vehicles/v4/{self.vin}/status', params=params, headers=headers) + r = requests.get( + f"{baseUrl}/vehicles/v4/{self.vin}/status", params=params, headers=headers + ) if r.status_code == 200: result = r.json() if result["status"] == 402: - r.raise_for_status() - return result['vehiclestatus'] + r.raise_for_status() + return result["vehiclestatus"] else: r.raise_for_status() def start(self): - ''' + """ Issue a start command to the engine - ''' - return self.__requestAndPoll('PUT', f'{baseUrl}/vehicles/v2/{self.vin}/engine/start') + """ + return self.__requestAndPoll( + "PUT", f"{baseUrl}/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.__requestAndPoll( + "DELETE", f"{baseUrl}/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.__requestAndPoll( + "PUT", f"{baseUrl}/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.__requestAndPoll( + "DELETE", f"{baseUrl}/vehicles/v2/{self.vin}/doors/lock" + ) def requestUpdate(self): - #Send request to refresh data from the cars module + # Send request to refresh data from the cars module self.__acquireToken() - status = self.__makeRequest('PUT', f'{baseUrl}/vehicles/v2/{self.vin}/status', None, None) + status = self.__makeRequest( + "PUT", f"{baseUrl}/vehicles/v2/{self.vin}/status", None, None + ) return status.json()["status"] - def __makeRequest(self, method, url, data, params): - ''' + """ Make a request to the given URL, passing data/params as needed - ''' + """ - headers = { - **apiHeaders, - 'auth-token': self.token - } + headers = {**apiHeaders, "auth-token": self.token} - return getattr(requests, method.lower())(url, headers=headers, data=data, params=params) + return getattr(requests, method.lower())( + url, headers=headers, data=data, params=params + ) def __pollStatus(self, url, 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.__makeRequest("GET", f"{url}/{id}", None, None) result = status.json() - if result['status'] == 552: - logging.info('Command is pending') + if result["status"] == 552: + logging.info("Command is pending") time.sleep(5) - return self.__pollStatus(url, id) # retry after 5s - elif result['status'] == 200: - logging.info('Command completed succesfully') + return self.__pollStatus(url, id) # retry after 5s + elif result["status"] == 200: + logging.info("Command completed succesfully") return True else: - logging.info('Command failed') + logging.info("Command failed") return False def __requestAndPoll(self, method, url): @@ -223,6 +232,6 @@ def __requestAndPoll(self, method, url): if command.status_code == 200: result = command.json() - return self.__pollStatus(url, result['commandId']) + return self.__pollStatus(url, result["commandId"]) else: - command.raise_for_status() \ No newline at end of file + command.raise_for_status() diff --git a/custom_components/fordpass/sensor.py b/custom_components/fordpass/sensor.py index 0f861c1..19a2628 100644 --- a/custom_components/fordpass/sensor.py +++ b/custom_components/fordpass/sensor.py @@ -1,12 +1,11 @@ import logging +from datetime import timedelta from homeassistant.helpers.entity import Entity +from homeassistant.util import Throttle from . import FordPassEntity from .const import DOMAIN -from datetime import timedelta -from homeassistant.util import Throttle - _LOGGER = logging.getLogger(__name__) @@ -14,27 +13,33 @@ 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] - snrarray = [ "odometer", "fuel", "battery", "oil", "tirePressure", "gps", "alarm", "ignitionStatus", "doorStatus"] + snrarray = [ + "odometer", + "fuel", + "battery", + "oil", + "tirePressure", + "gps", + "alarm", + "ignitionStatus", + "doorStatus", + ] sensors = [] for snr in snrarray: async_add_entities([CarSensor(entry, snr)], True) - - - -class CarSensor(FordPassEntity,Entity): +class CarSensor(FordPassEntity, Entity): def __init__(self, coordinator, sensor): self.sensor = sensor self._attr = {} self.coordinator = coordinator self._device_id = "fordpass_" + sensor - def get_value(self, ftype): if ftype == "state": - if self.sensor == "odometer": + if self.sensor == "odometer": return self.coordinator.data[self.sensor]["value"] elif self.sensor == "fuel": return self.coordinator.data[self.sensor]["fuelLevel"] @@ -51,12 +56,12 @@ def get_value(self, ftype): elif self.sensor == "ignitionStatus": return self.coordinator.data[self.sensor]["value"] elif self.sensor == "doorStatus": - for key,value in self.coordinator.data[self.sensor].items(): + for key, value in self.coordinator.data[self.sensor].items(): if value["value"] != "Closed": return "Open" return "Closed" elif ftype == "measurement": - if self.sensor == "odometer": + if self.sensor == "odometer": return "km" elif self.sensor == "fuel": return "L" @@ -75,7 +80,7 @@ def get_value(self, ftype): elif self.sensor == "doorStatus": return None elif ftype == "attribute": - if self.sensor == "odometer": + if self.sensor == "odometer": return self.coordinator.data[self.sensor].items() elif self.sensor == "fuel": return self.coordinator.data[self.sensor].items() @@ -93,19 +98,14 @@ def get_value(self, ftype): return self.coordinator.data[self.sensor].items() elif self.sensor == "doorStatus": doors = dict() - for key,value in self.coordinator.data[self.sensor].items(): + for key, value in self.coordinator.data[self.sensor].items(): doors[key] = value["value"] return doors - - - - - @property def name(self): return "fordpass_" + self.sensor - + @property def state(self): return self.get_value("state") @@ -121,7 +121,3 @@ def device_state_attributes(self): @property def unit_of_measurement(self): return self.get_value("measurement") - - - - diff --git a/custom_components/fordpass/switch.py b/custom_components/fordpass/switch.py index a905cd1..323b9db 100644 --- a/custom_components/fordpass/switch.py +++ b/custom_components/fordpass/switch.py @@ -7,6 +7,7 @@ _LOGGER = logging.getLogger(__name__) + async def async_setup_entry(hass, config_entry, async_add_entities): """Add the Switch from the config.""" entry = hass.data[DOMAIN][config_entry.entry_id] @@ -19,7 +20,11 @@ class Switch(FordPassEntity, SwitchEntity): """Define the Switch for turning ignition off/on""" def __init__(self, coordinator): - super().__init__(device_id="fordpass_ignitionsw", name="fordpass_Ignition_Switch", coordinator=coordinator) + super().__init__( + device_id="fordpass_ignitionsw", + name="fordpass_Ignition_Switch", + coordinator=coordinator, + ) async def async_turn_on(self, **kwargs): await self.coordinator.hass.async_add_executor_job( @@ -27,17 +32,18 @@ async def async_turn_on(self, **kwargs): ) await self.coordinator.async_request_refresh() - async def async_turn_off(self, **kwargs): await self.coordinator.hass.async_add_executor_job( self.coordinator.vehicle.stop ) await self.coordinator.async_request_refresh() - @property def is_on(self): """Determine if the vehicle is started.""" - if self.coordinator.data is None or self.coordinator.data["remoteStartStatus"] is None: + if ( + self.coordinator.data is None + or self.coordinator.data["remoteStartStatus"] is None + ): return None return self.coordinator.data["remoteStartStatus"]["value"]