├── README.md ├── TeslaData Widget.js ├── documentation ├── Developers.md ├── config.png ├── json_requirements.md ├── sample.json ├── screen_001.png ├── screen_001_med.png ├── screen_002.png ├── screen_003.png ├── screen_3d_002.png ├── screen_3d_003.png ├── screen_map_001.png ├── theme_listing.md └── version.js └── tesla_data ├── 3d.js └── parameters.js /README.md: -------------------------------------------------------------------------------- 1 | # TeslaData Widget 2 | A Scriptable widget to pull data from a given API, eg. TeslaFi, Teslalogger, Tronity to display a widget on your iPhone. 3 | 4 |     5 | 6 | ## Usage 7 | ### Install with Scriptdude 8 | 9 | [](https://scriptdu.de/?name=TeslaData&source=https://raw.githubusercontent.com/DrieStone/TeslaData-Widget/main/TeslaData%20Widget.js&docs=https://github.com/DrieStone/TeslaData-Widget#generator) 10 | 11 | ### Manual Install 12 | 13 | * Get Scriptable in the Apple App Store. 14 | * Download the `TeslaData Widget.js` file to your iCloud/Scriptable folder (or create a new widget in the scriptable app). 15 | 16 | ## Setup 17 | 18 | * Click on TeslaData within the Scriptable App and step through the confguration. 19 | * Create a Scriptable widget. 20 | * Long press on the widget, choose "Edit Widget" 21 | * Choose "TeslaData" from the Script Menu 22 | 23 | ### Optional/Advanced 24 | * Get a [map API key from MapQuest](https://developer.mapquest.com/) and add it to your parameters.js file. 25 | * Install any themes into the tesla_data folder, and modify the parameters.js file to include the theme you'd like to apply (e.g. custom_theme = "3d" will load the 3d.js theme from the themes directory). 26 | * If you have multiple cars, you can put your TeslaFi key or other datalogger URL into the script parameters. All other options will be the same across widgets. 27 | 28 | ### TeslaFi API 29 | You obviously need a TeslaFi account (and a Tesla). Get your [API Key](https://teslafi.com/api.php). 30 | 31 | Note, due to the lag with TeslaFi pulling data from your car, and the lag of iOS pulling the data, the resulting display could be ~5 minutes stale (and the data could be hours or even a day old because TeslaFi lets the car sleep, so its not sending data) 32 | 33 | ### Other API 34 | If you use other tools like [TeslaLogger](https://github.com/bassmaster187/TeslaLogger), [Tronity](https://tronity.io/home/5OiA7SfA), etc. you only have to provide [json file](documentation/sample.json) with the following data ([more details on the required fields](documentation/json_requirements.md)): 35 | 36 | ` 37 | { 38 | "response":null, 39 | "battery_level":27, 40 | "usable_battery_level":26, 41 | "charge_limit_soc":90, 42 | "carState":"Idling", 43 | "Date":"2020-10-28T14:57:15Z", 44 | "sentry_mode":0, 45 | "display_name":"Name", 46 | "locked":1, 47 | "is_climate_on":0, 48 | "inside_temp":14.6, 49 | "driver_temp_setting":22.0, 50 | "measure":"km", 51 | "est_battery_range":90.605842, 52 | "battery_range":125.2227454, 53 | "time_to_full_charge":0.0, 54 | "fast_charger_type":"" 55 | } 56 | ` 57 | 58 | API url (eg.): https://MY_USER:MY_PASS@MY_URL.com/api.json 59 | 60 | ## Map 61 | 62 | At medium sizes, the widget will show a map with the location of the car, but only if your API includes long/lat. We would recommend getting your own API key and putting it in the configuration to reduce load on our systems. Visit https://developer.mapquest.com/ to create an account and get an API. 63 | 64 | ## Configuration 65 | 66 | If you run TeslaData from the Scriptable app, the configuration screen will display, where you'll be able to configure TeslaData. 67 | 68 |   69 | 70 | At the bottom of the configuration screen, you'll find buttons for debug display of the small, medium, and large size widgets. This is used to preview the widget for development use, but you can also use it to preview the changes you've made to the configuration. Note that you need to update your configuration before previewing changes. 71 | 72 | 73 | ## Features 74 | 75 | This should support: 76 | * charging overview (current charge, charge limit, and time until charge complete) 77 | * conditioning on indicator 78 | * doors locked/unlocked 79 | * interior temperature 80 | * sentry mode on 81 | * sleeping, idle, driving indicator 82 | * time since the data was retreived from the car (respects TeslaFi sleep) 83 | * map location of the car's current position 84 | 85 | ## Themes 86 | 87 | To add themes to TeslaData you need to add theme files to the tesla_data file, and modify the custom_theme variable at the top of the widget code. To get an overview of themes, you can look at the [Theme Listing](documentation/theme_listing.md) page. 88 | 89 | ## Outstanding Bugs 90 | 91 | - There appears to be an issue with SF graphics in Scriptable where the images are stretched. 92 | - Dark mode doesn't currently work for widgets in Scriptable. 93 | 94 | ## Notes for Developers 95 | 96 | Starting with v1.5 TeslaData now supports theming. The theme file is loaded right before the widget is drawn and displayed, so the theme can override any existing code (so you can change how things work without worrying about your code being overwritten with future updates of Tesla Data). 97 | 98 | Note: due to the way themes are includes, debugging information from Scriptable is lacking. For testing purposes, it is probably best to develop by adding code to the end of the main Javascript file, and moving the code to a theme file once the code is running properly. 99 | 100 | Starting with v1.5 The all colors are defined as an obect at the top of the file. These can be overriden if you want to make changes (you should use a theme file for this). 101 | 102 | Starting with v1.5 TeslaData will optionally pull JSON files from iCloud for testing purposes. Place your JSON files in the tesla_themes directory on iCloud, and tell the widget to pull the data by modifying the debug_data string (i.e. debug_data = "standard" will load the "standard.json" file and ignore the URL). 103 | 104 | You can inject your own code without affecting the TeslaData codebase via theming and/or parameters.js The parameters are loaded after the default values are set but before data is pulled from the API. The theme is loaded right before the widget is rendered, but after the data is loaded and processed. You can use the parameters.js file to inject code to change the way that code is loaded. You can create a car_data.postLoad function that will recieve the json from the API (so you can affect the information loaded into the car_data object before it's acted on). 105 | 106 | 107 | ## Changelog 108 | 109 | - v2.0 110 | - Added a configuration system so the user doesn't have to edit Javascript files to set up TeslaData 111 | - Added iPad support 112 | - Fixed layout issues with medium layout (the car info was too high in the widget) 113 | - Changed several icons to fix issues with updated Scriptable changes 114 | - Made several UI changes to new design 115 | - Upscaled UI elements to appear cleaner on higher resolution devices 116 | - Added theme switcher 117 | - Moved themes and debug data into dedicated folders 118 | - Broke out existing theme to make it easier to create new themes 119 | - Added a theme that matches the original classic view 120 | 121 | - v1.7 122 | - Add my own map API key so users don't have to try to get their own (although still recommended). 123 | - Save a local copy of the map to reuse (to reduce calls to the mapping service). 124 | - Added link to either Google Maps or Apple Maps when clicking on the map (opens the app and drops a pin where the car is). 125 | - v1.6 126 | - Added Longitude/Latitude to data set 127 | - Updated theming to support async processing (so themes can pull data from external URLs) 128 | - Updated default theme to support medium 129 | - Added themeDebugArea as a place for theme development (bottom of the code) 130 | - Added map to medium sized widget if the data supports it 131 | - v1.5 132 | - Complete rewrite of the code for cleanliness. 133 | - Many fields are no longer required (but still preferred) 134 | - Added code to store a copy of the last data, so the widget doesn't error when the device doesn't have network 135 | - Added support for themes (and moved 3d bars into a theme) 136 | - Added support for external configuration file 137 | - v1.0 138 | - Use apiurl parameter instead apikey (now supports any JSON source) 139 | - Renamed to TeslaData widget. 140 | - Fixed issue where disance value wasn't correct for metric. 141 | - v0.8 142 | - Added color coded snowflake to show if we're heating or cooling. 143 | - Added target temperature to display when preheating/cooling. 144 | - Added charging icon to show when the charger is connected (but not currently charging). 145 | - Added metric range display (untested). 146 | - Added internal temperature showing faded when the data is more than 2 hours old (since the internal temp is probably invalid). 147 | - Added usable vs. total charge (for colder weather). 148 | - v0.7 149 | - Added custom bolt icon for charging so I could add a stroke, changed charging color. 150 | - Adjusted 3D styling 151 | - Added time since last communication with the car. 152 | - v0.6 153 | - Initial release added to GitHub 154 | -------------------------------------------------------------------------------- /TeslaData Widget.js: -------------------------------------------------------------------------------- 1 | // Variables used by Scriptable. 2 | // These must be at the very top of the file. Do not edit. 3 | // icon-color: deep-gray; icon-glyph: magic; 4 | 5 | // TeslaData Widget 6 | // Version 1.8 7 | // Jon Sweet (jon@driestone.com) 8 | // Tobias Merkl (@tabsl) 9 | // 10 | // Map Code blatenly stolen from @ThisIsBenny 11 | // 12 | // This pulls data from a given API, eg. TeslaFi, Teslalogger, Tronity to display a widget on your iPhone 13 | // 14 | // TelsaFi Notice: 15 | // This is better than other methods because TeslaFi tries to encourage your car to sleep to reduce phantom battery drain. Using this script will not directly connect to the car to respect the sleep status. 16 | // Notice that there is ~5 minute lag for data. The data may be stale because TeslaFi won't wake the car to get data. I've added a display so you can see how old the data is. The should (normally) be minutes except when the car is sleeping. 17 | 18 | 19 | var scriptName = Script.name(); 20 | let widgetParams = args.widgetParameter; 21 | var this_version = 2.0; 22 | 23 | /* Although you can change these options here, it's recommended that you make changes in the parameters.js file instead. */ 24 | { 25 | var show_battery_percentage = true; // show the battery percentage above the battery bar 26 | var show_range = true; // show the estimated range above the battery bar 27 | var show_range_est = true; // show range estimated instead of the car's range estimate 28 | var show_data_age = true; // show how stale the data is 29 | var custom_theme = ""; // if you want to load a theme (some available themes are "3d") 30 | var hide_map = false; 31 | 32 | var debug_data = ""; // this will force the widget to pull data from iCloud json files (put sample JSON in the debug_data directory) NOTE: omit the ".json" 33 | var debug_size = "medium"; // which size should the widget try to run as when run through Scriptable. (small, medium, large) 34 | 35 | var APItype = "TeslaFi"; 36 | var Tesla_Email = ""; 37 | var Tesla_Password = ""; 38 | var APIkey = ""; 39 | // You can embed your APIurl here, or add it as a widget parameter (you really should add it to parameters.js though) 40 | //APIurl = "YOUR_API_URL" // hardcode the API url 41 | 42 | var mapKey = ""; 43 | var useGoogleMaps = true; 44 | 45 | } 46 | 47 | if (args.queryParameters.widget != null){ 48 | debug_size = args.queryParameters.widget; 49 | } 50 | 51 | // load parameters from a file on iCloud 52 | let additional_manager = FileManager.iCloud() 53 | api_file = additional_manager.joinPath(additional_manager.documentsDirectory(),"tesla_data/parameters.js"); 54 | 55 | if (additional_manager.fileExists(api_file)){ 56 | additional_manager.downloadFileFromiCloud(api_file); 57 | eval(additional_manager.readString(api_file)); 58 | } 59 | 60 | if (widgetParams != null && widgetParams != ""){ 61 | //use the widget parameters for the APIurl/key 62 | APIurl = widgetParams; 63 | } 64 | 65 | 66 | // set up all the colors we want to use 67 | var colors = { 68 | background:{ 69 | main:"#DCD2C9", 70 | top:"#ffffff00", 71 | bottom:"#ffffff77" 72 | }, 73 | overlay:"#ffffff33", 74 | text:{ 75 | primary:"#333333cc", 76 | disabled:"#33333399", 77 | inverted:"#ffffffaa" 78 | }, 79 | battery:{ 80 | background:"#33333355", 81 | max_charge:"#00000033", 82 | charging:"#ddbb22", 83 | cold_charge:"#3172D4",//"#3172D4", 84 | usable_charge:"#2BD82E", 85 | low_charge:"#ddbb22", 86 | highlight:"#ffffff", 87 | border:"#333333cc", 88 | separator:"#333333cc" 89 | }, 90 | icons:{ 91 | default:"#33333399", 92 | disabled:"#33333366", 93 | charging_bolt:"#ffdd44ff", 94 | charging_bolt_outline:"#33333399", 95 | charging_bolt_circle:"#33333344", 96 | //charging_bolt_highlight:"#ffdd44", 97 | sentry_dot:"#ff0000", 98 | climate_hot:"#ff0000", 99 | climate_cold:"#0000ff" 100 | }, 101 | map:{ 102 | type:"light", // light or dark 103 | position:"222222" // hex without the # 104 | } 105 | } 106 | 107 | 108 | // set up a container for our data. 109 | 110 | //NOTE: these values may not align with the data names from our service. Review the documention for the expected values and their names. 111 | 112 | // If you want to do additional post-processing of data from your API, you should create a theme that modifies car_data.postLoad(json). 113 | 114 | var car_data = { 115 | source:"Unknown", 116 | theme:custom_theme, 117 | last_contact:"", 118 | data_is_stale:false, // if the data is especially old (> 2 hours) 119 | car_name:"Tesla", 120 | battery_level:-1, 121 | usable_battery_level:-1, 122 | battery_limit:-1, 123 | battery_range:-1, 124 | est_battery_range:-1, 125 | distance_label:"km", 126 | car_state:"Unknown", 127 | sentry_mode:false, 128 | doors_locked:true, 129 | climate_active:false, 130 | inside_temp:10000, 131 | temp_setting:10000, 132 | temp_label:"c", 133 | time_to_charge:10000, 134 | charger_attached:false, 135 | longitude:-1, 136 | latitude:-1, 137 | postLoad:function(json){ 138 | // update data where required after load 139 | // passes in the json from the API call. 140 | if (this.distance_label == "km" && this.source == "TeslaFi"){ 141 | // convert battery_range to metric if data comes from TeslaFi 142 | this.battery_range *= 1.309; 143 | this.est_battery_range *= 1.309; 144 | } 145 | } 146 | }; 147 | 148 | 149 | 150 | 151 | // a little helper to try to estimate the size of the widget in pixels 152 | var widgetPadding = 8; // how much padding around the widget 153 | var widgetSize = computeWidgetSize(widgetPadding); 154 | 155 | 156 | /* MAIN THEME */ 157 | { 158 | 159 | var theme = { 160 | small:{ 161 | available:true, 162 | init:function(){ 163 | 164 | }, 165 | draw:async function(widget,car_data,colors){ 166 | widget.setPadding(widgetPadding,widgetPadding,widgetPadding,widgetPadding); 167 | 168 | let g = new LinearGradient() 169 | g.locations = [0, 1] 170 | g.colors = [ 171 | new Color(colors.background.top), 172 | new Color(colors.background.bottom) 173 | ] 174 | widget.backgroundColor = new Color(colors.background.main); 175 | widget.backgroundGradient = g; 176 | 177 | /*widget.backgroundColor = new Color(colors.background); 178 | widget.backgroundGradient = new LinearGradient(); 179 | widget.backgroundGradient.colors = [new Color("#ffffff", 0.75), new Color("#000000", 0.75)] 180 | widget.backgroundGradient.locations = [0,1]; 181 | widget.backgroundGradient.startPoint = new Point(0,0); 182 | widget.backgroundGradient.endPoint = new Point(0,1);*/ 183 | 184 | theme.drawCarStatus(widget, car_data, colors,widgetSize); 185 | theme.drawCarName(widget, car_data, colors,widgetSize); 186 | theme.drawBatteryBar(widget, car_data, colors,widgetSize); 187 | theme.drawRangeInfo(widget, car_data, colors,widgetSize); 188 | theme.drawStatusLights(widget, car_data, colors,widgetSize); 189 | 190 | } 191 | }, 192 | medium:{available:false,init:function(){},draw:function(){}}, // this theme doesn't support medium 193 | large:{available:false,init:function(){},draw:function(){}}, // this theme doesn't support large 194 | init:function(){ 195 | var widgetSizing = debug_size; 196 | if (config.widgetFamily != null){ 197 | widgetSizing = config.widgetFamily; 198 | } 199 | switch (widgetSizing){ 200 | case "medium": 201 | if (this.medium.available){this.medium.init();} 202 | break; 203 | case "large": 204 | if (this.large.available){this.large.init();} 205 | break; 206 | case "small": 207 | default: 208 | if (this.small.available){this.small.init();} 209 | break; 210 | 211 | } 212 | }, 213 | draw:async function(widget,car_data,colors){ 214 | var widgetSizing = debug_size; 215 | if (config.widgetFamily != null){ 216 | widgetSizing = config.widgetFamily; 217 | } 218 | switch (widgetSizing){ 219 | case "medium": 220 | if (this.medium.available){await this.medium.draw(widget,car_data,colors);} 221 | else {drawErrorWidget(widget,'Theme not available at this size');} 222 | break; 223 | case "large": 224 | if (this.large.available){await this.large.draw(widget,car_data,colors);} 225 | else {drawErrorWidget(widget,'Theme not available at this size');} 226 | break; 227 | case "small": 228 | default: 229 | if (this.small.available){await this.small.draw(widget,car_data,colors);} 230 | else {drawErrorWidget(widget,'Theme not available at this size');} 231 | break; 232 | 233 | } 234 | } 235 | } 236 | theme.medium.available = true; 237 | theme.medium.init = theme.small.init; 238 | theme.medium.draw = theme.small.draw; 239 | 240 | 241 | /* We call the following function if we know we have info for the map, then we override the medium.draw function (inside this function) */ 242 | 243 | function addMapArea(){ // add the map area for medium size. 244 | if (!hide_map && car_data.longitude != -1 && car_data.latitude != -1){ 245 | // only if we have everything we need, otherwise leave the medium size as is. 246 | 247 | const mapZoomLevel = 15; 248 | 249 | 250 | // override the medium draw routine 251 | theme.medium.draw = async function(widget,car_data,colors){ 252 | widget.setPadding(0,0,0,0); 253 | widget.backgroundColor = new Color(colors.background.main); 254 | let body = widget.addStack(); 255 | 256 | body.layoutHorizontally(); 257 | 258 | let column_left = body.addStack(); 259 | column_left.setPadding(0,10,0,10); 260 | column_left.size = new Size(widgetSize.width/2+10,widgetSize.height+10); 261 | column_left.layoutVertically(); 262 | 263 | column_left.addSpacer(10); 264 | 265 | 266 | theme.drawCarStatus(column_left, car_data, colors,new Size(widgetSize.width/2,widgetSize.height)); 267 | theme.drawCarName(column_left, car_data, colors,new Size(widgetSize.width/2,widgetSize.height)); 268 | theme.drawBatteryBar(column_left, car_data, colors,new Size(widgetSize.width/2,widgetSize.height*1.1)); 269 | theme.drawRangeInfo(column_left, car_data, colors,new Size(widgetSize.width/2,widgetSize.height)); 270 | theme.drawStatusLights(column_left, car_data, colors,new Size(widgetSize.width/2,widgetSize.height)); 271 | 272 | let center_padding = body.addSpacer(null); 273 | let column_right = body.addStack(); 274 | var mapImage; 275 | 276 | 277 | roundedLat = Math.round(car_data.latitude*2000)/2000; 278 | roundedLong = Math.round(car_data.longitude*2000)/2000; 279 | storedFile = "tesla_map"+roundedLat*2000+"!"+roundedLong*2000+".image"; 280 | 281 | let map_image_manager = FileManager.local(); // change this to iCloud for debugging if needed 282 | map_image_file = map_image_manager.joinPath(map_image_manager.documentsDirectory(),storedFile); 283 | if (map_image_manager.fileExists(map_image_file)){ 284 | // load old map from disk 285 | mapImage = await map_image_manager.readImage(map_image_file); 286 | console.log("Read Map From Disk!"); 287 | } 288 | if (mapImage == null){ 289 | mapImage = await getMapImage(roundedLong,roundedLat,mapZoomLevel,colors); 290 | // write image to disk for future use 291 | map_image_manager.writeImage(map_image_file,mapImage); 292 | console.log("Map Written To Disk"); 293 | } 294 | 295 | 296 | column_right.topAlignContent(); 297 | if (useGoogleMaps) { 298 | // use Google Maps 299 | column_right.url = `comgooglemaps://maps.google.com/?center=${car_data.latitude},${car_data.longitude}&zoom=${mapZoomLevel}&q=${car_data.latitude},${car_data.longitude}`; 300 | } else { 301 | // use Apple Maps 302 | column_right.url =`http://maps.apple.com/?ll=${car_data.latitude},${car_data.longitude}&q=Tesla`; 303 | 304 | } 305 | //console.log(column_right.url); 306 | let mapImageObj = column_right.addImage(mapImage); 307 | column_right.backgroundColor = new Color("#ff0000"); 308 | mapImageObj.cornerRadius= 0; 309 | mapImageObj.rightAlignImage(); 310 | } 311 | 312 | } 313 | } 314 | 315 | var _0xd9c9=["\x32\x30\x30\x2C\x32\x30\x30\x40\x32\x78","","\x32\x4F\x6F\x59\x6D\x41\x46\x71\x49\x74\x53\x30\x71\x54\x54\x74\x48\x70\x37\x56\x72\x45\x56\x42\x48\x67\x49\x45\x7A\x4E\x58\x41","\x68\x74\x74\x70\x73\x3A\x2F\x2F\x77\x77\x77\x2E\x6D\x61\x70\x71\x75\x65\x73\x74\x61\x70\x69\x2E\x63\x6F\x6D\x2F\x73\x74\x61\x74\x69\x63\x6D\x61\x70\x2F\x76\x35\x2F\x6D\x61\x70\x3F\x6B\x65\x79\x3D","\x26\x6C\x6F\x63\x61\x74\x69\x6F\x6E\x73\x3D","\x2C","\x26\x7A\x6F\x6F\x6D\x3D","\x26\x66\x6F\x72\x6D\x61\x74\x3D\x70\x6E\x67\x26\x73\x69\x7A\x65\x3D","\x26\x74\x79\x70\x65\x3D","\x74\x79\x70\x65","\x6D\x61\x70","\x26\x64\x65\x66\x61\x75\x6C\x74\x4D\x61\x72\x6B\x65\x72\x3D\x6D\x61\x72\x6B\x65\x72\x2D","\x70\x6F\x73\x69\x74\x69\x6F\x6E","\x6C\x6F\x61\x64\x49\x6D\x61\x67\x65"];async function getMapImage(_0x4583x2,_0x4583x3,_0x4583x4,_0x4583x5){var _0x4583x6=_0xd9c9[0];if(mapKey== null|| mapKey== _0xd9c9[1]){mapKey= _0xd9c9[2]};let _0x4583x7=`${_0xd9c9[3]}${mapKey}${_0xd9c9[4]}${_0x4583x3}${_0xd9c9[5]}${_0x4583x2}${_0xd9c9[6]}${_0x4583x4}${_0xd9c9[7]}${_0x4583x6}${_0xd9c9[8]}${_0x4583x5[_0xd9c9[10]][_0xd9c9[9]]}${_0xd9c9[11]}${_0x4583x5[_0xd9c9[10]][_0xd9c9[12]]}${_0xd9c9[1]}`;r= new Request(_0x4583x7);i= await r[_0xd9c9[13]]();return i} 316 | 317 | theme.drawCarStatus = function(widget,car_data,colors,widgetSize){ 318 | let stack = widget.addStack(); 319 | var statusHeight = 45; 320 | if (widgetSize.height*0.22 > statusHeight) { statusHeight = widgetSize.height*0.22;} 321 | stack.size = new Size(widgetSize.width,widgetSize.height*0.20); 322 | //stack.topAlignContent(); 323 | stack.setPadding(0,6,0,0); 324 | //stack.backgroundColor = new Color(colors.overlay); 325 | 326 | let timeDiff = 0 327 | if (car_data.last_contact.length > 0){ 328 | let lastUpdateText = stack.addText(car_data.last_contact) 329 | lastUpdateText.textColor = new Color(colors.text.primary); 330 | lastUpdateText.textOpacity = 0.4 331 | lastUpdateText.font = Font.systemFont(12) 332 | lastUpdateText.leftAlignText() 333 | 334 | } 335 | let carStateSpacer = stack.addSpacer(null); // This forces the time to the left and allows the icon to sit on the right 336 | var carStatusIconSize = 30; 337 | if (car_data.sentry_mode){ 338 | sentryModeIcon = this.getSentryModeIcon(colors); 339 | var carState = stack.addImage(sentryModeIcon); 340 | carState.imageSize = new Size(carStatusIconSize,carStatusIconSize); 341 | carState.rightAlignImage() 342 | } else { 343 | switch (car_data.car_state){ 344 | case "Sleeping":{ 345 | sleepingIcon = this.getSleepingIcon(colors); 346 | var carState = stack.addImage(sleepingIcon); 347 | carState.tintColor = new Color(colors.icons.default); 348 | carState.imageSize = scaleImage(sleepingIcon.size,carStatusIconSize); 349 | carState.rightAlignImage(); 350 | break; 351 | } 352 | case "Idling":{ 353 | idlingIcon = this.getIdlingIcon(colors); 354 | var carState = stack.addImage(idlingIcon); 355 | carState.tintColor = new Color(colors.icons.default); 356 | carState.imageSize = scaleImage(idlingIcon.size,carStatusIconSize); 357 | carState.rightAlignImage(); 358 | break; 359 | } 360 | case "Driving":{ 361 | drivingIcon = this.getDrivingIcon(colors); 362 | var carState = stack.addImage(drivingIcon); 363 | carState.tintColor = new Color(colors.icons.default); 364 | carState.imageSize = scaleImage(drivingIcon.size,carStatusIconSize); 365 | carState.rightAlignImage(); 366 | break; 367 | } 368 | case "Charging":{ 369 | chargingIcon = this.getChargingIcon(colors); 370 | var carState = stack.addImage(chargingIcon); 371 | carState.imageSize = scaleImage(chargingIcon.size,carStatusIconSize); 372 | carState.rightAlignImage(); 373 | break; 374 | } 375 | default:{ 376 | 377 | } 378 | } 379 | } 380 | 381 | } 382 | 383 | { // helper functions to draw things for car status 384 | 385 | theme.getSleepingIcon = function(colors){ 386 | symbolToUse = "moon.zzz"; 387 | let statusSymbol = SFSymbol.named(symbolToUse); 388 | return statusSymbol.image; 389 | } 390 | 391 | theme.getIdlingIcon = function(colors){ 392 | symbolToUse = "parkingsign.circle"; 393 | let statusSymbol = SFSymbol.named(symbolToUse); 394 | return statusSymbol.image; 395 | } 396 | 397 | theme.getDrivingIcon = function(colors){ 398 | symbolToUse = "play.circle"; 399 | let statusSymbol = SFSymbol.named(symbolToUse); 400 | return statusSymbol.image; 401 | } 402 | 403 | theme.getChargingIcon = function(colors){ 404 | var multiplier=4; 405 | 406 | let iconHeight = 20; 407 | 408 | let carChargingImageContext = new DrawContext(); 409 | carChargingImageContext.opaque = false 410 | carChargingImageContext.size = new Size(iconHeight*multiplier ,iconHeight*multiplier) 411 | 412 | let boltLines = [[5,0],[0,7],[3,7],[2,12],[7,5],[4,5]]; 413 | const boltIcon = new Path() 414 | boltIcon.addLines(scaleLines(boltLines,(iconHeight*multiplier)-(2*multiplier) ,iconHeight*(0.25*multiplier),1*multiplier)); // this scales my bolts to the icon size I want 415 | boltIcon.closeSubpath() 416 | 417 | 418 | carChargingImageContext.setLineWidth(1*multiplier); 419 | carChargingImageContext.setStrokeColor(new Color(colors.icons.charging_bolt_circle)); 420 | carChargingImageContext.strokeEllipse(new Rect(0.15*iconHeight*multiplier,0.15*iconHeight*multiplier,0.7*iconHeight*multiplier,0.7*iconHeight*multiplier)); 421 | carChargingImageContext.addPath(boltIcon); 422 | carChargingImageContext.setFillColor(new Color(colors.icons.charging_bolt)); 423 | carChargingImageContext.fillPath(); 424 | carChargingImageContext.setStrokeColor(new Color(colors.icons.charging_bolt_outline)); 425 | carChargingImageContext.addPath(boltIcon); 426 | carChargingImageContext.strokePath(); 427 | 428 | return carChargingImageContext.getImage(); 429 | 430 | } 431 | 432 | theme.getSentryModeIcon = function(colors){ 433 | 434 | //sentrySymbol = SFSymbol.named("record.circle") 435 | //sentrySymbolImage = sentrySymbol.image 436 | 437 | var multiplier=4; 438 | 439 | let sentryModeContext = new DrawContext() 440 | sentryModeContext.opaque = false 441 | sentryModeContext.size = new Size(20*multiplier,20*multiplier); 442 | //sentryModeContext.imageOpacity = 0.8 443 | //sentryModeContext.drawImageAtPoint(sentrySymbolImage,new Point(0,0)) 444 | 445 | sentryModeContext.setFillColor(new Color(colors.icons.sentry_dot)) 446 | sentryModeContext.fillEllipse(new Rect(6*multiplier,6*multiplier,8*multiplier,8*multiplier)) 447 | 448 | sentryModeContext.setStrokeColor(new Color(colors.icons.default)); 449 | sentryModeContext.setLineWidth(2*multiplier); 450 | sentryModeContext.strokeEllipse(new Rect(2*multiplier,2*multiplier,16*multiplier,16*multiplier)); 451 | 452 | sentryModeContext.setStrokeColor(new Color(colors.icons.disabled)); 453 | sentryModeContext.strokeEllipse(new Rect(6*multiplier,6*multiplier,8*multiplier,8*multiplier)); 454 | 455 | return sentryModeContext.getImage(); 456 | } 457 | } 458 | 459 | theme.drawCarName = function(widget,car_data,colors,widgetSize){ 460 | let stack = widget.addStack(); 461 | stack.size = new Size(widgetSize.width,widgetSize.height*0.25); 462 | stack.centerAlignContent(); 463 | stack.setPadding(2,3,2,3); 464 | 465 | let carName = stack.addText(car_data.car_name); 466 | carName.textColor = new Color(colors.text.primary); 467 | carName.centerAlignText() 468 | carName.font = Font.semiboldSystemFont(24) 469 | carName.minimumScaleFactor = 0.5 470 | } 471 | 472 | theme.drawStatusLights = function(widget,car_data,colors,widgetSize){ 473 | let stack = widget.addStack(); 474 | stack.size = new Size(widgetSize.width,widgetSize.height*0.20); 475 | stack.setPadding(3,10,3,10); 476 | //stack.backgroundColor = new Color(colors.overlay);; 477 | stack.cornerRadius = 10; 478 | stack.centerAlignContent(); 479 | 480 | if (car_data.doors_locked){ 481 | var carControlLockIconImage = this.getLockedIcon(); 482 | } else { 483 | var carControlLockIconImage = this.getUnlockedIcon(); 484 | } 485 | let carControlLockIcon = stack.addImage(carControlLockIconImage); 486 | carControlLockIcon.imageSize = scaleImage(carControlLockIconImage.size,18) 487 | carControlLockIcon.containerRelativeShape = true 488 | carControlLockIcon.tintColor = new Color(colors.icons.default); 489 | carControlLockIcon.borderWidtrh = 1 490 | carControlLockIcon.imageOpacity = 0.8 491 | let carControlSpacer = stack.addSpacer(null) 492 | 493 | if (car_data.inside_temp < 1000){ // if we have a temp for interior 494 | let climateOpacity = 1.0; 495 | if (car_data.data_is_stale) { climateOpacity = 0.3; } // after 2 hours the climate info isn't really valid 496 | carClimateControlIconImage = this.getClimateIcon(); 497 | let carClimateControlIcon = stack.addImage(carClimateControlIconImage); 498 | carClimateControlIcon.imageSize = scaleImage(carClimateControlIcon.image.size,14) 499 | carClimateControlIcon.containerRelativeShape = true 500 | carClimateControlIcon.tintColor = new Color(colors.icons.default); 501 | carClimateControlIcon.borderWidtrh = 1 502 | carClimateControlIcon.imageOpacity = climateOpacity 503 | 504 | var climateText = " "+car_data.inside_temp+"°"; 505 | 506 | if (car_data.climate_active && car_data.temp_setting < 1000){ 507 | let tempDifferential = car_data.inside_temp-car_data.temp_setting; 508 | if (Math.abs(tempDifferential)>1){ // if the temp differential is more than 1 degree 509 | if (tempDifferential<0){ 510 | // the car is heating 511 | carClimateControlIcon.tintColor = new Color(colors.icons.climate_hot); 512 | } else { 513 | carClimateControlIcon.tintColor = new Color(colors.icons.climate_cold); 514 | } 515 | 516 | 517 | climateText += " ➝ "+car_data.temp_setting+"°"; 518 | } 519 | } 520 | let carTemp = stack.addText(climateText) 521 | carTemp.textColor = new Color(colors.icons.default); 522 | carTemp.font = Font.systemFont(15) 523 | carTemp.textOpacity = climateOpacity 524 | } 525 | 526 | } 527 | 528 | { // helper functions to draw things for status lights 529 | 530 | theme.getLockedIcon = function(){ 531 | lockSymbol = SFSymbol.named("lock.fill"); 532 | return lockSymbol.image; 533 | } 534 | 535 | theme.getUnlockedIcon = function(){ 536 | unlockSymbol = SFSymbol.named("lock.open.fill"); 537 | return unlockSymbol.image; 538 | } 539 | 540 | theme.getClimateIcon = function(){ 541 | unlockSymbol = SFSymbol.named("snow"); 542 | return unlockSymbol.image; 543 | } 544 | 545 | 546 | } 547 | 548 | theme.drawRangeInfo = function(widget,car_data,colors,widgetSize){ 549 | let stack = widget.addStack(); 550 | stack.size = new Size(widgetSize.width,widgetSize.height*0.15); 551 | stack.centerAlignContent(); 552 | stack.setPadding(0,10,5,10); 553 | 554 | let batteryCurrentCharge = "" 555 | if (!show_battery_percentage){ 556 | if (car_data.usable_battery_level > -1){ 557 | batteryCurrentCharge = car_data.usable_battery_level + "%" 558 | if (car_data.usable_battery_level < car_data.battery_level){ 559 | batteryCurrentCharge = car_data.usable_battery_level+"/"+car_data.battery_level+"%" 560 | } 561 | if (car_data.carState == "Charging" && show_range){ 562 | // we need to show a reduced size since there's not enough room 563 | batteryCurrentCharge = car_data.battery_level + "%" 564 | } 565 | 566 | let batteryCurrentChargePercentTxt = stack.addText(batteryCurrentCharge) 567 | batteryCurrentChargePercentTxt.textColor = new Color(colors.text.primary); 568 | batteryCurrentChargePercentTxt.textOpacity = 0.6 569 | batteryCurrentChargePercentTxt.font = Font.systemFont(12) 570 | batteryCurrentChargePercentTxt.centerAlignText() 571 | 572 | } 573 | } else { 574 | if (car_data.battery_range > -1){ 575 | /*if (show_battery_percentage){ 576 | let carChargingSpacer1 = stack.addSpacer(null) 577 | }*/ 578 | batteryCurrentCharge = ""+Math.floor(car_data.battery_range)+car_data.distance_label; 579 | if (show_range_est && car_data.est_battery_range > -1) { 580 | batteryCurrentCharge = ""+Math.floor(car_data.est_battery_range)+car_data.distance_label; 581 | } 582 | if (batteryCurrentCharge.length>0){ 583 | let batteryCurrentRangeTxt = stack.addText(batteryCurrentCharge) 584 | batteryCurrentRangeTxt.textColor = new Color(colors.text.primary); 585 | batteryCurrentRangeTxt.textOpacity = 0.6 586 | batteryCurrentRangeTxt.font = Font.systemFont(12) 587 | batteryCurrentRangeTxt.centerAlignText() 588 | } 589 | 590 | } 591 | } 592 | 593 | 594 | 595 | if (car_data.car_state == "Charging"){ 596 | if (show_battery_percentage || show_range){ 597 | let carChargingSpacer2 = stack.addSpacer(null); 598 | } 599 | 600 | 601 | // currently charging 602 | minutes = Math.round((car_data.time_to_charge - Math.floor(car_data.time_to_charge)) * 12) * 5; 603 | if (minutes < 10) {minutes = "0" + minutes} 604 | 605 | chargingSymbol = this.getChargerConnectedIcon(); 606 | 607 | let carControlIconBolt = stack.addImage(chargingSymbol); 608 | carControlIconBolt.imageSize = scaleImage(chargingSymbol.size,12); 609 | carControlIconBolt.tintColor = new Color(colors.text.primary); 610 | carControlIconBolt.imageOpacity = 0.8; 611 | 612 | let carChargeCompleteTime = stack.addText(" "+Math.floor(car_data.time_to_charge)+":"+minutes); 613 | carChargeCompleteTime.textColor = new Color(colors.text.primary); 614 | carChargeCompleteTime.font = Font.systemFont(12); 615 | carChargeCompleteTime.textOpacity = 0.6; 616 | 617 | stack.setPadding(5,5,0,5); 618 | 619 | } else if (car_data.charger_attached){ 620 | // car is connected to charger, but not charging 621 | if (show_battery_percentage || show_range){ 622 | let carChargingSpacer2 = stack.addSpacer(null); 623 | } 624 | chargingSymbol = this.getChargerConnectedIcon(); 625 | let carControlIconBolt = stack.addImage(chargingSymbol); 626 | carControlIconBolt.imageSize = scaleImage(chargingSymbol.size,12); 627 | carControlIconBolt.tintColor = new Color(colors.text.disabled); 628 | carControlIconBolt.imageOpacity = 0.6; 629 | } 630 | 631 | } 632 | 633 | { // helper functions to draw things for range info 634 | 635 | theme.getChargerConnectedIcon = function(){ 636 | lockSymbol = SFSymbol.named("bolt.circle"); 637 | return lockSymbol.image; 638 | } 639 | } 640 | 641 | theme.drawBatteryBar = function(widget,car_data,colors,widgetSize){ 642 | let stack = widget.addStack(); 643 | stack.size = new Size(widgetSize.width,widgetSize.height*0.20); 644 | stack.topAlignContent(); 645 | stack.setPadding(3,0,0,0); 646 | 647 | let batteryBarImg = stack.addImage(battery_bar.draw(car_data,colors,widgetSize)); 648 | //batteryBarImg.imageSize = scaleImage(batteryBarImg.size,(widgetSize.height)*0.20); 649 | //batteryBarImg.imageSize = new Size(130,20) 650 | batteryBarImg.centerAlignImage(); 651 | 652 | 653 | } 654 | 655 | 656 | var battery_bar = { // battery bar draw functions 657 | scale:3, 658 | batteryPath:new Path(), 659 | batteryPathInset:new Path(), 660 | width:widgetSize.width-6, 661 | height:24*3, 662 | init:function(){ 663 | }, 664 | draw:function(car_data,colors,widgetSize){ 665 | this.width = (widgetSize.width-6)*3; 666 | this.batteryPath.addRoundedRect(new Rect(1*this.scale,1*this.scale,this.width,this.height),8*this.scale,8*this.scale); 667 | this.batteryPathInset.addRoundedRect(new Rect(2*this.scale,2*this.scale,this.width-2*this.scale,this.height-2*this.scale),5*this.scale,5*this.scale); 668 | 669 | let myDrawContext = new DrawContext(); 670 | myDrawContext.opaque = false; 671 | myDrawContext.size = new Size(this.width+2*this.scale,this.height+2*this.scale); 672 | 673 | this.drawStart(car_data,colors,widgetSize,myDrawContext); 674 | 675 | this.drawMaxCharge(car_data,colors,widgetSize,myDrawContext); 676 | this.drawUsableBattery(car_data,colors,widgetSize,myDrawContext); 677 | this.drawText(car_data,colors,widgetSize,myDrawContext); 678 | 679 | this.drawEnd(car_data,colors,widgetSize,myDrawContext); 680 | 681 | 682 | return myDrawContext.getImage(); // return our final image 683 | 684 | 685 | }, 686 | drawStart:function(car_data,colors,widgetSize,myDrawContext){ 687 | // draw the background 688 | myDrawContext.addPath(this.batteryPath); 689 | myDrawContext.setFillColor(new Color(colors.battery.background)); 690 | myDrawContext.fillPath(); 691 | }, 692 | drawMaxCharge:function(car_data,colors,widgetSize,myDrawContext){ 693 | // draw the max charge (as set by the user) 694 | let batteryMaxCharge = new DrawContext() ; 695 | batteryMaxCharge.opaque = false; 696 | batteryMaxCharge.size = new Size(this.width*car_data.battery_limit/100,this.height) 697 | if (car_data.car_state == "Charging"){ 698 | batteryMaxCharge.setFillColor(new Color(colors.battery.charging)); 699 | } else { 700 | batteryMaxCharge.setFillColor(new Color(colors.battery.max_charge)); 701 | } 702 | batteryMaxCharge.addPath(this.batteryPath); 703 | batteryMaxCharge.fillPath(); 704 | 705 | myDrawContext.drawImageAtPoint(batteryMaxCharge.getImage(),new Point(0,0)); 706 | }, 707 | drawUsableBattery:function(car_data,colors,widgetSize,myDrawContext){ 708 | 709 | let usable_battery_level = Number(car_data.usable_battery_level); 710 | 711 | // draw the cold battery (if needed) 712 | if (usable_battery_level+1 < car_data.battery_level && car_data.battery_level > 1){ // draw the cold battery if it's cold enough. 713 | let unavailableCharge = new DrawContext() ; 714 | unavailableCharge.opaque = false; 715 | unavailableCharge.size = new Size(this.width*car_data.battery_level/100,this.height); 716 | 717 | unavailableCharge.setFillColor(new Color(colors.battery.cold_charge)); 718 | unavailableCharge.addPath(this.batteryPath); 719 | unavailableCharge.fillPath(); 720 | 721 | myDrawContext.drawImageAtPoint(unavailableCharge.getImage(),new Point(0,0)); 722 | 723 | usable_battery_level -= 1; // shave a little off so the cold battery display isn't just a sliver of blue. 724 | } 725 | 726 | // draw the available charge 727 | if (usable_battery_level>1){ 728 | // if there's at least some battery, build the current charge state bar 729 | let availableCharge = new DrawContext() ; 730 | availableCharge.opaque = false; 731 | availableCharge.size = new Size(this.width*usable_battery_level/100,this.height); 732 | 733 | if (usable_battery_level < 21 && car_data.car_state != "Charging"){ // draw the available battery in yellow if it's under 20% 734 | availableCharge.setFillColor(new Color(colors.battery.low_charge)); 735 | } else { 736 | availableCharge.setFillColor(new Color(colors.battery.usable_charge)); 737 | } 738 | availableCharge.addPath(this.batteryPath); 739 | availableCharge.fillPath(); 740 | 741 | myDrawContext.drawImageAtPoint(availableCharge.getImage(),new Point(0,0)); 742 | 743 | 744 | } 745 | }, 746 | drawText:function(car_data,colors,widgetSize,myDrawContext){ 747 | 748 | let usable_battery_level = Number(car_data.usable_battery_level); 749 | 750 | let text_to_draw = ""; 751 | 752 | if (show_battery_percentage){ 753 | text_to_draw = usable_battery_level+"%"; 754 | } else { 755 | text_to_draw = ""+Math.floor(car_data.battery_range)+car_data.distance_label; 756 | if (show_range_est && car_data.est_battery_range > -1) { 757 | text_to_draw = ""+Math.floor(car_data.est_battery_range)+car_data.distance_label; 758 | } 759 | 760 | } 761 | // draw battery percentage 762 | myDrawContext.setTextColor(new Color(colors.text.primary)); 763 | myDrawContext.setFont(Font.systemFont(14*this.scale)); 764 | myDrawContext.setFontSize(14*this.scale); 765 | if (usable_battery_level>35){ 766 | // draw inside the battery 767 | myDrawContext.setTextAlignedRight(); 768 | myDrawContext.drawTextInRect(text_to_draw,new Rect(0,4.25*this.scale,this.width*usable_battery_level/100-(4*this.scale),this.height-8*this.scale)); 769 | } else { 770 | if (car_data.car_state != "Charging") {myDrawContext.setTextColor(new Color(colors.text.inverted));} // invert the color if we're not charging. 771 | myDrawContext.setTextAlignedLeft(); 772 | myDrawContext.drawTextInRect(text_to_draw,new Rect(this.width*usable_battery_level/100+(4.25*this.scale),4*this.scale,this.width-(this.width*usable_battery_level/100),this.height-8*this.scale)); 773 | } 774 | 775 | 776 | }, 777 | drawEnd:function(car_data,colors,widgetSize,myDrawContext){ 778 | // add a final stroke to the whole thing 779 | let usable_battery_level = Number(car_data.usable_battery_level); 780 | 781 | if (Number(car_data.battery_level)<99){ 782 | myDrawContext.setFillColor(new Color(colors.battery.separator)); 783 | myDrawContext.fillRect(new Rect(this.width*car_data.battery_level/100,1,1,this.height-1)); 784 | if (usable_battery_level < car_data.battery_level){ 785 | myDrawContext.fillRect(new Rect(this.width*usable_battery_level/100,1,1,this.height-1)) 786 | } 787 | } 788 | 789 | 790 | myDrawContext.addPath(this.batteryPath);// have to add the path again for some reason 791 | myDrawContext.setStrokeColor(new Color(colors.battery.border)); 792 | myDrawContext.setLineWidth(1.5*this.scale); 793 | myDrawContext.strokePath(); 794 | 795 | } 796 | 797 | } 798 | battery_bar.init(); 799 | } 800 | 801 | 802 | 803 | if (APItype == "" || APItype == "TeslaFi"){ 804 | // Add some backward compatibility to TeslaFi (if the APIurl is just a token, then assume it's a TeslaFi API key, otherwise, just use the URL 805 | if (APIurl != null && APIurl != "" && !(APIurl.match(/\./g) || []).length){ 806 | APIkey = APIurl; 807 | APIurl = "https://www.teslafi.com/feed.php?token="+APIurl+"&command=lastGood&encode=1"; 808 | } 809 | 810 | if (APIurl != null && APIurl != "" && (APIurl.match(/teslafi/gi) || []).length){ 811 | car_data.source = "TeslaFi" 812 | } 813 | } 814 | 815 | 816 | if (config.runsInWidget || args.queryParameters.widget != null){ 817 | // Start processing our code (load the car data, then render) 818 | 819 | if (typeof APIurl === 'undefined'){APIurl = "";} 820 | let response = await loadCarData(APIurl) 821 | 822 | addMapArea(); // after loading car data we can decide if we can display the map 823 | 824 | 825 | if (response == "ok"){ 826 | let widget = await createWidget(car_data,colors); 827 | Script.setWidget(widget); 828 | presentWidget(widget); 829 | Script.complete(); 830 | } else { 831 | let widget = errorWidget(response); 832 | Script.setWidget(widget); 833 | presentWidget(widget); 834 | Script.complete(); 835 | } 836 | } else { 837 | await showConfig(); 838 | Script.complete(); 839 | } 840 | 841 | function presentWidget(widget){ 842 | switch (debug_size){ 843 | case "medium": 844 | widget.presentMedium(); 845 | break; 846 | case "large": 847 | widget.presentLarge(); 848 | break; 849 | case "small": 850 | default: 851 | widget.presentSmall(); 852 | break; 853 | } 854 | } 855 | 856 | async function createWidget(car_data,colors) { 857 | themeDebugArea(); 858 | 859 | let td_theme = FileManager.iCloud() 860 | 861 | // create the themes directory if needed (so the user doesn't have to do this) 862 | theme_file = td_theme.joinPath(td_theme.documentsDirectory(),"tesla_data"); 863 | if (!td_theme.isDirectory(theme_file)){ 864 | // create the directory 865 | td_theme.createDirectory(theme_file); 866 | } 867 | 868 | if ((custom_theme != "" && custom_theme != "none") || custom_theme != null){ 869 | // load a custom theme 870 | theme_file = td_theme.joinPath(td_theme.documentsDirectory(),"tesla_data/themes/"+custom_theme+".js"); 871 | 872 | if (td_theme.fileExists(theme_file)){ 873 | td_theme.downloadFileFromiCloud(theme_file); 874 | eval(td_theme.readString(theme_file)); 875 | } 876 | } 877 | 878 | let w = new ListWidget() 879 | theme.init(); 880 | await theme.draw(w,car_data,colors); 881 | 882 | 883 | return w 884 | } 885 | 886 | function errorWidget(reason){ 887 | let w = new ListWidget() 888 | 889 | drawErrorWidget(w,reason); 890 | 891 | return w 892 | } 893 | 894 | function drawErrorWidget(w,reason){ 895 | w.setPadding(5,5,5,5) 896 | let myGradient = new LinearGradient() 897 | 898 | w.backgroundColor = new Color("#933") 899 | myGradient.colors = [new Color("#44444466"), new Color("#88888855"), new Color("#66666655")] 900 | myGradient.locations = [0,0.8,1] 901 | w.backgroundGradient = myGradient 902 | 903 | 904 | let title = w.addText("Error") 905 | title.textColor = Color.white() 906 | title.font = Font.semiboldSystemFont(30) 907 | title.minimumScaleFactor = 0.5 908 | 909 | let reasonText = w.addText(reason) 910 | reasonText.textColor = Color.white() 911 | reasonText.minimumScaleFactor = 0.5 912 | 913 | } 914 | 915 | async function loadCarData(url) { 916 | 917 | if (url != null && url != ""){ 918 | 919 | // get the data from APIurl, then build our internal car_data object 920 | var req = await new Request(url); 921 | var backupManager = FileManager.local(); 922 | var backupLocation = backupManager.joinPath(backupManager.libraryDirectory(), "tesla_data.txt") 923 | 924 | 925 | if (debug_data==""){ 926 | try{ 927 | var json = await req.loadJSON(); 928 | if (json.response == null){ 929 | var jsonExport = JSON.stringify(json); 930 | backupManager.writeString(backupLocation,jsonExport); 931 | } 932 | 933 | }catch(e){ 934 | // offline, grab the backup copy 935 | var jsonImport = backupManager.readString(backupLocation); 936 | var json = JSON.parse(jsonImport); 937 | } 938 | } else { 939 | // TeslaFi only allows 3 API calls per minute, so during testing, we can just pull test data from iCloud 940 | 941 | let debugManager = FileManager.iCloud(); 942 | debug_file = debugManager.joinPath(debugManager.documentsDirectory(),"tesla_data/debug_data/"+debug_data+".json"); 943 | 944 | if (debugManager.fileExists(debug_file)){ 945 | debugManager.downloadFileFromiCloud(debug_file); 946 | var json = await JSON.parse(debugManager.readString(debug_file)); 947 | } else { 948 | var json = {"response":{"result":"That debug file doesn't exist"}}; 949 | } 950 | } 951 | } else { 952 | var json = getSampleData(); // the user hasn't provided a url, so we'll show sample data 953 | var now = new Date(); 954 | now.setTime(now.getTime() + 60000*5); 955 | json.Date = now.toISOString(); 956 | //console.log(json); 957 | } 958 | 959 | 960 | //logError(json); 961 | 962 | //process any of the items 963 | 964 | if (json.response == null){ 965 | 966 | // go through our data and normalize/clean up things so they're ready to be used. 967 | 968 | // required data 969 | if (json.usable_battery_level != null && json.usable_battery_level > 0) 970 | {car_data.usable_battery_level = Math.floor(json.usable_battery_level);} 971 | else 972 | {car_data.usable_battery_level = Math.floor(json.battery_level);} 973 | 974 | if (json.charge_limit_soc != null){car_data.battery_limit = json.charge_limit_soc ;} 975 | 976 | //optional data 977 | if (json.display_name != null){car_data.car_name = json.display_name ;} 978 | if (json.battery_level != null){car_data.battery_level = Math.floor(json.battery_level);} 979 | if (json.battery_range != null){car_data.battery_range = Math.floor(json.battery_range);} 980 | if (json.est_battery_range != null){car_data.est_battery_range = Math.floor(json.est_battery_range);} 981 | if (json.measure != null){car_data.distance_label = (json.measure == "imperial")?"mi":"km";} 982 | if (json.carState != null){car_data.car_state = json.carState ;} 983 | if (json.sentry_mode != null){car_data.sentry_mode = (json.sentry_mode == 1);} 984 | if (json.locked != null){car_data.doors_locked = (json.locked == 1) ;} 985 | if (json.is_climate_on != null){car_data.climate_active = (json.is_climate_on == 1) ;} 986 | if (json.temperature != null){car_data.temp_label = (json.temperature == "F")?"f":"c";} 987 | if (json.inside_temp != null){car_data.inside_temp = Math.floor((car_data.temp_label == "c")?json.inside_temp:json.inside_temp*1.8+32);} 988 | if (json.driver_temp_setting != null){car_data.temp_setting = Math.floor((car_data.temp_label == "c")?json.driver_temp_setting:json.driver_temp_setting*1.8+32);} 989 | if (json.time_to_full_charge != null){car_data.time_to_charge = json.time_to_full_charge ;} 990 | if (json.fast_charger_type != null){car_data.charger_attached = (json.fast_charger_type != "") ;} 991 | 992 | if (json.longitude != null){car_data.longitude = json.longitude;} 993 | if (json.latitude != null){car_data.latitude = json.latitude;} 994 | 995 | if (json.Date != null){ 996 | let lastUpdate = new Date(json.Date.replace(" ","T")) 997 | let now = new Date() 998 | timeDiff = Math.round((Math.abs(now - lastUpdate))/(1000 * 60)) 999 | if (timeDiff < 60) { 1000 | // been less than an hour since last update 1001 | car_data.last_contact = timeDiff+"m ago" 1002 | } else if(timeDiff < 1440){ 1003 | car_data.last_contact = Math.floor(timeDiff/60)+"h ago" 1004 | } else { 1005 | car_data.last_contact = Math.floor(timeDiff/1440)+"d ago" 1006 | } 1007 | if (timeDiff/60 > 2){ 1008 | car_data.data_is_stale = true; // data is more than 2 hours old. 1009 | } 1010 | } 1011 | 1012 | car_data.postLoad(json); 1013 | 1014 | return "ok"; 1015 | } else { 1016 | return json.response.result 1017 | } 1018 | } 1019 | 1020 | 1021 | // utility functions 1022 | 1023 | function scaleLines(lineArray,maxHeight,offsetX,offsetY){ 1024 | //scale an array of lines and make it an array of scaled Points 1025 | let pointArray = []; 1026 | let scaleFactor = 0; 1027 | for(var i = 0;i scaleFactor){scaleFactor = lineArray[i][1];} 1029 | //console.log(i+" : "+scaleFactor); 1030 | } 1031 | scaleFactor = maxHeight/scaleFactor; 1032 | for(var i = 0;i width) { width = deviceScreen.height;} 1051 | icon_size = 55; 1052 | gutter_size = ((deviceScreen.width - 360) /7); 1053 | } 1054 | 1055 | var extra_size = 10 - widgetPadding; 1056 | 1057 | var widgetSize = new Size(gutter_size + icon_size+extra_size, gutter_size + icon_size+extra_size); // small widget size 1058 | widgetSize.gutter_size = gutter_size; 1059 | 1060 | var widgetSizing = debug_size; 1061 | if (config.widgetFamily != null){ 1062 | widgetSizing = config.widgetFamily; 1063 | } 1064 | switch (widgetSizing){ 1065 | case "medium": 1066 | widgetSize = new Size(gutter_size*3 + (icon_size*2) + extra_size, gutter_size + icon_size + extra_size); // medium widget size 1067 | break; 1068 | case "large": 1069 | widgetSize = new Size(gutter_size*3 + (icon_size*2) + extra_size, gutter_size*3 + (icon_size*2) + extra_size); // large widget size 1070 | break; 1071 | } 1072 | 1073 | 1074 | 1075 | return widgetSize 1076 | } 1077 | 1078 | 1079 | 1080 | 1081 | function getSampleData(){ 1082 | return { 1083 | "response":null, 1084 | "battery_level":27, 1085 | "usable_battery_level":26, 1086 | "charge_limit_soc":90, 1087 | "carState":"Idling", 1088 | "Date":"2020-10-28T14:57:15Z", 1089 | "sentry_mode":0, 1090 | "display_name":"No Source", 1091 | "locked":1, 1092 | "is_climate_on":0, 1093 | "inside_temp":14.6, 1094 | "driver_temp_setting":22.0, 1095 | "measure":"km", 1096 | "est_battery_range":90.605842, 1097 | "battery_range":125.2227454, 1098 | "time_to_full_charge":0.0, 1099 | "fast_charger_type":"" 1100 | } 1101 | } 1102 | 1103 | 1104 | async function showConfig(){ 1105 | // Show the config screen inside of Scriptable 1106 | 1107 | 1108 | online_version_url = "https://raw.githubusercontent.com/DrieStone/TeslaData-Widget/main/documentation/version.js"; 1109 | 1110 | req = new Request(online_version_url); 1111 | versioning_string = await req.loadString(); 1112 | eval(versioning_string); 1113 | 1114 | var version_message; 1115 | //console.error(online_version+" : "+this_version); 1116 | 1117 | var version_message = ""; 1118 | 1119 | if (this_version < online_version){ 1120 | version_message = "
There is a new version of TeslaData available.
" 1121 | } 1122 | 1123 | 1124 | var paramString = ""; 1125 | // if there are arguments, update our values 1126 | //console.error(args.queryParameters); 1127 | if (args.queryParameters.APItypeSelect != null){ 1128 | switch (args.queryParameters.APItypeSelect){ 1129 | case "Other": 1130 | APItype = "Other"; 1131 | APIurl = args.queryParameters.APIurl; 1132 | paramString += "APIurl = \""+APIurl+"\";\n"; 1133 | paramString += "APItype = \""+APItype+"\";\n"; 1134 | break; 1135 | case "Tesla": 1136 | APItype = "Tesla"; 1137 | Tesla_Email = args.queryParameters.tesla_email; 1138 | Tesla_Password = args.queryParameters.tesla_password; 1139 | paramString += "Tesla_Email = \""+Tesla_Email+"\";\n"; 1140 | paramString += "Tesla_Password = \""+Tesla_Password+"\";\n"; 1141 | paramString += "APItype = \""+APItype+"\";\n"; 1142 | 1143 | break; 1144 | case "TeslaFi": 1145 | default: 1146 | APItype = "TeslaFi"; 1147 | APIkey = APIurl = args.queryParameters.APIkey; 1148 | paramString += "APIurl = \""+APIkey+"\";\n"; 1149 | paramString += "APItype = \""+APItype+"\";\n"; 1150 | break; 1151 | } 1152 | 1153 | if (args.queryParameters.show_map == "yes"){ 1154 | hide_map = false; 1155 | paramString += "hide_map = false;\n"; 1156 | } else { 1157 | hide_map = true; 1158 | paramString += "hide_map = true;\n"; 1159 | } 1160 | 1161 | paramString += "custom_theme = \""+args.queryParameters.theme+"\";\n"; 1162 | custom_theme = args.queryParameters.theme; 1163 | 1164 | 1165 | if (args.queryParameters.battery_percentage == "yes"){ 1166 | show_battery_percentage = true; 1167 | paramString += "show_battery_percentage = true;\n"; 1168 | } else { 1169 | show_battery_percentage = false; 1170 | paramString += "show_battery_percentage = false;\n"; 1171 | } 1172 | 1173 | if (args.queryParameters.show_range == "yes"){ 1174 | show_range = true; 1175 | paramString += "show_range = true;\n"; 1176 | } else { 1177 | show_range = false; 1178 | paramString += "show_range = false;\n"; 1179 | } 1180 | 1181 | if (args.queryParameters.show_range_est == "yes"){ 1182 | show_range_est = true; 1183 | paramString += "show_range_est = true;\n"; 1184 | } else { 1185 | show_range_est = false; 1186 | paramString += "show_range_est = false;\n"; 1187 | } 1188 | 1189 | if (args.queryParameters.show_data_age == "yes"){ 1190 | show_data_age = true; 1191 | paramString += "show_data_age = true;\n"; 1192 | } else { 1193 | show_data_age = false; 1194 | paramString += "show_data_age = false;\n"; 1195 | } 1196 | 1197 | if (args.queryParameters.MapKey != null && args.queryParameters.MapKey != ""){ 1198 | mapKey = args.queryParameters.MapKey; 1199 | paramString += "mapKey = \""+args.queryParameters.MapKey+"\";\n"; 1200 | } 1201 | 1202 | if (args.queryParameters.mapChoice == "Google"){ 1203 | useGoogleMaps = true; 1204 | paramString += "useGoogleMaps = true;\n"; 1205 | } else { 1206 | useGoogleMaps = false; 1207 | paramString += "useGoogleMaps = false;\n"; 1208 | } 1209 | 1210 | // save parameters from a file on iCloud 1211 | let additional_manager = FileManager.iCloud() 1212 | api_file = additional_manager.joinPath(additional_manager.documentsDirectory(),"tesla_data/parameters.js"); 1213 | additional_manager.writeString(api_file,paramString); 1214 | 1215 | } else { 1216 | 1217 | // load parameters from a file on iCloud (shouldn't need to do this since it should have been done earlier in code 1218 | /*let additional_manager = FileManager.iCloud() 1219 | api_file = additional_manager.joinPath(additional_manager.documentsDirectory(),"tesla_data/parameters.js"); 1220 | 1221 | if (additional_manager.fileExists(api_file)){ 1222 | additional_manager.downloadFileFromiCloud(api_file); 1223 | eval(additional_manager.readString(api_file)); 1224 | }*/ 1225 | } 1226 | // Now our values should be set correctly 1227 | 1228 | if (typeof APIurl === 'undefined'){APIurl = "";} 1229 | 1230 | if (APIurl != null && APIurl != "" && !(APIurl.match(/\./g) || []).length){ 1231 | APIkey = APIurl; 1232 | APIurl = "https://www.teslafi.com/feed.php?token="+APIurl+"&command=lastGood&encode=1"; 1233 | } 1234 | 1235 | 1236 | // get a list of available themes from the themes directory 1237 | let themeManager = FileManager.iCloud(); 1238 | themePath = themeManager.joinPath(themeManager.documentsDirectory(),"tesla_data/themes"); 1239 | themeList = themeManager.listContents(themePath); 1240 | let i =0; 1241 | let themeOptions = ""; 1242 | while (themeList[i]){ 1243 | if (custom_theme == (themeList[i].replace(/\.[^/.]+$/, ""))){ 1244 | themeOptions += ''; 1245 | } else { 1246 | themeOptions += ''; 1247 | } 1248 | i++; 1249 | } 1250 | 1251 | 1252 | var mapChecked = "checked"; 1253 | var batteryPercentChecked = "checked"; 1254 | var rangeChecked = "checked"; 1255 | var APIrangeChecked = "checked"; 1256 | var ageChecked = "checked"; 1257 | var mapGoogleSelected = mapAppleSelected = ""; 1258 | 1259 | if (hide_map){ 1260 | mapChecked = ""; 1261 | } 1262 | if (!show_battery_percentage){ 1263 | batteryPercentChecked = ""; 1264 | } 1265 | if (!show_range){ 1266 | rangeChecked = ""; 1267 | } 1268 | if (!show_range_est){ 1269 | APIrangeChecked = ""; 1270 | } 1271 | if (!show_data_age){ 1272 | ageChecked = ""; 1273 | } 1274 | if (useGoogleMaps){ 1275 | mapGoogleSelected = " selected" 1276 | } else { 1277 | mapAppleSelected = " selected" 1278 | } 1279 | var teslaFiSelected = otherSelected = teslaSelected = showType = ""; 1280 | switch (APItype){ 1281 | case "TeslaFi": 1282 | teslaFiSelected = " selected"; 1283 | showType="showteslaFi"; 1284 | break; 1285 | case "Other": 1286 | otherSelected = " selected"; 1287 | showType="showother"; 1288 | break; 1289 | case "Tesla": 1290 | teslaSelected = " selected"; 1291 | showType="showtesla"; 1292 | break; 1293 | 1294 | } 1295 | 1296 | // HTML 1297 | var html=` 1298 | 1299 | 1300 | 1301 | 1302 | 1303 | 1403 | 1404 | 1405 | 1406 | ${version_message} 1407 |
Options Updated
1408 |
1409 |
1410 | API (required) 1411 | 1417 |
1418 | 1419 |
1420 | 1422 |
1423 |
1424 | 1426 |
1427 |
1428 | 1430 | 1432 |
1433 |
1434 |
1435 | 1436 | 1441 | 1442 | 1443 |
1444 | Map Options 1445 | 1446 | 1447 | 1448 |
1449 | 1450 |
1451 | Other Options 1452 | 1453 | 1454 |
1455 | 1456 | 1457 |
1458 |

