homebridge-fordpass
Advanced tools
Comparing version 1.9.0 to 1.10.0-test.0
@@ -6,64 +6,62 @@ { | ||
"schema": { | ||
"username": { | ||
"title": "FordPass Username", | ||
"type": "string", | ||
"required": true | ||
}, | ||
"password": { | ||
"title": "FordPass Password", | ||
"type": "string", | ||
"required": true | ||
}, | ||
"options": { | ||
"title": "Advanced Settings", | ||
"expandable": true, | ||
"type": "object", | ||
"properties": { | ||
"region": { | ||
"title": "Region", | ||
"description": "Select the closest region to you.", | ||
"type": "string", | ||
"default": "US", | ||
"oneOf": [ | ||
{ "title": "US", "enum": ["71A3AD0A-CF46-4CCF-B473-FC7FE5BC4592"] }, | ||
{ "title": "CA", "enum": ["71A3AD0A-CF46-4CCF-B473-FC7FE5BC4592"] }, | ||
{ "title": "EU", "enum": ["1E8C7794-FF5F-49BC-9596-A1E0C86C5B19"] }, | ||
{ "title": "AU", "enum": ["5C80A6BB-CF0D-4A30-BDBF-FC804B5C1A98"] } | ||
] | ||
}, | ||
"batteryName": { | ||
"title": "Battery Name", | ||
"description": "The name to be used for the battery indicator.", | ||
"type": "string", | ||
"default": "Fuel Level" | ||
}, | ||
"autoRefresh": { | ||
"title": "Auto-refresh", | ||
"description": "Note: This will use your vehicle's battery to refresh data.", | ||
"type": "boolean", | ||
"default": false | ||
}, | ||
"refreshRate": { | ||
"title": "Refresh rate (in minutes)", | ||
"description": "Note: Faster refresh times will significantly drain your vehicle's battery.", | ||
"type": "integer", | ||
"default": 180, | ||
"minimum": 60, | ||
"maximum": 720, | ||
"condition": { | ||
"functionBody": "return model.options.autoRefresh === true;" | ||
} | ||
}, | ||
"chargingSwitch": { | ||
"title": "Charging Switch", | ||
"description": "Adds a button that can trigger automations when your electric vehicle begins charging.", | ||
"type": "boolean", | ||
"default": false | ||
}, | ||
"plugSwitch": { | ||
"title": "Plug Switch", | ||
"description": "Adds an occupancy sensor that can trigger automations when your electric vehicle is plugged into the charging port.", | ||
"type": "boolean", | ||
"default": false | ||
"type": "object", | ||
"properties": { | ||
"batteryName": { | ||
"title": "Battery Name", | ||
"description": "The name to be used for the battery indicator.", | ||
"type": "string", | ||
"default": "Fuel Level" | ||
}, | ||
"autoRefresh": { | ||
"title": "Auto-refresh", | ||
"description": "Note: This will use your vehicle's battery to refresh data.", | ||
"type": "boolean", | ||
"default": false | ||
}, | ||
"refreshRate": { | ||
"title": "Refresh rate (in minutes)", | ||
"description": "Note: Faster refresh times will significantly drain your vehicle's battery.", | ||
"type": "integer", | ||
"default": 180, | ||
"minimum": 60, | ||
"maximum": 720, | ||
"condition": { | ||
"functionBody": "return model.options.autoRefresh === true;" | ||
} | ||
}, | ||
"chargingSwitch": { | ||
"title": "Charging Switch", | ||
"description": "Adds a button that can trigger automations when your electric vehicle begins charging.", | ||
"type": "boolean", | ||
"default": false | ||
}, | ||
"plugSwitch": { | ||
"title": "Plug Switch", | ||
"description": "Adds an occupancy sensor that can trigger automations when your electric vehicle is plugged into the charging port.", | ||
"type": "boolean", | ||
"default": false | ||
}, | ||
"application_id": { | ||
"title": "Application ID", | ||
"type": "string", | ||
"required": true, | ||
"description": "Enter the application ID from the FordPass API app when you requested access. Usually starts with 1" | ||
}, | ||
"client_id": { | ||
"title": "Client ID", | ||
"type": "string", | ||
"required": true, | ||
"description": "Enter the client ID from the FordPass API app when you requested access. Usually starts with 1" | ||
}, | ||
"client_secret": { | ||
"title": "Client Secret", | ||
"type": "string", | ||
"required": true, | ||
"description": "Enter the client secret from the FordPass API app when you requested access. Usually starts with RHO" | ||
}, | ||
"code": { | ||
"title": "Code", | ||
"type": "string", | ||
"required": true, | ||
"description": "Enter the code from the FordPass API app" | ||
} | ||
@@ -70,0 +68,0 @@ } |
@@ -17,31 +17,25 @@ "use strict"; | ||
const axios_1 = __importDefault(require("axios")); | ||
const crypto_1 = __importDefault(require("crypto")); | ||
const base64url_1 = __importDefault(require("base64url")); | ||
const url_1 = require("url"); | ||
const vehiclesUrl = 'https://api.mps.ford.com/api/expdashboard/v1/details'; | ||
const userUrl = 'https://api.mps.ford.com/api/users'; | ||
const catWithRefreshTokenUrl = 'https://api.mps.ford.com/api/token/v2/cat-with-refresh-token'; | ||
const catWithCIAccessTokenUrl = 'https://api.mps.ford.com/api/token/v2/cat-with-ci-access-token'; | ||
const authorizeUrl = 'https://sso.ci.ford.com/v1.0/endpoint/default/authorize'; | ||
const tokenUrl = 'https://sso.ci.ford.com/oidc/endpoint/default/token'; | ||
const autonomicUrl = 'https://accounts.autonomic.ai/v1/auth/oidc/token'; | ||
const defaultAppId = '71A3AD0A-CF46-4CCF-B473-FC7FE5BC4592'; | ||
const clientId = '9fb503e0-715b-47e8-adfd-ad4b7770f73b'; | ||
const userAgent = 'FordPass/5 CFNetwork/1333.0.4 Darwin/21.5.0'; | ||
const randomStr = (len) => { | ||
let result = ''; | ||
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; | ||
const charactersLength = characters.length; | ||
for (let i = 0; i < len; i++) { | ||
result += characters.charAt(Math.floor(Math.random() * charactersLength)); | ||
} | ||
return result; | ||
}; | ||
const codeChallenge = (str) => { | ||
const hash = crypto_1.default.createHash('sha256'); | ||
hash.update(str); | ||
return (0, base64url_1.default)(hash.digest()); | ||
}; | ||
const form_data_1 = __importDefault(require("form-data")); | ||
const fs_1 = __importDefault(require("fs")); | ||
const authorizeUrl = 'https://dah2vb2cprod.b2clogin.com/914d88b1-3523-4bf6-9be4-1b96b4f6f919/oauth2/v2.0/token?p=B2C_1A_signup_signin_common'; | ||
const baseApiUrl = 'https://api.mps.ford.com/api/fordconnect'; | ||
const vehiclesUrl = '/v2/vehicles'; | ||
const vehicleStatusUrl = '/v1/vehicles/{vehicleId}/status'; | ||
const vehicleSatusRefreshUrl = '/v1/vehicles/{vehicleId}/statusrefresh/{commandId}'; | ||
const vehicleUnlockUrl = '/v1/vehicles/{vehicleId}/unlock'; | ||
const vehicleUnlockRefreshUrl = '/v1/vehicles/{vehicleId}/unlock/{commandId}'; | ||
const vehicleLockUrl = '/v1/vehicles/{vehicleId}/lock'; | ||
const vehicleLockRefreshUrl = '/v1/vehicles/{vehicleId}/lock/{commandId}'; | ||
const vehicleStartUrl = '/v1/vehicles/{vehicleId}/startEngine'; | ||
const vehicleStartRefreshUrl = '/v1/vehicles/{vehicleId}/start/{commandId}'; | ||
const vehicleStopUrl = '/v1/vehicles/{vehicleId}/stop'; | ||
const vehicleStopRefreshUrl = '/v1/vehicles/{vehicleId}/stop/{commandId}'; | ||
const vehicleStartChargeUrl = '/v1/vehicles/{vehicleId}/startCharge'; | ||
const vehicleStartChargeRefreshUrl = '/v1/vehicles/{vehicleId}/startCharge/{commandId}'; | ||
const vehicleStopChargeUrl = '/v1/vehicles/{vehicleId}/stopCharge'; | ||
const vehicleStopChargeRefreshUrl = '/v1/vehicles/{vehicleId}/stopCharge/{commandId}'; | ||
const vehicleInformationUrl = '/v3/vehicles/{vehicleId}'; | ||
const headers = { | ||
'User-Agent': userAgent, | ||
'User-Agent': 'Mozilla/5.0', | ||
'Accept-Language': 'en-US,en;q=0.9', | ||
@@ -51,233 +45,298 @@ 'Accept-Encoding': 'gzip, deflate, br', | ||
class Connection { | ||
constructor(config, log) { | ||
var _a; | ||
constructor(config, log, api) { | ||
this.config = config; | ||
this.log = log; | ||
this.applicationId = ((_a = config.options) === null || _a === void 0 ? void 0 : _a.region) || defaultAppId; | ||
this.api = api; | ||
this.credentials = this.getCredentials(); | ||
} | ||
refreshAuth() { | ||
getVehicles() { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
if (!this.credentials.access_token) { | ||
yield this.auth(); | ||
} | ||
const options = { | ||
method: 'GET', | ||
url: baseApiUrl + vehiclesUrl, | ||
headers: { | ||
'Content-Type': 'application/json', | ||
'Application-Id': this.config.application_id, | ||
Authorization: `Bearer ${this.credentials.access_token}`, | ||
}, | ||
}; | ||
try { | ||
if (this.config.refresh_token) { | ||
const result = yield axios_1.default.post(catWithRefreshTokenUrl, { | ||
refresh_token: this.config.refresh_token, | ||
}, { | ||
headers: Object.assign({ 'Content-Type': 'application/json', 'Application-Id': this.applicationId }, headers), | ||
}); | ||
if (result.status === 200 && result.data.access_token) { | ||
this.config.access_token = result.data.access_token; | ||
this.config.refresh_token = result.data.refresh_token; | ||
return this.getAutonomicToken(result.data.access_token); | ||
const result = yield (0, axios_1.default)(options); | ||
if (result.status < 300 && result.data) { | ||
const vehicles = []; | ||
for (const info of result.data.vehicles) { | ||
vehicles.push(Object.assign(Object.assign({}, info), result.data.vehicles[0])); | ||
} | ||
else { | ||
this.log.error(`Auth failed with status: ${result.status}`); | ||
} | ||
return vehicles; | ||
} | ||
return []; | ||
} | ||
catch (error) { | ||
this.log.error(`Error occurred during request: ${error.message}`); | ||
if (error.response) { | ||
this.log.error(`Response status: ${error.response.status}`); | ||
this.log.error(`Response data: ${JSON.stringify(error.response.data)}`); | ||
this.log.error(`Response headers: ${JSON.stringify(error.response.headers)}`); | ||
} | ||
else if (error.request) { | ||
this.log.error(`Request made but no response received: ${error.request}`); | ||
} | ||
else { | ||
return yield this.auth(); | ||
this.log.error(`Error details: ${JSON.stringify(error)}`); | ||
} | ||
return; | ||
return []; | ||
} | ||
catch (error) { | ||
this.log.error(`Auth failed with error: ${error.code || error.response.status}`); | ||
return; | ||
} | ||
}); | ||
} | ||
getUser() { | ||
getVehicleInformation(vehicleId) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
if (!this.config.access_token) { | ||
return; | ||
if (!this.credentials.access_token) { | ||
yield this.auth(); | ||
} | ||
const url = baseApiUrl + vehicleInformationUrl.replace('{vehicleId}', vehicleId); | ||
const options = { | ||
method: 'GET', | ||
url: userUrl, | ||
headers: Object.assign({ 'Content-Type': 'application/json', 'Auth-Token': this.config.access_token, 'Application-Id': this.applicationId }, headers), | ||
url: url, | ||
headers: { | ||
'Content-Type': 'application/json', | ||
'Application-Id': this.config.application_id, | ||
Authorization: `Bearer ${this.credentials.access_token}`, | ||
}, | ||
}; | ||
try { | ||
const result = yield (0, axios_1.default)(options); | ||
if (result.status === 200 && result.data) { | ||
return result.data.profile; | ||
const result = yield axios_1.default.request(options); | ||
if (result.status < 300 && result.data) { | ||
if (result.data.status === 'SUCCESS') { | ||
return result.data.vehicle; | ||
} | ||
return {}; | ||
} | ||
else { | ||
this.log.error(`User failed with status: ${result.status}`); | ||
this.log.error(`Vehicle info failed with status: ${result.status}`); | ||
} | ||
return; | ||
return {}; | ||
} | ||
catch (error) { | ||
this.log.error(`User failed with error: ${error.code || error.response.status}`); | ||
return; | ||
this.log.error(`Vehicle info failed with error: ${error.code || error.response.status}`); | ||
return {}; | ||
} | ||
}); | ||
} | ||
getVehicles() { | ||
issueCommand(vehicleId, command) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
const user = yield this.getUser(); | ||
if (!this.config.access_token || !user) { | ||
return []; | ||
if (!this.credentials.access_token) { | ||
yield this.auth(); | ||
} | ||
let url = ''; | ||
switch (command) { | ||
case 'unlock': | ||
url = vehicleUnlockUrl.replace('{vehicleId}', vehicleId); | ||
break; | ||
case 'lock': | ||
url = vehicleLockUrl.replace('{vehicleId}', vehicleId); | ||
break; | ||
case 'start': | ||
url = vehicleStartUrl.replace('{vehicleId}', vehicleId); | ||
break; | ||
case 'stop': | ||
url = vehicleStopUrl.replace('{vehicleId}', vehicleId); | ||
break; | ||
case 'startCharge': | ||
url = vehicleStartChargeUrl.replace('{vehicleId}', vehicleId); | ||
break; | ||
case 'stopCharge': | ||
url = vehicleStopChargeUrl.replace('{vehicleId}', vehicleId); | ||
break; | ||
case 'status': | ||
url = vehicleStatusUrl.replace('{vehicleId}', vehicleId); | ||
break; | ||
default: | ||
break; | ||
} | ||
const options = { | ||
method: 'POST', | ||
url: vehiclesUrl, | ||
headers: Object.assign({ 'Content-Type': 'application/json', 'Auth-Token': this.config.access_token, 'Application-Id': this.applicationId, locale: user.language, countryCode: user.country }, headers), | ||
data: JSON.stringify({ | ||
dashboardRefreshRequest: 'All', | ||
}), | ||
url: baseApiUrl + url.replace('{vehicleId}', vehicleId), | ||
headers: { | ||
'Content-Type': 'application/json', | ||
'Application-Id': this.config.application_id, | ||
Authorization: `Bearer ${this.credentials.access_token}`, | ||
}, | ||
}; | ||
try { | ||
const result = yield (0, axios_1.default)(options); | ||
const result = yield axios_1.default.request(options); | ||
if (result.status < 300 && result.data) { | ||
const vehicles = []; | ||
for (const info of result.data.userVehicles.vehicleDetails) { | ||
vehicles.push(Object.assign(Object.assign({}, info), result.data.vehicleProfile.find((v) => v.VIN === info.VIN))); | ||
} | ||
this.log.debug(`Found vehicles : ${JSON.stringify(vehicles, null, 2)}`); | ||
return vehicles; | ||
return result.data; | ||
} | ||
else { | ||
this.log.error(`Vehicles failed with status: ${result.status}`); | ||
this.log.error(`Command failed with status: ${JSON.stringify(result)}`); | ||
} | ||
return []; | ||
return {}; | ||
} | ||
catch (error) { | ||
this.log.error(`Vehicles failed with error: ${error.code || error.response.status}`); | ||
return []; | ||
this.log.error(`Command failed with error: ${JSON.stringify(error)}`); | ||
return {}; | ||
} | ||
}); | ||
} | ||
auth() { | ||
var _a; | ||
issueCommandRefresh(commandId, vehicleId, command) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
const numOfRedirects = 2; | ||
const code = randomStr(43); | ||
let cookies = undefined; | ||
const url = `${authorizeUrl}?redirect_uri=fordapp://userauthorized&response_type=code&scope=openid&max_age=3600&client_id=9fb503e0-715b-47e8-adfd-ad4b7770f73b&code_challenge=${codeChallenge(code)}&code_challenge_method=S256`; | ||
let nextUrl = url; | ||
for (let i = 0; i < numOfRedirects; i++) { | ||
const options = { | ||
method: 'GET', | ||
maxRedirects: 0, | ||
url: nextUrl, | ||
headers: Object.assign({}, headers), | ||
}; | ||
if (options.headers && cookies) { | ||
options.headers.cookie = cookies; | ||
} | ||
try { | ||
yield (0, axios_1.default)(options); | ||
throw new Error('Authentication failed'); | ||
} | ||
catch (err) { | ||
if (((_a = err === null || err === void 0 ? void 0 : err.response) === null || _a === void 0 ? void 0 : _a.status) === 302) { | ||
nextUrl = err.response.headers.location; | ||
cookies = err.response.headers['set-cookie']; | ||
} | ||
else { | ||
this.log.error(`Auth failed with status: ${err.status}`); | ||
} | ||
} | ||
if (!this.credentials.access_token) { | ||
yield this.auth(); | ||
} | ||
return this.doLogin(nextUrl, code, cookies); | ||
}); | ||
} | ||
doLogin(url, code_verifier, cookies) { | ||
var _a; | ||
return __awaiter(this, void 0, void 0, function* () { | ||
if (!this.config.username || !this.config.password) { | ||
throw new Error('Username or password is empty in config'); | ||
let url = ''; | ||
switch (command) { | ||
case 'unlock': | ||
url = vehicleUnlockRefreshUrl.replace('{vehicleId}', vehicleId); | ||
break; | ||
case 'lock': | ||
url = vehicleLockRefreshUrl.replace('{vehicleId}', vehicleId); | ||
break; | ||
case 'start': | ||
url = vehicleStartRefreshUrl.replace('{vehicleId}', vehicleId); | ||
break; | ||
case 'stop': | ||
url = vehicleStopRefreshUrl.replace('{vehicleId}', vehicleId); | ||
break; | ||
case 'startCharge': | ||
url = vehicleStartChargeRefreshUrl.replace('{vehicleId}', vehicleId); | ||
break; | ||
case 'stopCharge': | ||
url = vehicleStopChargeRefreshUrl.replace('{vehicleId}', vehicleId); | ||
break; | ||
case 'status': | ||
url = vehicleSatusRefreshUrl.replace('{vehicleId}', vehicleId); | ||
break; | ||
default: | ||
break; | ||
} | ||
const options = { | ||
method: 'POST', | ||
maxRedirects: 0, | ||
url: url, | ||
headers: Object.assign({ 'Content-Type': 'application/x-www-form-urlencoded', cookie: cookies }, headers), | ||
data: new url_1.URLSearchParams({ | ||
operation: 'verify', | ||
'login-form-type': 'pwd', | ||
username: this.config.username, | ||
password: this.config.password, | ||
}).toString(), | ||
method: 'GET', | ||
url: baseApiUrl + url.replace('{commandId}', commandId), | ||
headers: { | ||
'Content-Type': 'application/json', | ||
'Application-Id': this.config.application_id, | ||
Authorization: `Bearer ${this.credentials.access_token}`, | ||
}, | ||
}; | ||
try { | ||
yield (0, axios_1.default)(options); | ||
throw new Error('Authentication failed'); | ||
const result = yield axios_1.default.request(options); | ||
if (result.status < 300 && result.data) { | ||
let commandStatus = result.data.commandStatus; | ||
let tries = 10; | ||
while (commandStatus === 'QUEUED' && tries > 0) { | ||
yield new Promise((resolve) => setTimeout(resolve, 5000)); | ||
const refreshResult = yield (0, axios_1.default)(options); | ||
commandStatus = refreshResult.data.commandStatus; | ||
tries--; | ||
} | ||
return result.data; | ||
} | ||
else { | ||
this.log.error(`Command failed with status: ${JSON.stringify(result)}`); | ||
} | ||
return {}; | ||
} | ||
catch (err) { | ||
if (((_a = err === null || err === void 0 ? void 0 : err.response) === null || _a === void 0 ? void 0 : _a.status) === 302) { | ||
const nextUrl = err.response.headers.location; | ||
const cookies = err.response.headers['set-cookie']; | ||
return this.getAuthorizationCode(nextUrl, code_verifier, cookies); | ||
catch (error) { | ||
if (error.response) { | ||
this.log.error(`issueCommandRefresh Response status: ${error.response.status}`); | ||
this.log.error(`issueCommandRefresh Response data: ${JSON.stringify(error.response.data)}`); | ||
this.log.error(`issueCommandRefresh Response headers: ${JSON.stringify(error.response.headers)}`); | ||
} | ||
else if (error.request) { | ||
this.log.error(`issueCommandRefresh Request made but no response received: ${error.request}`); | ||
} | ||
else { | ||
this.log.error(`Auth failed with status: ${err.status}`); | ||
this.log.error(`issueCommandRefresh Error details: ${JSON.stringify(error)}`); | ||
} | ||
return {}; | ||
} | ||
}); | ||
} | ||
getAuthorizationCode(url, code_verifier, cookies) { | ||
var _a; | ||
auth() { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
const options = { | ||
method: 'GET', | ||
maxRedirects: 0, | ||
url: url, | ||
headers: Object.assign({ cookie: cookies }, headers), | ||
}; | ||
try { | ||
yield (0, axios_1.default)(options); | ||
throw new Error('Authentication failed'); | ||
this.credentials = this.getCredentials(); | ||
if (!this.config.code && !this.config.client_secret) { | ||
this.log.error('Missing code or client_secret'); | ||
return; | ||
} | ||
catch (err) { | ||
if (((_a = err === null || err === void 0 ? void 0 : err.response) === null || _a === void 0 ? void 0 : _a.status) === 302) { | ||
const nextUrl = err.response.headers.location; | ||
const params = new url_1.URLSearchParams(nextUrl.split('?')[1]); | ||
const code = params.get('code'); | ||
if (code) { | ||
return this.getAccessToken(code, code_verifier); | ||
} | ||
} | ||
else { | ||
this.log.error(`Auth failed with status: ${err.status}`); | ||
} | ||
if (!this.credentials.refresh_token) { | ||
return yield this.getAccessToken(); | ||
} | ||
else { | ||
const refreshToken = yield this.getRefreshToken(); | ||
return refreshToken; | ||
} | ||
}); | ||
} | ||
getAccessToken(code, code_verifier) { | ||
getAccessToken() { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
const options = { | ||
method: 'POST', | ||
url: tokenUrl, | ||
headers: Object.assign({ 'Content-Type': 'application/x-www-form-urlencoded' }, headers), | ||
data: new url_1.URLSearchParams({ | ||
client_id: clientId, | ||
grant_type: 'authorization_code', | ||
scope: 'openid', | ||
redirect_uri: 'fordapp://userauthorized', | ||
code: code, | ||
code_verifier: code_verifier, | ||
}).toString(), | ||
}; | ||
try { | ||
const res = yield (0, axios_1.default)(options); | ||
if (!this.config.code || !this.config.client_secret || !this.config.client_id) { | ||
this.log.error('Missing code or client_secret, or client_id'); | ||
return; | ||
} | ||
const data = new url_1.URLSearchParams(); | ||
data.append('grant_type', 'authorization_code'); | ||
data.append('client_id', this.config.client_id); | ||
data.append('client_secret', this.config.client_secret); | ||
data.append('code', this.config.code); | ||
data.append('redirect_url', 'https://localhost:3000'); | ||
const options = { | ||
method: 'post', | ||
url: authorizeUrl, | ||
headers: Object.assign({ 'Content-Type': 'application/x-www-form-urlencoded' }, headers), | ||
maxBodyLength: Infinity, | ||
data: data.toString(), | ||
}; | ||
const res = yield axios_1.default.request(options); | ||
if (res.status === 200 && res.data.access_token) { | ||
return this.getRefreshToken(res.data.access_token); | ||
this.log.debug('Successfully got token from FordPass API'); | ||
this.credentials.refresh_token = res.data.refresh_token; | ||
this.credentials.access_token = res.data.access_token; | ||
this.credentials.expires_in = res.data.expires_in; | ||
return this.getRefreshToken(); | ||
} | ||
} | ||
catch (err) { | ||
this.log.error(`Auth failed with error: ${err}`); | ||
catch (error) { | ||
this.log.error("Auth failed for FordPass. Please follow the FordPass API Setup instructions to retrieve the 'code'."); | ||
this.log.error(`Error occurred during request: ${error.message}`); | ||
if (error.response) { | ||
this.log.error(`getAccessToken Response status: ${error.response.status}`); | ||
this.log.error(`getAccessToken Response data: ${JSON.stringify(error.response.data)}`); | ||
this.log.error(`getAccessToken Response headers: ${JSON.stringify(error.response.headers)}`); | ||
} | ||
else if (error.request) { | ||
this.log.error(`Request made but no response received: ${error.request}`); | ||
} | ||
else { | ||
this.log.error(`getAccessToken Error details: ${JSON.stringify(error)}`); | ||
} | ||
} | ||
}); | ||
} | ||
getRefreshToken(access_token) { | ||
getRefreshToken() { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
try { | ||
const res = yield axios_1.default.post(catWithCIAccessTokenUrl, { | ||
ciToken: access_token, | ||
}, { | ||
headers: Object.assign({ 'Content-Type': 'application/json', 'Application-Id': this.applicationId }, headers), | ||
}); | ||
const data = new form_data_1.default(); | ||
data.append('grant_type', 'refresh_token'); | ||
data.append('refresh_token', this.credentials.refresh_token); | ||
data.append('client_id', this.config.client_id); | ||
data.append('client_secret', this.config.client_secret); | ||
const options = { | ||
method: 'post', | ||
maxBodyLength: Infinity, | ||
url: authorizeUrl, | ||
headers: Object.assign({ 'Content-Type': 'multipart/form-data' }, data.getHeaders()), | ||
data: data, | ||
}; | ||
const res = yield axios_1.default.request(options); | ||
if (res.status === 200 && res.data.access_token) { | ||
this.config.access_token = res.data.access_token; | ||
this.config.refresh_token = res.data.refresh_token; | ||
return this.getAutonomicToken(res.data.access_token); | ||
this.credentials.access_token = res.data.access_token; | ||
this.credentials.refresh_token = res.data.refresh_token; | ||
this.credentials.expires_in = res.data.expires_in; | ||
this.createOrUpdateCredentials(); | ||
return res.data; | ||
} | ||
@@ -289,34 +348,45 @@ else { | ||
catch (error) { | ||
this.log.error(`Auth failed with error: ${error.code || error.response.status}`); | ||
if (error.response) { | ||
this.log.error(`getRefreshToken Response status: ${error.response.status}`); | ||
this.log.error(`getRefreshToken Response data: ${JSON.stringify(error.response.data)}`); | ||
this.log.error(`getRefreshToken Response headers: ${JSON.stringify(error.response.headers)}`); | ||
} | ||
else if (error.request) { | ||
this.log.error(`getRefreshToken Request made but no response received: ${error.request}`); | ||
} | ||
else { | ||
this.log.error(`getRefreshToken Error details: ${JSON.stringify(error)}`); | ||
} | ||
} | ||
}); | ||
} | ||
getAutonomicToken(access_token) { | ||
createOrUpdateCredentials() { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
const options = { | ||
method: 'POST', | ||
url: autonomicUrl, | ||
headers: Object.assign({ 'Content-Type': 'application/x-www-form-urlencoded' }, headers), | ||
data: new url_1.URLSearchParams({ | ||
client_id: 'fordpass-prod', | ||
grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', | ||
subject_token: access_token, | ||
subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', | ||
subject_issuer: 'fordpass', | ||
}).toString(), | ||
}; | ||
try { | ||
const res = yield (0, axios_1.default)(options); | ||
if (res.status === 200 && res.data.access_token) { | ||
this.config.autonomic_token = res.data.access_token; | ||
return res.data; | ||
} | ||
const storagePath = this.api.user.storagePath(); | ||
const credentialsPath = `${storagePath}/fordpass-security-credentials.json`; | ||
if (!fs_1.default.existsSync(credentialsPath)) { | ||
fs_1.default.writeFileSync(credentialsPath, JSON.stringify(this.credentials)); | ||
} | ||
catch (err) { | ||
this.log.error(`Auth failed with error: ${err}`); | ||
else { | ||
fs_1.default.writeFileSync(credentialsPath, JSON.stringify(this.credentials)); | ||
} | ||
return this.getCredentials(); | ||
}); | ||
} | ||
getCredentials() { | ||
try { | ||
const storagePath = this.api.user.storagePath(); | ||
const credentialsPath = `${storagePath}/fordpass-security-credentials.json`; | ||
if (fs_1.default.existsSync(credentialsPath)) { | ||
const credentials = JSON.parse(fs_1.default.readFileSync(credentialsPath).toString()); | ||
return credentials; | ||
} | ||
} | ||
catch (error) { | ||
this.log.error(`getCredentials Error occurred retrieving credentials: ${error}`); | ||
} | ||
return { access_token: '', refresh_token: '', expires_in: 0 }; | ||
} | ||
} | ||
exports.Connection = Connection; | ||
//# sourceMappingURL=fordpass-connection.js.map |
@@ -11,23 +11,9 @@ "use strict"; | ||
}; | ||
var __importDefault = (this && this.__importDefault) || function (mod) { | ||
return (mod && mod.__esModule) ? mod : { "default": mod }; | ||
}; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.Vehicle = void 0; | ||
const axios_1 = __importDefault(require("axios")); | ||
const vehicle_1 = require("./types/vehicle"); | ||
const fordpass_connection_1 = require("./fordpass-connection"); | ||
const events_1 = require("events"); | ||
const defaultHeaders = { | ||
'Content-Type': 'application/json', | ||
'User-Agent': 'FordPass/5 CFNetwork/1333.0.4 Darwin/21.5.0', | ||
}; | ||
const defaultAppId = '71A3AD0A-CF46-4CCF-B473-FC7FE5BC4592'; | ||
const fordAPIUrl = 'https://api.autonomic.ai/v1beta/telemetry/sources/fordpass/vehicles/'; | ||
const commandUrl = 'https://api.autonomic.ai/v1/command/vehicles/'; | ||
const handleError = function (name, status, log) { | ||
log.error(`${name} failed with status: ${status}`); | ||
}; | ||
class Vehicle extends events_1.EventEmitter { | ||
constructor(name, vin, config, log) { | ||
var _a, _b, _c; | ||
constructor(name, vehicleId, config, log, api) { | ||
super(); | ||
@@ -37,75 +23,96 @@ this.updating = false; | ||
this.log = log; | ||
this.api = api; | ||
this.name = name; | ||
this.vin = vin; | ||
this.autoRefresh = ((_a = config.options) === null || _a === void 0 ? void 0 : _a.autoRefresh) || false; | ||
this.refreshRate = ((_b = config.options) === null || _b === void 0 ? void 0 : _b.refreshRate) || 180; | ||
this.applicationId = ((_c = config.options) === null || _c === void 0 ? void 0 : _c.region) || defaultAppId; | ||
this.vehicleId = vehicleId; | ||
this.autoRefresh = config.autoRefresh || false; | ||
this.refreshRate = config.refreshRate || 180; | ||
} | ||
status() { | ||
issueCommand(command) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
if (!this.config.autonomic_token) { | ||
return; | ||
if (this.updating) { | ||
return ''; | ||
} | ||
const url = fordAPIUrl + this.vin; | ||
const options = { | ||
url: url, | ||
headers: defaultHeaders, | ||
}; | ||
if (options.headers) { | ||
options.headers['Application-Id'] = this.applicationId; | ||
options.headers.Authorization = `Bearer ${this.config.autonomic_token}`; | ||
this.updating = true; | ||
let commandType = ''; | ||
switch (command) { | ||
case vehicle_1.Command.START: { | ||
commandType = 'remoteStart'; | ||
break; | ||
} | ||
case vehicle_1.Command.STOP: { | ||
commandType = 'stop'; | ||
break; | ||
} | ||
case vehicle_1.Command.LOCK: { | ||
commandType = 'lock'; | ||
break; | ||
} | ||
case vehicle_1.Command.UNLOCK: { | ||
commandType = 'unlock'; | ||
break; | ||
} | ||
case vehicle_1.Command.REFRESH: { | ||
commandType = 'status'; | ||
break; | ||
} | ||
default: { | ||
this.log.error('invalid command'); | ||
break; | ||
} | ||
} | ||
if (!this.updating) { | ||
this.updating = true; | ||
if (commandType) { | ||
try { | ||
const result = yield (0, axios_1.default)(options); | ||
if (result.status === 200) { | ||
this.info = result.data; | ||
return this.info; | ||
const result = yield new fordpass_connection_1.Connection(this.config, this.log, this.api).issueCommand(this.vehicleId, commandType); | ||
if (result) { | ||
if (command !== vehicle_1.Command.REFRESH) { | ||
yield new fordpass_connection_1.Connection(this.config, this.log, this.api).issueCommand(this.vehicleId, 'status'); | ||
} | ||
this.updating = false; | ||
return result.commandId; | ||
} | ||
} | ||
catch (error) { | ||
if (error.code !== 'ETIMEDOUT') { | ||
this.log.error(`Status failed with error: ${error.code || error.response.status}`); | ||
this.updating = false; | ||
if (error.response) { | ||
this.log.error(`Response status: ${error.response.status}`); | ||
this.log.error(`Response data: ${JSON.stringify(error.response.data)}`); | ||
this.log.error(`Response headers: ${JSON.stringify(error.response.headers)}`); | ||
} | ||
else if (error.request) { | ||
this.log.error(`Request made but no response received: ${error.request}`); | ||
} | ||
else { | ||
handleError('Status', error.status, this.log); | ||
this.log.error(`Error details: ${JSON.stringify(error)}`); | ||
} | ||
} | ||
finally { | ||
this.updating = false; | ||
this.emit('updated'); | ||
} | ||
} | ||
else { | ||
yield (0, events_1.once)(this, 'updated'); | ||
return this.info; | ||
} | ||
return ''; | ||
}); | ||
} | ||
issueCommand(command) { | ||
issueCommandRefresh(commandId, command) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
if (!this.config.autonomic_token) { | ||
if (this.updating) { | ||
return ''; | ||
} | ||
let type = ''; | ||
this.updating = true; | ||
let commandType = ''; | ||
switch (command) { | ||
case vehicle_1.Command.START: { | ||
type = 'remoteStart'; | ||
commandType = 'remoteStart'; | ||
break; | ||
} | ||
case vehicle_1.Command.STOP: { | ||
type = 'cancelRemoteStart'; | ||
commandType = 'stop'; | ||
break; | ||
} | ||
case vehicle_1.Command.LOCK: { | ||
type = 'lock'; | ||
commandType = 'lock'; | ||
break; | ||
} | ||
case vehicle_1.Command.UNLOCK: { | ||
type = 'unlock'; | ||
commandType = 'unlock'; | ||
break; | ||
} | ||
case vehicle_1.Command.REFRESH: { | ||
type = 'statusRefresh'; | ||
commandType = 'status'; | ||
break; | ||
@@ -118,29 +125,23 @@ } | ||
} | ||
if (type) { | ||
const url = commandUrl + this.vin + '/commands'; | ||
const options = { | ||
method: 'POST', | ||
url: url, | ||
headers: defaultHeaders, | ||
data: { | ||
properties: {}, | ||
tags: {}, | ||
type: type, | ||
wakeUp: true, | ||
}, | ||
}; | ||
if (options.headers) { | ||
options.headers['Application-Id'] = this.applicationId; | ||
options.headers.Authorization = `Bearer ${this.config.autonomic_token}`; | ||
} | ||
if (commandType) { | ||
try { | ||
const result = yield (0, axios_1.default)(options); | ||
if (result.status < 200 && result.status > 299) { | ||
handleError('IssueCommand', result.status, this.log); | ||
return ''; | ||
const result = yield new fordpass_connection_1.Connection(this.config, this.log, this.api).issueCommandRefresh(commandId, this.vehicleId, commandType); | ||
if (result) { | ||
this.updating = false; | ||
return result; | ||
} | ||
return result.data.id; | ||
} | ||
catch (error) { | ||
this.log.error(`Command failed with error: ${error.code || error.response.status}`); | ||
this.updating = false; | ||
if (error.response) { | ||
this.log.error(`issueCommandRefresh Response status: ${error.response.status}`); | ||
this.log.error(`issueCommandRefresh Response data: ${JSON.stringify(error.response.data)}`); | ||
this.log.error(`issueCommandRefresh Response headers: ${JSON.stringify(error.response.headers)}`); | ||
} | ||
else if (error.request) { | ||
this.log.error(`issueCommandRefresh Request made but no response received: ${error.request}`); | ||
} | ||
else { | ||
this.log.error(`issueCommandRefresh Error details: ${JSON.stringify(error)}`); | ||
} | ||
} | ||
@@ -151,24 +152,11 @@ } | ||
} | ||
commandStatus(commandId) { | ||
retrieveVehicleInfo() { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
if (!this.config.autonomic_token) { | ||
return; | ||
const result = yield new fordpass_connection_1.Connection(this.config, this.log, this.api).getVehicleInformation(this.vehicleId); | ||
if (result) { | ||
this.log.debug(`Retrieved vehicle information for vehicle. Mileage: ${result.vehicleDetails.mileage} Distance to E: ${result.vehicleDetails.batteryChargeLevel.distanceToEmpty * 0.621371} Tire Pressure Warning: ${result.vehicleStatus.tirePressureWarning}`); | ||
this.info = result; | ||
this.vehicleId = result.vehicleId; | ||
this.name = result.nickName; | ||
} | ||
const url = commandUrl + this.vin + '/commands/' + commandId; | ||
const options = { | ||
method: 'GET', | ||
url: url, | ||
headers: defaultHeaders, | ||
}; | ||
if (options.headers) { | ||
options.headers['Application-Id'] = this.applicationId; | ||
options.headers.Authorization = `Bearer ${this.config.autonomic_token}`; | ||
} | ||
try { | ||
const result = yield (0, axios_1.default)(options); | ||
return result.data; | ||
} | ||
catch (error) { | ||
this.log.error(`CommandStatus failed with error: ${error.code || error.response.status}`); | ||
} | ||
return; | ||
@@ -175,0 +163,0 @@ }); |
@@ -22,4 +22,4 @@ "use strict"; | ||
this.accessories = []; | ||
this.vehicles = []; | ||
this.pendingLockUpdate = false; | ||
this.pendingStatusUpdate = false; | ||
this.log = log; | ||
@@ -31,4 +31,4 @@ this.api = api; | ||
} | ||
if (!config.username || !config.password) { | ||
this.log.error('Please add a userame and password to your config.json'); | ||
if (!config.code || !config.client_secret) { | ||
this.log.error('Please add a code and client_secret to your config.json. See README.md for more information.'); | ||
return; | ||
@@ -39,4 +39,2 @@ } | ||
configureAccessory(accessory) { | ||
var _a, _b, _c; | ||
const self = this; | ||
this.log.info(`Configuring accessory ${accessory.displayName}`); | ||
@@ -46,3 +44,3 @@ accessory.on("identify", () => { | ||
}); | ||
const vehicle = new fordpass_1.Vehicle(accessory.context.name, accessory.context.vin, this.config, this.log); | ||
this.vehicle = new fordpass_1.Vehicle(accessory.context.name, accessory.context.vin, this.config, this.log, this.api); | ||
const fordAccessory = new accessory_1.FordpassAccessory(accessory); | ||
@@ -52,4 +50,4 @@ const defaultState = hap.Characteristic.LockTargetState.UNSECURED; | ||
const switchService = fordAccessory.createService(hap.Service.Switch); | ||
const batteryService = fordAccessory.createService(hap.Service.Battery, ((_a = this.config.options) === null || _a === void 0 ? void 0 : _a.batteryName) || 'Fuel Level'); | ||
if ((_b = this.config.options) === null || _b === void 0 ? void 0 : _b.chargingSwitch) { | ||
const batteryService = fordAccessory.createService(hap.Service.Battery, this.config.batteryName || 'Fuel Level'); | ||
if (this.config.chargingSwitch) { | ||
fordAccessory.createService(hap.Service.OccupancySensor, 'Charging'); | ||
@@ -60,3 +58,3 @@ } | ||
} | ||
if ((_c = this.config.options) === null || _c === void 0 ? void 0 : _c.plugSwitch) { | ||
if (this.config.plugSwitch) { | ||
fordAccessory.createService(hap.Service.OccupancySensor, 'Plug'); | ||
@@ -72,4 +70,14 @@ } | ||
.on("set", (value, callback) => __awaiter(this, void 0, void 0, function* () { | ||
this.log.debug(`${value ? 'Locking' : 'Unlocking'} ${accessory.displayName}`); | ||
let commandId = ''; | ||
var _a, _b, _c, _d, _e, _f, _g, _h, _j; | ||
this.log.debug(` | ||
SET ${value ? 'Locking' : 'Unlocking'} ${accessory.displayName} | ||
${(_c = (_b = (_a = this.vehicle) === null || _a === void 0 ? void 0 : _a.info) === null || _b === void 0 ? void 0 : _b.vehicleStatus.lockStatus) === null || _c === void 0 ? void 0 : _c.value}`); | ||
if (value === (((_f = (_e = (_d = this.vehicle) === null || _d === void 0 ? void 0 : _d.info) === null || _e === void 0 ? void 0 : _e.vehicleStatus.lockStatus) === null || _f === void 0 ? void 0 : _f.value) === 'LOCKED') | ||
? hap.Characteristic.LockTargetState.SECURED | ||
: hap.Characteristic.LockTargetState.UNSECURED) { | ||
this.log.debug('LOCK is already in the requested state'); | ||
callback(); | ||
return; | ||
} | ||
this.log.debug(`SET ${value ? 'Locking' : 'Unlocking'} ${accessory.displayName}`); | ||
let command = vehicle_1.Command.LOCK; | ||
@@ -79,27 +87,16 @@ if (value === hap.Characteristic.LockTargetState.UNSECURED) { | ||
} | ||
commandId = yield vehicle.issueCommand(command); | ||
let tries = 30; | ||
this.pendingLockUpdate = true; | ||
const self = this; | ||
const interval = setInterval(() => __awaiter(this, void 0, void 0, function* () { | ||
if (tries > 0) { | ||
const status = yield vehicle.commandStatus(commandId); | ||
if ((status === null || status === void 0 ? void 0 : status.currentStatus) === 'SUCCESS') { | ||
lockService.updateCharacteristic(hap.Characteristic.LockCurrentState, value); | ||
self.pendingLockUpdate = false; | ||
clearInterval(interval); | ||
} | ||
tries--; | ||
} | ||
else { | ||
self.pendingLockUpdate = false; | ||
clearInterval(interval); | ||
} | ||
}), 3000); | ||
callback(undefined, value); | ||
yield this.vehicle.issueCommand(command); | ||
this.log.debug('Waiting 6 seconds to update vehicle info'); | ||
yield new Promise((resolve) => setTimeout(resolve, 6000)); | ||
this.log.debug('Done waiting...Updating vehicle info'); | ||
yield this.vehicle.retrieveVehicleInfo(); | ||
this.log.debug(`Lock status is now: ${(_j = (_h = (_g = this.vehicle) === null || _g === void 0 ? void 0 : _g.info) === null || _h === void 0 ? void 0 : _h.vehicleStatus.lockStatus) === null || _j === void 0 ? void 0 : _j.value}`); | ||
this.pendingLockUpdate = false; | ||
callback(); | ||
})) | ||
.on("get", (callback) => __awaiter(this, void 0, void 0, function* () { | ||
var _d, _e, _f; | ||
var _k, _l, _m, _o, _p; | ||
let lockNumber = hap.Characteristic.LockTargetState.UNSECURED; | ||
const lockStatus = ((_e = (_d = vehicle === null || vehicle === void 0 ? void 0 : vehicle.info) === null || _d === void 0 ? void 0 : _d.metrics.doorLockStatus.find((x) => x.vehicleDoor === 'ALL_DOORS')) === null || _e === void 0 ? void 0 : _e.value) || 'LOCKED'; | ||
let lockStatus = ((_m = (_l = (_k = this.vehicle) === null || _k === void 0 ? void 0 : _k.info) === null || _l === void 0 ? void 0 : _l.vehicleStatus.lockStatus) === null || _m === void 0 ? void 0 : _m.value) || 'LOCKED'; | ||
if (lockStatus === 'LOCKED') { | ||
@@ -109,18 +106,33 @@ lockNumber = hap.Characteristic.LockTargetState.SECURED; | ||
callback(undefined, lockNumber); | ||
if (!this.config.access_token || !vehicle) { | ||
return; | ||
} | ||
const status = yield vehicle.status(); | ||
if (status) { | ||
let lockNumber = hap.Characteristic.LockTargetState.UNSECURED; | ||
const lockStatus = (_f = status.metrics.doorLockStatus.find((x) => x.vehicleDoor === 'ALL_DOORS')) === null || _f === void 0 ? void 0 : _f.value; | ||
if (lockStatus === 'LOCKED') { | ||
lockNumber = hap.Characteristic.LockTargetState.SECURED; | ||
if (this.vehicle && this.vehicle.vehicleId) { | ||
if (!this.pendingStatusUpdate) { | ||
this.pendingStatusUpdate = true; | ||
yield this.vehicle.issueCommand(vehicle_1.Command.REFRESH); | ||
yield new Promise((resolve) => setTimeout(resolve, 6000)); | ||
yield this.vehicle.retrieveVehicleInfo(); | ||
this.pendingStatusUpdate = false; | ||
lockStatus = ((_p = (_o = this.vehicle.info) === null || _o === void 0 ? void 0 : _o.vehicleStatus.lockStatus) === null || _p === void 0 ? void 0 : _p.value) || 'LOCKED'; | ||
lockNumber = | ||
lockStatus === 'LOCKED' | ||
? hap.Characteristic.LockTargetState.SECURED | ||
: hap.Characteristic.LockTargetState.UNSECURED; | ||
lockService.updateCharacteristic(hap.Characteristic.LockCurrentState, lockNumber); | ||
lockService.updateCharacteristic(hap.Characteristic.LockTargetState, lockNumber); | ||
} | ||
lockService.updateCharacteristic(hap.Characteristic.LockCurrentState, lockNumber); | ||
lockService.updateCharacteristic(hap.Characteristic.LockTargetState, lockNumber); | ||
else { | ||
const interval = setInterval(() => { | ||
var _a, _b, _c; | ||
if (!this.pendingStatusUpdate) { | ||
clearInterval(interval); | ||
lockStatus = ((_c = (_b = (_a = this.vehicle) === null || _a === void 0 ? void 0 : _a.info) === null || _b === void 0 ? void 0 : _b.vehicleStatus.lockStatus) === null || _c === void 0 ? void 0 : _c.value) || 'LOCKED'; | ||
lockNumber = | ||
lockStatus === 'LOCKED' | ||
? hap.Characteristic.LockTargetState.SECURED | ||
: hap.Characteristic.LockTargetState.UNSECURED; | ||
lockService.updateCharacteristic(hap.Characteristic.LockCurrentState, lockNumber); | ||
lockService.updateCharacteristic(hap.Characteristic.LockTargetState, lockNumber); | ||
} | ||
}, 3000); | ||
} | ||
} | ||
else { | ||
self.log.error(`Cannot get information for ${accessory.displayName} lock`); | ||
} | ||
})); | ||
@@ -131,30 +143,46 @@ switchService | ||
.on("set", (value, callback) => __awaiter(this, void 0, void 0, function* () { | ||
var _q, _r, _s, _t, _u, _v, _w; | ||
this.log.debug(`SET ${value ? 'Starting' : 'Stopping'} ${accessory.displayName} ${(_s = (_r = (_q = this.vehicle) === null || _q === void 0 ? void 0 : _q.info) === null || _r === void 0 ? void 0 : _r.vehicleStatus.lockStatus) === null || _s === void 0 ? void 0 : _s.value}`); | ||
if (value === (((_u = (_t = this.vehicle) === null || _t === void 0 ? void 0 : _t.info) === null || _u === void 0 ? void 0 : _u.vehicleStatus.ignitionStatus.value) === 'ON')) { | ||
this.log.debug('Engine is already in the requested state'); | ||
callback(); | ||
return; | ||
} | ||
this.log.debug(`${value ? 'Starting' : 'Stopping'} ${accessory.displayName}`); | ||
if (value) { | ||
yield vehicle.issueCommand(vehicle_1.Command.START); | ||
yield this.vehicle.issueCommand(vehicle_1.Command.START); | ||
} | ||
else { | ||
yield vehicle.issueCommand(vehicle_1.Command.STOP); | ||
yield this.vehicle.issueCommand(vehicle_1.Command.STOP); | ||
} | ||
callback(undefined, value); | ||
yield new Promise((resolve) => setTimeout(resolve, 6000)); | ||
yield this.vehicle.retrieveVehicleInfo(); | ||
this.log.debug(`Start status is now: ${(_w = (_v = this.vehicle) === null || _v === void 0 ? void 0 : _v.info) === null || _w === void 0 ? void 0 : _w.vehicleStatus.ignitionStatus.value}`); | ||
callback(); | ||
})) | ||
.on("get", (callback) => __awaiter(this, void 0, void 0, function* () { | ||
var _g; | ||
const engineStatus = ((_g = vehicle === null || vehicle === void 0 ? void 0 : vehicle.info) === null || _g === void 0 ? void 0 : _g.metrics.ignitionStatus.value) || 'OFF'; | ||
var _x, _y, _z, _0; | ||
let engineStatus = ((_y = (_x = this.vehicle) === null || _x === void 0 ? void 0 : _x.info) === null || _y === void 0 ? void 0 : _y.vehicleStatus.ignitionStatus.value) || 'OFF'; | ||
callback(undefined, engineStatus); | ||
if (!this.config.access_token) { | ||
return; | ||
} | ||
const status = yield vehicle.status(); | ||
if (status) { | ||
let started = true; | ||
const engineStatus = status.metrics.ignitionStatus.value; | ||
if (engineStatus === 'OFF') { | ||
started = false; | ||
if (this.vehicle && this.vehicle.vehicleId) { | ||
if (!this.pendingStatusUpdate) { | ||
this.pendingStatusUpdate = true; | ||
yield this.vehicle.issueCommand(vehicle_1.Command.REFRESH); | ||
yield new Promise((resolve) => setTimeout(resolve, 6000)); | ||
yield this.vehicle.retrieveVehicleInfo(); | ||
this.pendingStatusUpdate = false; | ||
engineStatus = ((_0 = (_z = this.vehicle) === null || _z === void 0 ? void 0 : _z.info) === null || _0 === void 0 ? void 0 : _0.vehicleStatus.ignitionStatus.value) || 'OFF'; | ||
switchService.getCharacteristic(hap.Characteristic.On).updateValue(engineStatus === 'ON'); | ||
} | ||
switchService.updateCharacteristic(hap.Characteristic.On, started); | ||
else { | ||
const interval = setInterval(() => { | ||
var _a, _b; | ||
if (!this.pendingStatusUpdate) { | ||
clearInterval(interval); | ||
engineStatus = ((_b = (_a = this.vehicle) === null || _a === void 0 ? void 0 : _a.info) === null || _b === void 0 ? void 0 : _b.vehicleStatus.ignitionStatus.value) || 'OFF'; | ||
switchService.getCharacteristic(hap.Characteristic.On).updateValue(engineStatus === 'ON'); | ||
} | ||
}, 3000); | ||
} | ||
} | ||
else { | ||
self.log.error(`Cannot get information for ${accessory.displayName} engine`); | ||
} | ||
})); | ||
@@ -165,5 +193,9 @@ batteryService | ||
.on("get", (callback) => __awaiter(this, void 0, void 0, function* () { | ||
var _h, _j, _k, _l, _m, _o, _p, _q; | ||
const fuel = (_j = (_h = vehicle === null || vehicle === void 0 ? void 0 : vehicle.info) === null || _h === void 0 ? void 0 : _h.metrics.fuelLevel) === null || _j === void 0 ? void 0 : _j.value; | ||
const battery = (_l = (_k = vehicle === null || vehicle === void 0 ? void 0 : vehicle.info) === null || _k === void 0 ? void 0 : _k.metrics.xevBatteryStateOfCharge) === null || _l === void 0 ? void 0 : _l.value; | ||
var _1, _2, _3, _4, _5, _6, _7, _8, _9; | ||
if (!this.vehicle) { | ||
callback(undefined, 100); | ||
return; | ||
} | ||
const fuel = (_3 = (_2 = (_1 = this.vehicle) === null || _1 === void 0 ? void 0 : _1.info) === null || _2 === void 0 ? void 0 : _2.vehicleStatus.fuelLevel) === null || _3 === void 0 ? void 0 : _3.value; | ||
const battery = (_6 = (_5 = (_4 = this.vehicle) === null || _4 === void 0 ? void 0 : _4.info) === null || _5 === void 0 ? void 0 : _5.vehicleDetails.batteryChargeLevel) === null || _6 === void 0 ? void 0 : _6.value; | ||
let level = fuel || battery || 100; | ||
@@ -177,72 +209,56 @@ if (level > 100) { | ||
callback(undefined, level); | ||
if (!this.config.access_token) { | ||
return; | ||
} | ||
const status = yield vehicle.status(); | ||
if (status) { | ||
const fuel = (_m = status.metrics.fuelLevel) === null || _m === void 0 ? void 0 : _m.value; | ||
const battery = (_o = status.metrics.xevBatteryStateOfCharge) === null || _o === void 0 ? void 0 : _o.value; | ||
const chargingStatus = (_q = (_p = vehicle === null || vehicle === void 0 ? void 0 : vehicle.info) === null || _p === void 0 ? void 0 : _p.metrics.xevBatteryChargeDisplayStatus) === null || _q === void 0 ? void 0 : _q.value; | ||
let level = fuel || battery || 100; | ||
if (level > 100) { | ||
level = 100; | ||
const chargingStatus = (_9 = (_8 = (_7 = this.vehicle) === null || _7 === void 0 ? void 0 : _7.info) === null || _8 === void 0 ? void 0 : _8.vehicleStatus.chargingStatus) === null || _9 === void 0 ? void 0 : _9.value; | ||
batteryService.updateCharacteristic(hap.Characteristic.BatteryLevel, level); | ||
if (battery) { | ||
if (chargingStatus === 'ChargingAC') { | ||
batteryService.updateCharacteristic(hap.Characteristic.ChargingState, hap.Characteristic.ChargingState.CHARGING); | ||
} | ||
if (level < 0) { | ||
level = 0; | ||
} | ||
batteryService.updateCharacteristic(hap.Characteristic.BatteryLevel, level); | ||
if (battery) { | ||
if (chargingStatus === 'ChargingAC') { | ||
batteryService.updateCharacteristic(hap.Characteristic.ChargingState, hap.Characteristic.ChargingState.CHARGING); | ||
} | ||
else { | ||
batteryService.updateCharacteristic(hap.Characteristic.ChargingState, hap.Characteristic.ChargingState.NOT_CHARGING); | ||
} | ||
} | ||
else { | ||
batteryService.updateCharacteristic(hap.Characteristic.ChargingState, hap.Characteristic.ChargingState.NOT_CHARGEABLE); | ||
batteryService.updateCharacteristic(hap.Characteristic.ChargingState, hap.Characteristic.ChargingState.NOT_CHARGING); | ||
} | ||
if (level < 10) { | ||
batteryService.updateCharacteristic(hap.Characteristic.StatusLowBattery, hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW); | ||
} | ||
else { | ||
batteryService.updateCharacteristic(hap.Characteristic.StatusLowBattery, hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL); | ||
} | ||
} | ||
else { | ||
self.log.error(`Cannot get information for ${accessory.displayName} engine`); | ||
batteryService.updateCharacteristic(hap.Characteristic.ChargingState, hap.Characteristic.ChargingState.NOT_CHARGEABLE); | ||
} | ||
if (level < 20) { | ||
batteryService.updateCharacteristic(hap.Characteristic.StatusLowBattery, hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW); | ||
} | ||
else { | ||
batteryService.updateCharacteristic(hap.Characteristic.StatusLowBattery, hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL); | ||
} | ||
})); | ||
this.vehicles.push(vehicle); | ||
this.accessories.push(accessory); | ||
} | ||
didFinishLaunching() { | ||
var _a; | ||
return __awaiter(this, void 0, void 0, function* () { | ||
const self = this; | ||
const ford = new fordpass_connection_1.Connection(this.config, this.log); | ||
this.log.debug('Configuring FordPass'); | ||
const ford = new fordpass_connection_1.Connection(this.config, this.log, this.api); | ||
const authInfo = yield ford.auth(); | ||
if (authInfo) { | ||
setInterval(() => __awaiter(this, void 0, void 0, function* () { | ||
self.log.debug('Reauthenticating with refresh token'); | ||
yield ford.refreshAuth(); | ||
}), authInfo.expires_in * 1000 - 10000); | ||
yield this.addVehicles(ford); | ||
yield this.updateVehicles(); | ||
yield this.refreshVehicles(); | ||
this.log.debug('Reauthenticating with refresh token'); | ||
yield ford.getRefreshToken(); | ||
}), ((_a = authInfo.expires_in) !== null && _a !== void 0 ? _a : 0) * 1000 - 10000); | ||
yield this.addVehicle(ford); | ||
yield this.updateVehicle(); | ||
yield this.refreshVehicle(); | ||
setInterval(() => __awaiter(this, void 0, void 0, function* () { | ||
yield self.updateVehicles(); | ||
}), 60 * 1000 * 15); | ||
yield this.updateVehicle(); | ||
}), 60 * 1000 * 5); | ||
} | ||
}); | ||
} | ||
addVehicles(connection) { | ||
addVehicle(connection) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
const vehicles = yield connection.getVehicles(); | ||
vehicles === null || vehicles === void 0 ? void 0 : vehicles.forEach((vehicle) => __awaiter(this, void 0, void 0, function* () { | ||
vehicle.VIN = vehicle.VIN.toUpperCase(); | ||
const name = vehicle.nickName || vehicle.year + ' ' + vehicle.make + ' ' + vehicle.model; | ||
const uuid = hap.uuid.generate(vehicle.VIN); | ||
if (vehicles) { | ||
const vehicleConfig = vehicles[0]; | ||
vehicleConfig.vehicleId = vehicleConfig.vehicleId.toUpperCase(); | ||
this.vehicle.vehicleId = vehicleConfig.vehicleId; | ||
const name = vehicleConfig.nickName || vehicleConfig.modelYear + ' ' + vehicleConfig.make + ' ' + vehicleConfig.modelName; | ||
const uuid = hap.uuid.generate(vehicleConfig.vehicleId); | ||
const accessory = new Accessory(name, uuid); | ||
accessory.context.name = name; | ||
accessory.context.vin = vehicle.VIN; | ||
accessory.context.vehicleId = vehicleConfig.vehicleId; | ||
const accessoryInformation = accessory.getService(hap.Service.AccessoryInformation); | ||
@@ -252,3 +268,3 @@ if (accessoryInformation) { | ||
accessoryInformation.setCharacteristic(hap.Characteristic.Model, name); | ||
accessoryInformation.setCharacteristic(hap.Characteristic.SerialNumber, vehicle.VIN); | ||
accessoryInformation.setCharacteristic(hap.Characteristic.SerialNumber, vehicleConfig.vehicleId); | ||
} | ||
@@ -260,74 +276,58 @@ if (!this.accessories.find((x) => x.UUID === uuid)) { | ||
} | ||
})); | ||
this.accessories.forEach((accessory) => { | ||
if (!(vehicles === null || vehicles === void 0 ? void 0 : vehicles.find((x) => x.VIN === accessory.context.vin))) { | ||
this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]); | ||
const index = this.accessories.indexOf(accessory); | ||
if (index > -1) { | ||
this.accessories.splice(index, 1); | ||
this.vehicles.slice(index, 1); | ||
} | ||
} | ||
}); | ||
} | ||
}); | ||
} | ||
updateVehicles() { | ||
updateVehicle() { | ||
var _a, _b, _c, _d, _e, _f; | ||
return __awaiter(this, void 0, void 0, function* () { | ||
this.vehicles.forEach((vehicle) => __awaiter(this, void 0, void 0, function* () { | ||
var _a, _b, _c; | ||
const status = yield vehicle.status(); | ||
if (!status) { | ||
return; | ||
yield new Promise((resolve) => setTimeout(resolve, 10000)); | ||
yield this.vehicle.retrieveVehicleInfo(); | ||
const status = (_b = (_a = this.vehicle) === null || _a === void 0 ? void 0 : _a.info) === null || _b === void 0 ? void 0 : _b.vehicleStatus; | ||
const lockStatus = (_c = status === null || status === void 0 ? void 0 : status.lockStatus) === null || _c === void 0 ? void 0 : _c.value; | ||
let lockNumber = hap.Characteristic.LockCurrentState.UNSECURED; | ||
if (lockStatus === 'LOCKED') { | ||
lockNumber = hap.Characteristic.LockCurrentState.SECURED; | ||
} | ||
const engineStatus = status === null || status === void 0 ? void 0 : status.ignitionStatus.value; | ||
let started = true; | ||
if (engineStatus === 'OFF') { | ||
started = false; | ||
} | ||
const uuid = hap.uuid.generate(this.vehicle.vehicleId); | ||
const accessory = this.accessories.find((x) => x.UUID === uuid); | ||
if (accessory) { | ||
const fordAccessory = new accessory_1.FordpassAccessory(accessory); | ||
if (!this.pendingLockUpdate) { | ||
const lockService = fordAccessory.findService(hap.Service.LockMechanism); | ||
lockService && lockService.updateCharacteristic(hap.Characteristic.LockCurrentState, lockNumber); | ||
lockService && lockService.updateCharacteristic(hap.Characteristic.LockTargetState, lockNumber); | ||
} | ||
this.log.debug(`Updating info for ${vehicle.name}`); | ||
this.log.debug(`Vehicle Status : ${JSON.stringify(status, null, 2)}`); | ||
const lockStatus = (_a = status.metrics.doorLockStatus.find((x) => x.vehicleDoor === 'ALL_DOORS')) === null || _a === void 0 ? void 0 : _a.value; | ||
let lockNumber = hap.Characteristic.LockCurrentState.UNSECURED; | ||
if (lockStatus === 'LOCKED') { | ||
lockNumber = hap.Characteristic.LockCurrentState.SECURED; | ||
} | ||
const engineStatus = status.metrics.ignitionStatus.value; | ||
let started = true; | ||
if (engineStatus === 'OFF') { | ||
started = false; | ||
} | ||
const uuid = hap.uuid.generate(vehicle.vin); | ||
const accessory = this.accessories.find((x) => x.UUID === uuid); | ||
if (accessory) { | ||
const fordAccessory = new accessory_1.FordpassAccessory(accessory); | ||
if (!this.pendingLockUpdate) { | ||
const lockService = fordAccessory.findService(hap.Service.LockMechanism); | ||
lockService && lockService.updateCharacteristic(hap.Characteristic.LockCurrentState, lockNumber); | ||
lockService && lockService.updateCharacteristic(hap.Characteristic.LockTargetState, lockNumber); | ||
} | ||
const switchService = fordAccessory.findService(hap.Service.Switch); | ||
switchService && switchService.updateCharacteristic(hap.Characteristic.On, started); | ||
const plugService = fordAccessory.findService(hap.Service.OccupancySensor, 'Plug'); | ||
plugService && | ||
plugService.updateCharacteristic(hap.Characteristic.OccupancyDetected, ((_b = status === null || status === void 0 ? void 0 : status.metrics.xevPlugChargerStatus) === null || _b === void 0 ? void 0 : _b.value) | ||
? hap.Characteristic.OccupancyDetected.OCCUPANCY_DETECTED | ||
: hap.Characteristic.OccupancyDetected.OCCUPANCY_NOT_DETECTED); | ||
const chargingService = fordAccessory.findService(hap.Service.OccupancySensor, 'Charging'); | ||
chargingService && | ||
chargingService.updateCharacteristic(hap.Characteristic.OccupancyDetected, ((_c = status === null || status === void 0 ? void 0 : status.metrics.xevBatteryChargeDisplayStatus) === null || _c === void 0 ? void 0 : _c.value) === 'ChargingAC' | ||
? hap.Characteristic.OccupancyDetected.OCCUPANCY_DETECTED | ||
: hap.Characteristic.OccupancyDetected.OCCUPANCY_NOT_DETECTED); | ||
} | ||
else { | ||
this.log.warn(`Accessory not found for ${vehicle.name}`); | ||
} | ||
})); | ||
const switchService = fordAccessory.findService(hap.Service.Switch); | ||
switchService && switchService.updateCharacteristic(hap.Characteristic.On, started); | ||
const plugService = fordAccessory.findService(hap.Service.OccupancySensor, 'Plug'); | ||
plugService && | ||
plugService.updateCharacteristic(hap.Characteristic.OccupancyDetected, ((_d = status === null || status === void 0 ? void 0 : status.plugStatus) === null || _d === void 0 ? void 0 : _d.value) | ||
? hap.Characteristic.OccupancyDetected.OCCUPANCY_DETECTED | ||
: hap.Characteristic.OccupancyDetected.OCCUPANCY_NOT_DETECTED); | ||
this.log.debug(`Charging status: ${(_e = status === null || status === void 0 ? void 0 : status.chargingStatus) === null || _e === void 0 ? void 0 : _e.value}`); | ||
const chargingService = fordAccessory.findService(hap.Service.OccupancySensor, 'Charging'); | ||
chargingService && | ||
chargingService.updateCharacteristic(hap.Characteristic.OccupancyDetected, ((_f = status === null || status === void 0 ? void 0 : status.chargingStatus) === null || _f === void 0 ? void 0 : _f.value) === 'ChargingAC' | ||
? hap.Characteristic.OccupancyDetected.OCCUPANCY_DETECTED | ||
: hap.Characteristic.OccupancyDetected.OCCUPANCY_NOT_DETECTED); | ||
} | ||
else { | ||
this.log.warn(`Accessory not found for ${this.vehicle.name}`); | ||
} | ||
}); | ||
} | ||
refreshVehicles() { | ||
refreshVehicle() { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
this.vehicles.forEach((vehicle) => __awaiter(this, void 0, void 0, function* () { | ||
if (vehicle.autoRefresh && vehicle.refreshRate && vehicle.refreshRate > 0) { | ||
this.log.debug(`Configuring ${vehicle.name} to refresh every ${vehicle.refreshRate} minutes.`); | ||
setInterval(() => __awaiter(this, void 0, void 0, function* () { | ||
this.log.debug(`Refreshing info for ${vehicle.name}`); | ||
yield vehicle.issueCommand(vehicle_1.Command.REFRESH); | ||
}), 60000 * vehicle.refreshRate); | ||
} | ||
})); | ||
this.log.debug(`Configuring ${this.vehicle.name} (${this.config.autoRefresh}) to refresh every ${this.config.refreshRate} minutes.`); | ||
if (this.vehicle.autoRefresh && this.vehicle.refreshRate && this.vehicle.refreshRate > 0) { | ||
setInterval(() => __awaiter(this, void 0, void 0, function* () { | ||
this.log.debug(`Refreshing info for ${this.vehicle.name}`); | ||
yield this.vehicle.issueCommand(vehicle_1.Command.REFRESH); | ||
}), 60000 * this.vehicle.refreshRate); | ||
} | ||
}); | ||
@@ -334,0 +334,0 @@ } |
{ | ||
"displayName": "Homebridge FordPass", | ||
"name": "homebridge-fordpass", | ||
"version": "1.9.0", | ||
"version": "1.10.0-test.0", | ||
"description": "Fordpass plugin for homebridge: https://homebridge.io/", | ||
@@ -24,6 +24,6 @@ "main": "dist/index.js", | ||
"packlist": "npm pack --dry-run && rm *.tgz", | ||
"format": "prettier --write \"src/**/*.ts\" \"src/**/*.js\"", | ||
"format": "prettier --write .", | ||
"test": "jest --coverage", | ||
"watch:tests": "jest --watch", | ||
"lint": "eslint './src/**/*.ts' --fix", | ||
"lint": "eslint src/ --fix", | ||
"prepare": "npm run build", | ||
@@ -55,3 +55,4 @@ "prepublishOnly": "npm run lint", | ||
"axios": "^1.5.1", | ||
"base64url": "^3.0.1" | ||
"base64url": "^3.0.1", | ||
"form-data": "^4.0.0" | ||
}, | ||
@@ -58,0 +59,0 @@ "devDependencies": { |
@@ -23,12 +23,14 @@ <p align="center"> | ||
## WARNING | ||
Ford is locking accounts of users who use this plug-in. See #196 for more info. | ||
Ford is locking accounts of users who use this plug-in. See #196 for more info. | ||
## Prerequisites | ||
Your vehicle must be connected to [FordPass Connect](https://owner.ford.com/fordpass/fordpass-sync-connect.html). Download the app or contact your local dealer to see if your vehicle is compatible with FordPass Connect. | ||
<a href="https://c00.adobe.com/v3/6b72dd687901669e3ed55059dd6f60d5d3c844c25518eefaff82ed287725d462/start?a_dl=5ad0dab8511fb41c63233b99" aria-label="Google Play store opens in new tab or window" target="_blank" class="cx-cta cx-cta--image"> | ||
<img alt="Google Play" src="https://owner.ford.com/ownerlibs/content/dam/ford-dot-com/cx_en_english/FordPass/1_LINCOLN-GOOGLE-store.png"></a> | ||
<a href="https://c00.adobe.com/v3/6b72dd687901669e3ed55059dd6f60d5d3c844c25518eefaff82ed287725d462/start?a_dl=5ad0da2e511fb41c63233b8e" aria-label="Apple App Store opens in new tab or window" target="_blank" class="cx-cta cx-cta--image"><img alt="Apple App Store" src="https://owner.ford.com/ownerlibs/content/dam/ford-dot-com/cx_en_english/FordPass/1_LINCOLN-APPLE-store.png"></a> | ||
<a href="https://play.google.com/store/apps/details?id=com.ford.fordpass&hl=en_US&gl=US" aria-label="Google Play store opens in new tab or window" target="_blank" class="cx-cta cx-cta--image">Google Play Store</a> | ||
<a href="https://apps.apple.com/us/app/fordpass/id1095418609" aria-label="Apple App Store opens in new tab or window" target="_blank" class="cx-cta cx-cta--image">Apple AppStore</a> | ||
## Installation | ||
1. Install this plugin using: `npm install -g --unsafe-perm homebridge-fordpass` | ||
@@ -39,9 +41,14 @@ 2. Add username, passwrod, and vehicles to `config.json` | ||
### Config.json Example | ||
``` | ||
{ | ||
"username": "YOUR_USERNAME", | ||
"password": "YOUR_PASSWORD", | ||
"options": { | ||
"autoRefresh": false | ||
}, | ||
"batteryName": "Battery", | ||
"autoRefresh": true, | ||
"refreshRate": 30, | ||
"chargingSwitch": true, | ||
"plugSwitch": true, | ||
"application_id": "APPLICATION_ID", | ||
"client_id": "CLIENT_ID", | ||
"client_secret": "SECRET_ID", | ||
"code": "FORD_AUTH_CODE", | ||
"platform": "FordPass" | ||
@@ -51,3 +58,22 @@ } | ||
## FordPass API Signup Process | ||
1. First, you MUST have a FordPass account. If you don't, you will need to do that first and that's outside of the scope for these instructions. [Start here](https://www.ford.com/support/how-tos/fordpass/getting-started-with-fordpass/download-fordpass/) | ||
2. Sign up at FordPass API program at [developer.ford.com](https://developer.ford.com/). | ||
3. Go to [FordConnect](https://developer.ford.com/apis/fordconnect) and request access. | ||
4. Create Application Credentials at [https://developer.ford.com/my-developer-account/my-dashboard](https://developer.ford.com/my-developer-account/my-dashboard) and copy Secret 1 Hint. | ||
5. Paste Secret into "client_secret" property of config. | ||
6. Construct this URL in your Browser: | ||
https://fordconnect.cv.ford.com/common/login/?make=F&application_id=AFDC085B-377A-4351-B23E-5E1D35FB3700&client_id=30990062-9618-40e1-a27b-7c6bcb23658a&response_type=code&state=123&redirect_uri=https%3A%2F%2Flocalhost%3A3000&scope=access | ||
7. application_id and client_id may be different. Review the FordPass API Documentation found in the Ford Developer website to verify if different. application_id goes into "application_id" of config and client_id goes into "client_id" of config. | ||
8. Sign in with your FordPass login that you use for FordPass' app. | ||
9. Select the car you wish to integrate with. | ||
10. Click Authorize | ||
11. The page will eventually send you to an invalid page. This is normal. Copy the URL into a notepad, delete everything from the beginning of the url until after code= | ||
12. Take the remaining text and copy it for the FordPass Plugin config's "code" property. | ||
## Things to try | ||
- "Hey siri, turn on my car." | ||
@@ -59,4 +85,5 @@ - "Hey siri, is the mustang locked?" | ||
## Donate to Support homebridge-fordpass | ||
This plugin was made with you in mind. If you would like to show your appreciation for its continued development, please consider [sponsoring me on Github](https://github.com/sponsors/Brandawg93). | ||
<sub><sup>**Disclaimer:** This plugin and its contributers are not affiliated with Ford Motor Company in any way.</sub></sup> |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
125134
997
86
3
2
2
1
+ Addedform-data@^4.0.0