Skip to content

Commit

Permalink
Merge pull request #280 from itchannel/1.50
Browse files Browse the repository at this point in the history
1.50
  • Loading branch information
itchannel committed Sep 6, 2023
2 parents 6e9f996 + 2a82fa8 commit 0fb8acb
Show file tree
Hide file tree
Showing 17 changed files with 561 additions and 446 deletions.
24 changes: 24 additions & 0 deletions .gitlab-ci.yml
Original file line number Diff line number Diff line change
@@ -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
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand All @@ -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
Expand Down
61 changes: 32 additions & 29 deletions custom_components/fordpass/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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, {})
Expand All @@ -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]
Expand All @@ -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
}

Expand All @@ -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")
Expand Down Expand Up @@ -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
Expand All @@ -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__(
Expand All @@ -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:
Expand Down Expand Up @@ -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,
}
64 changes: 50 additions & 14 deletions custom_components/fordpass/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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"
Expand All @@ -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):
Expand All @@ -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 = {
Expand All @@ -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,
Expand Down
17 changes: 15 additions & 2 deletions custom_components/fordpass/device_tracker.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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")
Expand All @@ -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
Expand All @@ -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"
Loading

0 comments on commit 0fb8acb

Please sign in to comment.