*Your Tesla account information is stored on your device and only transmitted to authenticate with your vehicle to get a key from Tesla once in a while.

1459 |
1460 |

Load Debug Widget

1461 | Small 1462 | Medium 1463 | Large 1464 |

TeslaData version ${this_version}

1465 |
1466 | 1472 | 1473 | 1474 | 1475 | 1476 | ` 1477 | 1478 | // WebView 1479 | WebView.loadHTML(html, null, new Size(0, 100)); 1480 | 1481 | } 1482 | 1483 | 1484 | 1485 | 1486 | function themeDebugArea(){ 1487 | // This is a working area for theme development (so errors will give you correct line numbers 1488 | // Once you've finished, move your code to a JS file in the tesla_data folder 1489 | 1490 | 1491 | 1492 | 1493 | 1494 | 1495 | 1496 | 1497 | } 1498 | 1499 | -------------------------------------------------------------------------------- /documentation/Developers.md: -------------------------------------------------------------------------------- 1 | # Rough theme development overveiw 2 | 3 | In general, you should use themes for any customization you want to do to the display or data. Refer to the [Scriptable documentation](https://scriptable.app/) for relavant javascript functions. 4 | 5 | ## colors 6 | 7 | The easiest way to theme the widget is to modify the colors. This can be done by overriding the colors used in the theme file: 8 | 9 | colors.background:"#ddbbbb"; 10 | 11 | Colors should be hex colors, RRGGBBAA (red, green, blue, alpha) or RRGGBB (red, green, blue). RGB (red, green, blue) is valid, but there's a bug in Scriptable where green and blue are swapped. It's recommended not to use this format. 12 | 13 | ## car_data.postLoad(json) 14 | 15 | The postLoad function can be overwritten so you can consume additional data from your json file and modify (or add) variables to the car_data object. For instance, you may wish to grab your car's color and add it to car_data: 16 | 17 | 18 | car_data.postLoad = function (json){ 19 | var this.car_color = json.exterior_color; 20 | var colors.car_color = "#ffffff"; 21 | switch (this.car_color){ 22 | case "deepBlue": 23 | colors.car_color = "#0000ff"; 24 | break; 25 | } 26 | } 27 | 28 | ## Theme 29 | 30 | The theme object is called to draw the data to the widget. Themes are broken down into small, medium, and large (so you can write a theme that supports all three sizes). A theme.[size].available variable is set to let the system know if the theme is available in that size. 31 | 32 | You can override the theme's draw function to completely change the way that the widget is drawn. This should be done by replacing the theme.small.draw function. 33 | 34 | However, if you want to use the existing theme, but change certain parts, the existing theme is split into 5 boxes that are stacked vertically: Car Status, Car Name, Status Lights, Range Info, and Battery Bar. You can override any of these functions to provide your own style. 35 | 36 | For instance, if you wanted to change the way that the car's name is displayed, you can override the drawCarName function: 37 | 38 | theme.drawCarName = function (widget, car_data, colors){ 39 | let stack = widget.addStack(); 40 | stack.size = new Size(widgetSize.width,widgetSize.height*0.25); 41 | stack.centerAlignContent(); 42 | stack.setPadding(0,3,5,3); 43 | 44 | let carName = stack.addText(car_data.car_name); 45 | carName.textColor = new Color(colors.text.primary); 46 | carName.leftAlignText() // align the text left instead of center 47 | carName.font = Font.semiboldSystemFont(24) 48 | carName.minimumScaleFactor = 0.5 49 | } 50 | 51 | ## Icons 52 | 53 | Iconography is broken into separate functions as well, so you can override one of these functions to change the icon used to draw for certain states: 54 | 55 | theme.getSleepingIcon = function(colors){ 56 | symbolToUse = "zzz"; // change from moon.zzz.fill to a plain zzz icon 57 | let statusSymbol = SFSymbol.named(symbolToUse); 58 | return statusSymbol.image; 59 | } 60 | 61 | ## Battery Bar 62 | 63 | You can review the themes directory to see the way that the 3d.js file restyles the battery bar. The battery bar is its own object that you can modify. You can replace the entire draw function to create a different style battery bar. The function must return an image. 64 | -------------------------------------------------------------------------------- /documentation/config.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DrieStone/TeslaData-Widget/db0b661fb7cdf46a4b26c8f442818bb68ccdebb5/documentation/config.png -------------------------------------------------------------------------------- /documentation/json_requirements.md: -------------------------------------------------------------------------------- 1 | # Overview of the fields in that are consumed by the widget. 2 | 3 | ## Required 4 | * usable_battery_level : the actual usable amount (0-100), this is used when the battery is cold 5 | * charge_limit_soc : the charge limit you have configured for the car (0-100) 6 | 7 | Although, it's a good idea to include all of the following fields, the widget will still render what it can when you include any of the following fields. 8 | 9 | ## Optional 10 | * response : should be null unless there is an error 11 | * Date : the last contact with the vehicle (This can be almost any date format, but should include date and time, TelsaFi uses UTC style YYYY-MM-DD HH:MM:SS) 12 | * battery_level : the current battery value (0-100) 13 | * carState : the current state of the car (Sleeping, Charging, Driving, Idle) 14 | * sentry_mode : is sentry mode on (0 or 1) 15 | * display_name : the name of the car (as defined by the user) 16 | * locked : are the doors locked (0 or 1) 17 | * is_climate_on : is the car's climate system on (0 or 1) 18 | * inside_temp : the inside temperature of the car in C 19 | * driver_temp_setting : the set temp for the climate control in C 20 | * temperature : if we should display F or C temperatures 21 | * inside_tempF : the inside temperature of the car in F (only used if temperature == F) 22 | * driver_temp_settingF : the set temp for the climate control in F (only used if temperature == F) 23 | * measure : if the range/distance measurements are in km or m 24 | * battery_range : the range of the car (as calculated by the car) 25 | * est_battery_range : the range of the car (as calculated by the logging service) 26 | * time_to_full_charge : how long until the car is fully charged 27 | * fast_charger_type : what kind of charger is attached. Should be "" if the car is not connected to a charger (the widget only checks to see if it's attached, so the actual type is unimportant). 28 | * latitude : the latitude location of the car 29 | * longitude : the longitude location of the car 30 | -------------------------------------------------------------------------------- /documentation/sample.json: -------------------------------------------------------------------------------- 1 | { 2 | "response":null, 3 | "battery_level":27, 4 | "usable_battery_level":26, 5 | "charge_limit_soc":90, 6 | "carState":"Idling", 7 | "Date":"2020-10-28T14:57:15Z", 8 | "sentry_mode":0, 9 | "display_name":"Name", 10 | "locked":1, 11 | "is_climate_on":0, 12 | "inside_temp":14.6, 13 | "driver_temp_setting":22.0, 14 | "measure":"km", 15 | "est_battery_range":90.605842, 16 | "battery_range":125.2227454, 17 | "time_to_full_charge":0.0, 18 | "fast_charger_type":"" 19 | } 20 | -------------------------------------------------------------------------------- /documentation/screen_001.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DrieStone/TeslaData-Widget/db0b661fb7cdf46a4b26c8f442818bb68ccdebb5/documentation/screen_001.png -------------------------------------------------------------------------------- /documentation/screen_001_med.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DrieStone/TeslaData-Widget/db0b661fb7cdf46a4b26c8f442818bb68ccdebb5/documentation/screen_001_med.png -------------------------------------------------------------------------------- /documentation/screen_002.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DrieStone/TeslaData-Widget/db0b661fb7cdf46a4b26c8f442818bb68ccdebb5/documentation/screen_002.png -------------------------------------------------------------------------------- /documentation/screen_003.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DrieStone/TeslaData-Widget/db0b661fb7cdf46a4b26c8f442818bb68ccdebb5/documentation/screen_003.png -------------------------------------------------------------------------------- /documentation/screen_3d_002.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DrieStone/TeslaData-Widget/db0b661fb7cdf46a4b26c8f442818bb68ccdebb5/documentation/screen_3d_002.png -------------------------------------------------------------------------------- /documentation/screen_3d_003.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DrieStone/TeslaData-Widget/db0b661fb7cdf46a4b26c8f442818bb68ccdebb5/documentation/screen_3d_003.png -------------------------------------------------------------------------------- /documentation/screen_map_001.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DrieStone/TeslaData-Widget/db0b661fb7cdf46a4b26c8f442818bb68ccdebb5/documentation/screen_map_001.png -------------------------------------------------------------------------------- /documentation/theme_listing.md: -------------------------------------------------------------------------------- 1 | # Listing of themes available 2 | 3 | ## 3D Bar 4 | 5 | Shows the battery bar in a 3D style. 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /documentation/version.js: -------------------------------------------------------------------------------- 1 | // this is a remote file that is loaded in the configuration to notify the user if there is a new version of the widget available 2 | online_version = 1.7; 3 | 4 | 5 | -------------------------------------------------------------------------------- /tesla_data/3d.js: -------------------------------------------------------------------------------- 1 | 2 | colors.battery.border_3d = "#33333333"; 3 | 4 | battery_bar.draw = function(car_data,colors){ 5 | let myDrawContext = new DrawContext(); 6 | myDrawContext.opaque = false; 7 | myDrawContext.size = new Size(this.width+2,this.height+2); 8 | 9 | // draw the background 10 | myDrawContext.addPath(this.batteryPath); 11 | myDrawContext.setFillColor(new Color(colors.battery.background)); 12 | myDrawContext.fillPath(); 13 | 14 | // draw the max charge (as set by the user) 15 | let batteryMaxCharge = new DrawContext() ; 16 | batteryMaxCharge.opaque = false; 17 | batteryMaxCharge.size = new Size(this.width*car_data.battery_limit/100,this.height) 18 | if (car_data.car_state == "Charging"){ 19 | batteryMaxCharge.setFillColor(new Color(colors.battery.charging)); 20 | } else { 21 | batteryMaxCharge.setFillColor(new Color(colors.battery.max_charge)); 22 | } 23 | batteryMaxCharge.addPath(this.batteryPath); 24 | batteryMaxCharge.fillPath(); 25 | 26 | myDrawContext.drawImageAtPoint(batteryMaxCharge.getImage(),new Point(0,0)); 27 | 28 | let usable_battery_level = Number(car_data.usable_battery_level); 29 | 30 | // draw the cold battery (if needed) 31 | if (usable_battery_level < car_data.battery_level && car_data.battery_level > 1){ 32 | let unavailableCharge = new DrawContext() ; 33 | unavailableCharge.opaque = false; 34 | unavailableCharge.size = new Size(this.width*car_data.battery_level/100,this.height); 35 | 36 | unavailableCharge.setFillColor(new Color(colors.battery.cold_charge)); 37 | unavailableCharge.addPath(this.batteryPath); 38 | unavailableCharge.fillPath(); 39 | 40 | myDrawContext.drawImageAtPoint(unavailableCharge.getImage(),new Point(0,0)); 41 | 42 | usable_battery_level -= 2; // shave a little off so the cold battery display isn't just a sliver of blue. 43 | } 44 | 45 | 46 | myDrawContext.addPath(this.batteryPathInset); 47 | myDrawContext.setStrokeColor(new Color(colors.battery.border_3d,0.3)); 48 | myDrawContext.setLineWidth(4); 49 | myDrawContext.strokePath(); 50 | 51 | 52 | // draw the available charge 53 | if (usable_battery_level>1){ 54 | // if there's at least some battery, build the current charge state bar 55 | let availableCharge = new DrawContext() ; 56 | availableCharge.opaque = false; 57 | availableCharge.size = new Size(this.width*usable_battery_level/100,this.height); 58 | 59 | availableCharge.setFillColor(new Color(colors.battery.usable_charge)); 60 | availableCharge.addPath(this.batteryPath); 61 | availableCharge.fillPath(); 62 | 63 | this.drawHighlight(availableCharge,this.width*usable_battery_level/100,colors); 64 | 65 | availableCharge.addPath(this.batteryPathInset); 66 | availableCharge.setStrokeColor(new Color(colors.battery.border_3d,0.7)); 67 | availableCharge.setLineWidth(2); 68 | availableCharge.strokePath(); 69 | 70 | 71 | myDrawContext.drawImageAtPoint(availableCharge.getImage(),new Point(0,0)); 72 | } 73 | 74 | if (Number(car_data.battery_level)<99){ 75 | myDrawContext.setFillColor(new Color(colors.battery.separator)); 76 | myDrawContext.fillRect(new Rect(this.width*car_data.battery_level/100,1,1,this.height-1)); 77 | if (usable_battery_level < car_data.battery_level){ 78 | myDrawContext.fillRect(new Rect(this.width*usable_battery_level/100,1,1,this.height-1)) 79 | } 80 | 81 | myDrawContext.setFillColor(new Color(colors.battery.border_3d,0.7)); 82 | myDrawContext.fillRect(new Rect(this.width*car_data.battery_level/100+1,1,3,this.height-1)) 83 | 84 | } 85 | 86 | 87 | // add a final stroke to the whole thing 88 | myDrawContext.addPath(this.batteryPath);// have to add the path again for some reason 89 | myDrawContext.setStrokeColor(new Color(colors.battery.border)); 90 | myDrawContext.setLineWidth(1); 91 | myDrawContext.strokePath(); 92 | 93 | return myDrawContext.getImage(); // return our final image 94 | 95 | } 96 | 97 | 98 | battery_bar.drawHighlight =function(contextToDrawOn,width,colors){ 99 | width = width - 10; 100 | if (width > 4){ 101 | const batteryHighlight = new Path(); 102 | batteryHighlight.addRoundedRect(new Rect(5,6,width+2,3),1,1); 103 | 104 | contextToDrawOn.setFillColor(new Color(colors.battery.highlight,0.3)); 105 | contextToDrawOn.addPath(batteryHighlight); 106 | contextToDrawOn.fillPath(); 107 | 108 | contextToDrawOn.setFillColor(new Color(colors.battery.highlight,0.3)); 109 | contextToDrawOn.fillEllipse(new Rect(7,6,width/4,4)); 110 | contextToDrawOn.fillEllipse(new Rect(6,6,width/8,6)); 111 | contextToDrawOn.fillEllipse(new Rect(5,6,width/12,8)); 112 | 113 | contextToDrawOn.fillEllipse(new Rect(width,6,5,3)) ; 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /tesla_data/parameters.js: -------------------------------------------------------------------------------- 1 | //APIurl = "YOUR_API_URL" // hardcode the API url 2 | 3 | //show_battery_percentage = true; // show the battery percentage above the battery bar 4 | //show_range = true; // show the estimated range above the battery bar 5 | //show_range_est = true; // show range estimated instead of the car's range estimate 6 | //show_data_age = false; // show how stale the data is 7 | //custom_theme = ""; // if you want to load a theme (some available themes are "3d") 8 | 9 | //debug_data = ""; // this will force the widget to pull data from iCloud json files (put sample JSON in the themes directory) 10 | 11 | //debug_size = "small"; // which size should the widget try to run as when run through Scriptable. (small, medium, large) --------------------------------------------------------------------------------