diff --git a/main.js b/main.js index 3604d6d..a70d6ba 100644 --- a/main.js +++ b/main.js @@ -141,14 +141,17 @@ function createWindow() { // get tesla Data let authToken - const getTeslaData = async () => { + const getTeslaData = async (vehicleIdx) => { authToken = store.get('authToken') + if (typeof vehicleIdx !== 'number') vehicleIdx = store.get('vehicleIdx') || 0 if (mainWindow.isVisible() && authToken) { try { let vehicle try { - vehicle = await tesla.vehicle(authToken) + const vehicles = await tesla.vehicles(authToken) + vehicleIdx = (vehicleIdx % vehicles.length) || 0 + vehicle = vehicles[vehicleIdx] } catch (error) { log.error(error) store.delete('authToken') @@ -159,8 +162,10 @@ function createWindow() { } store.set('vehicleId', vehicle.vehicleID) + store.set('vehicleIdx', vehicleIdx) if (vehicle.state !== 'online') { mainWindow.webContents.send('tesla-data', { + vehicleIdx, ...vehicle }) await tesla.wakeUp(authToken, vehicle.vehicleID) @@ -172,6 +177,7 @@ function createWindow() { ) mainWindow.webContents.send('tesla-data', { model: vehicle.model, + vehicleIdx, ...vehicleData }) mainWindow.webContents.send('tesla-data-error', false) @@ -208,6 +214,10 @@ function createWindow() { startLogin(false, loginEmailPw) }) + ipcMain.on('switch-vehicle', async (event, vehicleIdx) => { + getTeslaData(vehicleIdx) + }) + // wait helper function async function wait(ms) { return new Promise(resolve => { @@ -219,7 +229,7 @@ function createWindow() { mainWindow.webContents.send('action-loading', action) try { if (action === 'door-unlock') { - await tesla.unLockDoor(store.get('authToken'), store.get('vehicleId')) + await tesla.unlockDoor(store.get('authToken'), store.get('vehicleId')) } else { await tesla.lockDoor(store.get('authToken'), store.get('vehicleId')) } @@ -359,12 +369,16 @@ function createWindow() { } let firstshow = true + let loading = false // lock to ensure we don't load more than we ought to mainWindow.on('show', async () => { if (firstshow) { firstshow = false return } - getTeslaData() + if (loading) return + loading = true + await getTeslaData() + loading = false }) }) diff --git a/package.json b/package.json index 595a1e7..bca5da7 100644 --- a/package.json +++ b/package.json @@ -13,11 +13,13 @@ "directories": { "output": "packaged" }, - "publish": [{ - "provider": "github", - "owner": "geraldoramos", - "repo": "nikola" - }], + "publish": [ + { + "provider": "github", + "owner": "geraldoramos", + "repo": "nikola" + } + ], "appId": "com.github.geraldoramos.nikola", "mac": { "category": "public.app-category.utilities", @@ -94,6 +96,6 @@ "electron-updater": "^4.0.6", "google-maps-react": "^2.0.2", "react-tooltip": "^3.10.0", - "teslajs": "^4.3.9" + "teslajs": "^4.7.4" } -} \ No newline at end of file +} diff --git a/src/assets/css/App.css b/src/assets/css/App.css index 16ad1e3..000b745 100644 --- a/src/assets/css/App.css +++ b/src/assets/css/App.css @@ -42,6 +42,27 @@ body { border-top-right-radius: var(--window-border-radius); } +.car-picker > svg { + position: absolute; + top: 5px; +} + +.car-picker > svg:hover { + color: #bbbbff; +} + +.car-picker > svg:active { + color: #ffffbb; +} + +.car-picker .car-left { + left: 2px; +} + +.car-picker .car-right { + right: 2px; +} + .window-content { background-color: var(--background-color); } diff --git a/src/components/Home.js b/src/components/Home.js index b739cf9..592fd6e 100644 --- a/src/components/Home.js +++ b/src/components/Home.js @@ -4,7 +4,11 @@ import React, { Component } from 'react' const { ipcRenderer, remote } = window.require('electron') import batteryLevelIcon from './helpers/battery-level-icon' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { faWifi } from '@fortawesome/free-solid-svg-icons' +import { + faWifi, + faChevronCircleLeft, + faChevronCircleRight +} from '@fortawesome/free-solid-svg-icons' import App from './App' import Login from './Login' @@ -21,6 +25,8 @@ class Home extends React.Component { online: true } this.alertOnlineStatus = this.alertOnlineStatus.bind(this) + this.handleVehicleIdxDec = this.handleVehicleIdxDec.bind(this) + this.handleVehicleIdxInc = this.handleVehicleIdxInc.bind(this) } alertOnlineStatus() { @@ -106,6 +112,7 @@ class Home extends React.Component { ? store.gui_settings.gui_charge_rate_units : null }, + vehicleIdx: store.vehicleIdx, status: { driverTempSetting: store.climate_state ? store.climate_state.driver_temp_setting @@ -163,6 +170,14 @@ class Home extends React.Component { window.removeEventListener('offline', this.alertOnlineStatus) } + handleVehicleIdxDec() { + ipcRenderer.send('switch-vehicle', this.state.vehicleIdx - 1) + } + + handleVehicleIdxInc() { + ipcRenderer.send('switch-vehicle', this.state.vehicleIdx + 1) + } + render() { if (!this.state.online) { return ( @@ -209,6 +224,22 @@ class Home extends React.Component {
+
+ + +
diff --git a/src/components/Login.js b/src/components/Login.js index 93668ee..8cf79df 100644 --- a/src/components/Login.js +++ b/src/components/Login.js @@ -65,6 +65,7 @@ class Login extends React.Component { onChange={this.handleChange} className="form-control" placeholder="Enter email" + autofocus={true} />
diff --git a/src/components/helpers/battery-level-icon.js b/src/components/helpers/battery-level-icon.js index d1f656e..fd44c9a 100644 --- a/src/components/helpers/battery-level-icon.js +++ b/src/components/helpers/battery-level-icon.js @@ -6,23 +6,18 @@ import { faBatteryThreeQuarters } from '@fortawesome/free-solid-svg-icons' -export default level => { - let battery - switch (true) { - case 0 <= level && level <= 9: - battery = { type: faBatteryEmpty, color: '#cc0001' } - break - case 10 <= level && level <= 39: - battery = { type: faBatteryQuarter, color: '#cc0001' } - break - case 40 <= level && level <= 69: - battery = { type: faBatteryHalf, color: '#1BC47D' } - break - case 70 <= level && level <= 90: - battery = { type: faBatteryThreeQuarters, color: '#1BC47D' } - break - default: - battery = { type: faBatteryFull, color: '#1BC47D' } +const levels = { + 10: { type: faBatteryEmpty, color: '#cc0001' }, + 40: { type: faBatteryQuarter, color: '#cc0001' }, + 70: { type: faBatteryHalf, color: '#1BC47D' }, + 90: { type: faBatteryThreeQuarters, color: '#1BC47D' }, + 100: { type: faBatteryFull, color: '#1BC47D' }, +} + +export default function batteryLevelIcon(thisLevel) { + for (const levelThresh of Object.keys(levels)) { + if (thisLevel <= Number(levelThresh)) return levels[levelThresh] } - return battery + // Over 100? + return levels[100] } diff --git a/tesla-api.js b/tesla-api.js index a13399b..d59270d 100644 --- a/tesla-api.js +++ b/tesla-api.js @@ -2,156 +2,84 @@ const tjs = require('teslajs'); const log = require('electron-log'); module.exports = { - login: ({ username, password}) => { - return new Promise((resolve, reject) => { - tjs.login(username, password, function (err, result) { - if (err) { - reject(err) - return - } - resolve(result.authToken) - }) - }) + login({username, password}) { + log.debug('API: Logging in') + return tjs.loginAsync(username, password) + .then((result) => result.authToken) }, - vehicle: (authToken) => { - return new Promise((resolve, reject) => { - const options = { - authToken: authToken - }; - log.info('Hitting Tesla API on ' + new Date()) - tjs.vehicle(options, function (err, vehicle) { - if (err) { - log.error(err) - reject(err) - return - } - resolve({ - authToken: authToken, + vehicle(authToken) { + const options = { authToken } + log.debug('API: Getting vehicle') + return tjs.vehicleAsync(options) + .then((vehicle) => { + return { vehicleID: vehicle.id_s, model: tjs.getModel(vehicle), state: vehicle.state - }) - }); - })}, - wakeUp: (authToken, vehicleID) => { - return new Promise((resolve, reject) => { - const vehicleOptions = { - authToken, - vehicleID - }; - tjs.wakeUp(vehicleOptions, function (err, data) { - if (err) { - log.error(err) - reject(err) - } - resolve(data) - }); + } }) }, - vehicleData: (authToken, vehicleID) => { - return new Promise((resolve, reject) => { - const vehicleOptions = { - authToken, - vehicleID - }; - tjs.vehicleData(vehicleOptions, function (err, data) { - if (err) { - log.error(err) - reject(err) - } - resolve(data) - }); + vehicles(authToken) { + const options = { authToken } + log.debug('API: Getting all vehicles') + return tjs.vehiclesAsync(options) + .then((vehicles) => { + return vehicles.map((vehicle) => ({ + vehicleID: vehicle.id_s, + model: tjs.getModel(vehicle), + state: vehicle.state + })) }) }, - lockDoor: (authToken, vehicleID) => { - return new Promise((resolve, reject) => { - const vehicleOptions = { - authToken, - vehicleID - }; - tjs.doorLock(vehicleOptions, function (err, data) { - if (err) { - log.error(err) - reject(err) - } - resolve(data) - }); - }) + wakeUp(authToken, vehicleID) { + log.debug('API: Waking up') + const vehicleOptions = { authToken, vehicleID } + return tjs.wakeUpAsync(vehicleOptions) }, - unLockDoor: (authToken, vehicleID) => { - return new Promise((resolve, reject) => { - const vehicleOptions = { - authToken, - vehicleID - }; - tjs.doorUnlock(vehicleOptions, function (err, data) { - if (err) { - log.error(err) - reject(err) - } - resolve(data) - }); - }) + vehicleData(authToken, vehicleID) { + log.debug(`API: Getting data for vehicle id ${vehicleID}`) + const vehicleOptions = { authToken, vehicleID } + return tjs.vehicleDataAsync(vehicleOptions) }, - climateStart: (authToken, vehicleID) => { - return new Promise((resolve, reject) => { - const vehicleOptions = { - authToken, - vehicleID - }; - tjs.climateStart(vehicleOptions, function (err, data) { - if (err) { - log.error(err) - reject(err) - } - resolve(data) - }); - }) + lockDoor(authToken, vehicleID) { + log.debug(`API: Locking door for vehicle id ${vehicleID}`) + const vehicleOptions = { authToken, vehicleID } + return tjs.doorLockAsync(vehicleOptions) }, - climateStop: (authToken, vehicleID) => { - return new Promise((resolve, reject) => { - const vehicleOptions = { - authToken, - vehicleID - }; - tjs.climateStop(vehicleOptions, function (err, data) { - if (err) { - log.error(err) - reject(err) - } - resolve(data) - }); - }) + unlockDoor(authToken, vehicleID) { + log.debug(`API: Unlocking door for vehicle id ${vehicleID}`) + const vehicleOptions = { authToken, vehicleID } + return tjs.doorUnlockAsync(vehicleOptions) }, - setSentryMode: (authToken, vehicleID, onoff) => { - return new Promise((resolve, reject) => { - const vehicleOptions = { - authToken, - vehicleID - }; - tjs.setSentryMode(vehicleOptions, onoff, function (err, done) { - if (err) { - log.error(err) - reject(err) - } - resolve(done) - }); - }) + climateStart(authToken, vehicleID) { + log.debug(`API: Climate start for vehicle id ${vehicleID}`) + const vehicleOptions = { authToken, vehicleID } + return tjs.climateStartAsync(vehicleOptions) }, - setTemps: (authToken, vehicleID, temp) => { - return new Promise((resolve, reject) => { - const vehicleOptions = { - authToken, - vehicleID - }; - tjs.setTemps(vehicleOptions, temp, null, function (err, done) { - if (done.result && !done.err) { - resolve(done.result) - return - } - log.error(done.reason, err) - reject(done.reason) - }); - }) + climateStop(authToken, vehicleID) { + log.debug(`API: Climate stop for vehicle id ${vehicleID}`) + const vehicleOptions = { authToken, vehicleID } + return tjs.climateStopAsync(vehicleOptions) + }, + setSentryMode(authToken, vehicleID, onoff) { + log.debug(`API: Turning Sentry mode ${onoff ? 'on' : 'off'} for vehicle id ${vehicleID}`) + const vehicleOptions = { authToken, vehicleID } + return tjs.setSentryModeAsync(vehicleOptions, onoff) + }, + setTemps(authToken, vehicleID, temp) { + log.debug(`API: Setting temp to ${temp} for vehicle id ${vehicleID}`) + const vehicleOptions = { authToken, vehicleID } + return tjs.setTempsAsync(vehicleOptions, temp, null) } } + +// Add error logger. Log & rethrow. +for (const methodName of Object.keys(module.exports)) { + const fn = module.exports[methodName]; + module.exports[methodName] = function(...args) { + return fn(...args).catch((err) => { + log.error(`API: Error executing "${methodName}": ${err.message}`) + throw err; + }); + } +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index fa0e9d3..a7bb28f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6958,10 +6958,10 @@ terser@^4.0.0: source-map "~0.6.1" source-map-support "~0.5.10" -teslajs@^4.3.9: - version "4.3.10" - resolved "https://registry.yarnpkg.com/teslajs/-/teslajs-4.3.10.tgz#9e030f3a3558a02a1ad5cca2f7d4abb531950b25" - integrity sha512-lPuuzOsdXF/9Y3AvujV2whfXkWPR3NeR5AkoYdgrX3VP+lsVqNh+LwtCoYhWtseXEEHpagSbU6ntXKq+gf6Bbw== +teslajs@^4.7.4: + version "4.7.4" + resolved "https://registry.yarnpkg.com/teslajs/-/teslajs-4.7.4.tgz#49ab09f17808ddbfd218845b0d89954e338b532e" + integrity sha512-mkQ6YfsuotPilrXQ+/J2hSWeXdFP27q2cpWsyzk1Jfk6aYCDPWFpmu+aEJoK8ulsDN9BiMHZXPGxbqzrlaA0IA== dependencies: promise "^8.0.1" request "^2.81.0"