├── .env.example ├── .gitattributes ├── .gitignore ├── .gitmodules ├── Car.js ├── LICENSE ├── README.md ├── config-wizard.sh ├── config └── config.example.json ├── db.sql ├── db_mysql.sql ├── docker ├── .env.example ├── Dockerfile-app ├── Dockerfile-web ├── docker-compose.yml └── docker-start.sh ├── docs ├── beginnerguide.md ├── certbot.sh ├── config.md ├── docker.md ├── img │ ├── idView.png │ ├── widget.png │ ├── widget_and_chargingOverview.png │ ├── widgetcharging.png │ └── widgets.png ├── install.sh ├── ioswidget.md └── update.sh ├── public ├── DatabaseConnection.php ├── carPicture.php ├── carStatus.php ├── chargingSessionDataProvider.php ├── chargingSessions.php ├── env.php ├── idView │ ├── carGraphData.php │ ├── carGraphDataProvider.php │ ├── chargingOverview.html │ ├── css │ │ ├── chargingOverview.css │ │ ├── datepicker.css │ │ ├── idView.css │ │ ├── pageNavigation.css │ │ └── theme.css │ ├── idView.php │ └── js │ │ ├── AnimatedValue.js │ │ ├── DoughnutValue.js │ │ ├── SelectableList.js │ │ ├── chargingOverview.js │ │ ├── idView.js │ │ └── pageNavigation.js ├── index.php └── login │ ├── login.php │ ├── loginCheck.php │ └── logon.php ├── src ├── Autoloader.php ├── utils │ ├── ErrorUtils.php │ ├── Logger.php │ └── QueryCreationHelper.php ├── vwid │ ├── CarPictureHandler.php │ ├── CarStatusFetcher.php │ ├── CarStatusUpdateReceiver.php │ ├── CarStatusWriter.php │ ├── CarStatusWrittenUpdateReceiver.php │ ├── Main.php │ ├── Server.php │ ├── api │ │ ├── API.php │ │ ├── LoginInformation.php │ │ ├── MobileAppAPI.php │ │ ├── WebsiteAPI.php │ │ └── exception │ │ │ ├── IDAPIException.php │ │ │ ├── IDAuthorizationException.php │ │ │ └── IDLoginException.php │ ├── chargesession │ │ ├── ChargeSession.php │ │ └── ChargeSessionHandler.php │ ├── db │ │ ├── DBmigrator.php │ │ └── DatabaseConnection.php │ ├── integrations │ │ └── ABRP.php │ └── wizard │ │ ├── ConfigWizard.php │ │ ├── InteractiveWizard.php │ │ └── SetupWizard.php └── webutils │ ├── CurlError.php │ ├── CurlWrapper.php │ ├── Form.php │ └── HTTPUtils.php └── start.sh /.env.example: -------------------------------------------------------------------------------- 1 | DB_HOST="" 2 | DB_NAME="" 3 | DB_USER="" 4 | DB_PASSWORD=null 5 | DB_DRIVER="pgsql" -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.sh text eol=lf -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | vendor/ 3 | data/ 4 | config.json 5 | .env 6 | log/ -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "public/idView/ChartJS"] 2 | path = public/idView/ChartJS 3 | url = https://github.com/robske110/php-chartjs.git 4 | -------------------------------------------------------------------------------- /Car.js: -------------------------------------------------------------------------------- 1 | // Variables used by Scriptable. 2 | // These must be at the very top of the file. Do not edit. 3 | // icon-color: deep-green; icon-glyph: car; 4 | // CONFIGURATION 5 | 6 | const baseURL = "" 7 | const apiKey = "" 8 | 9 | const rangeInMiles = false //set to true to show range in miles 10 | const showFinishTime = true //set to false to hide charge finish time 11 | const forceImageRefresh = false //set to true to refresh the image 12 | 13 | const exampleData = false 14 | 15 | const timetravel = null; //set to a unix timestamp to emulate the script being run at that time (seconds!) 16 | 17 | const socThreshold = 95 //not implemented 18 | 19 | // WIDGET VERSION: v0.0.7-InDev 20 | 21 | // Created by robske_110 24.01.2020 22 | // This script is originally inspired from https://gist.github.com/mountbatt/772e4512089802a2aa2622058dd1ded7 23 | 24 | let scriptRun = new Date() 25 | if(timetravel !== null){ 26 | scriptRun = new Date(timetravel*1000); 27 | } 28 | 29 | 30 | // Translations 31 | const translations = { 32 | en: { 33 | chargeStatus: { 34 | disconnected: "Disconnected", 35 | holdingCharge: "holding charge", 36 | connected: "connected", 37 | charging: "charging…" 38 | }, 39 | soc: "SOC", 40 | range: "Range", 41 | targetSOC: "Target SOC", 42 | hvac: "HVAC", 43 | hvacStatus: { 44 | heating: "heating", 45 | cooling: "cooling", 46 | ventilation: "ventilating", 47 | off: "off" 48 | } 49 | }, 50 | de: { 51 | chargeStatus: { 52 | disconnected: "Entkoppelt", 53 | holdingCharge: "Ladezustand halten", 54 | connected: "Verbunden", 55 | charging: "Lädt…" 56 | }, 57 | soc: "Ladezustand", 58 | range: "Reichweite", 59 | targetSOC: "Zielladung", 60 | hvac: "Klimaanlage", 61 | hvacStatus: { 62 | heating: "Heizen", 63 | cooling: "Kühlen", 64 | ventilation: "Lüften", 65 | off: "Aus" 66 | } 67 | } 68 | } 69 | 70 | function getTranslatedText(key){ 71 | let lang = Device.language(); 72 | let translation = translations[lang]; 73 | if(translation == undefined){ 74 | translation = translations.en; 75 | } 76 | let nested = key.split("."); 77 | key.split(".").forEach(function(element){ 78 | translation = translation[element]; 79 | }); 80 | return translation; 81 | } 82 | 83 | let widget = await createWidget() 84 | 85 | // present the widget in app 86 | if (!config.runsInWidget) { 87 | await widget.presentMedium() 88 | } 89 | Script.setWidget(widget) 90 | Script.complete() 91 | 92 | // adds a vertical stack to widgetStack 93 | function verticalStack(widgetStack){ 94 | let stack = widgetStack.addStack() 95 | stack.layoutVertically() 96 | return stack 97 | } 98 | 99 | // adds a value - title pair 100 | function addFormattedData(widgetStack, dataTitle, dataValue){ 101 | let stack = widgetStack.addStack() 102 | stack.layoutVertically() 103 | const label = stack.addText(dataTitle) 104 | label.font = Font.mediumSystemFont(12) 105 | const value = stack.addText(dataValue) 106 | value.font = Font.boldSystemFont(16) 107 | } 108 | 109 | // build the widget 110 | async function createWidget() { 111 | let widget = new ListWidget() 112 | const data = await getData() 113 | 114 | widget.setPadding(20, 15, 20, 15) //top, leading, bottom, trailing 115 | widget.backgroundColor = Color.dynamic(new Color("eee"), new Color("111")) 116 | 117 | const wrap = widget.addStack() 118 | //wrap.centerAlignContent() 119 | wrap.spacing = 15 120 | 121 | const carColumn = verticalStack(wrap) 122 | 123 | carColumn.addSpacer(5) 124 | 125 | const carImage = await getImage( 126 | baseURL.substr(baseURL.indexOf("://")+3).replaceAll("/", "_")+"-car.png", 127 | baseURL+"/carPicture.php?key="+apiKey) 128 | carColumn.addImage(carImage) 129 | 130 | //carColumn.addSpacer(5) 131 | 132 | let chargeStatus 133 | 134 | switch (data.plugConnectionState){ 135 | case "disconnected": 136 | chargeStatus = "⚫ "+getTranslatedText("chargeStatus.disconnected") 137 | break; 138 | case "connected": 139 | //widget.refreshAfterDate = new Date(Date.now() + 300) //increase refresh rate? 140 | switch (data.chargeState){ 141 | case "chargePurposeReachedAndNotConservationCharging": 142 | case "notReadyForCharging": 143 | case "readyForCharging": 144 | chargeStatus = "🟠 "+getTranslatedText("chargeStatus.connected") 145 | break; 146 | case "chargePurposeReachedAndConservation": 147 | chargeStatus = "🟢 "+getTranslatedText("chargeStatus.holdingCharge") 148 | break; 149 | case "charging": 150 | chargeStatus = "⚡ "+getTranslatedText("chargeStatus.charging") 151 | break; 152 | default: 153 | chargeStatus = "unknown cS: "+data.chargeState 154 | } 155 | let plugLockStatus; 156 | switch (data.plugLockState){ 157 | case "locked": 158 | plugLockStatus = " (🔒)" 159 | break; 160 | case "unlocked": 161 | plugLockStatus = " (🔓)" 162 | break; 163 | case "invalid": 164 | plugLockStatus = " (❌)" 165 | break; 166 | default: 167 | plugLockStatus = "unknown pLS: "+data.plugConnectionState 168 | break; 169 | } 170 | chargeStatus = chargeStatus + plugLockStatus; 171 | break; 172 | default: 173 | chargeStatus = "unknown pCS: "+data.plugConnectionState+" cS: "+data.chargeState 174 | } 175 | 176 | //const chargeInfo = verticalStack(carColumn) 177 | //chargeInfo.setPadding(0,10,0,10) 178 | const chargeInfo = carColumn 179 | 180 | let dF = new DateFormatter() 181 | dF.useNoDateStyle() 182 | dF.useShortTimeStyle() 183 | 184 | chargeStatus = chargeInfo.addText(chargeStatus) 185 | chargeStatus.font = Font.regularSystemFont(10) 186 | chargeInfo.addSpacer(5) 187 | let dataTimestamp = null; 188 | if(!Number.isNaN(Date.parse(data.time))){ 189 | dataTimestamp = new Date(Date.parse(data.time)); 190 | } 191 | if(data.chargeState === "charging" || data.chargeState === "chargePurposeReachedAndConservation"){ 192 | let realRemainChgTime = data.remainingChargingTime; 193 | let finishTime = "" 194 | if(dataTimestamp != null){ 195 | realRemainChgTime -= (scriptRun.getTime() - dataTimestamp.getTime()) / 60000; 196 | realRemainChgTime = Math.max(0, realRemainChgTime); 197 | finishTime = " ("+dF.string(new Date(dataTimestamp.getTime() + realRemainChgTime * 60000))+")"; 198 | } 199 | let timeStr = Math.floor(realRemainChgTime / 60) + ":" + String(Math.round(realRemainChgTime % 60)).padStart(2, '0') + "h" 200 | let chargeStateLabel = chargeInfo.addText(data.chargePower + " kW | " + timeStr + (showFinishTime ? finishTime : "")) 201 | chargeStateLabel.font = Font.regularSystemFont(10) 202 | }else{ 203 | chargeInfo.addSpacer(10) 204 | } 205 | 206 | const dataCol1 = verticalStack(wrap) 207 | 208 | addFormattedData(dataCol1, getTranslatedText("soc"), data.batterySOC.toString()+"%") 209 | dataCol1.addSpacer(10) 210 | let range 211 | if(!rangeInMiles){ 212 | range = data.remainingRange+"km"; 213 | }else{ 214 | range = Math.round(data.remainingRange/1.609344)+"mi"; 215 | } 216 | addFormattedData(dataCol1, getTranslatedText("range"), range) 217 | 218 | const dataCol2 = verticalStack(wrap) 219 | 220 | addFormattedData(dataCol2, getTranslatedText("targetSOC"), data.targetSOC+"%") 221 | dataCol2.addSpacer(10) 222 | let hvacStatus; 223 | switch (data.hvacState){ 224 | case "heating": 225 | hvacStatus = getTranslatedText("hvacStatus.heating"); 226 | break; 227 | case "cooling:": 228 | hvacStatus = getTranslatedText("hvacStatus.cooling"); 229 | break; 230 | case "ventilation": 231 | hvacStatus = getTranslatedText("hvacStatus.ventilation"); 232 | break; 233 | case "off": 234 | hvacStatus = getTranslatedText("hvacStatus.off"); 235 | break; 236 | default: 237 | hvacStatus = "unknown hS: "+date.hvacState; 238 | } 239 | addFormattedData(dataCol2, getTranslatedText("hvac"), hvacStatus+" ("+data.hvacTargetTemp+"°C)") 240 | 241 | timedebug = widget.addText("carUpdate "+(dataTimestamp == null ? data.time : dF.string(dataTimestamp))+" (widget "+dF.string(scriptRun)+")") 242 | timedebug.font = Font.lightSystemFont(8) 243 | timedebug.textColor = Color.dynamic(Color.lightGray(), Color.darkGray()) 244 | timedebug.rightAlignText() 245 | return widget; 246 | } 247 | 248 | 249 | // fetch data 250 | async function getData() { 251 | let state 252 | if(exampleData || baseURL == ""){ 253 | state = {}; 254 | state["batterySOC"] = "40" 255 | state["remainingRange"] = "150" 256 | state["remainingChargingTime"] = "61" 257 | state["chargeState"] = "charging" 258 | state["chargePower"] = "100" 259 | state["targetSOC"] = "100" 260 | state["plugConnectionState"] = "connected" 261 | state["plugLockState"] = "locked" 262 | state["hvacState"] = "heating" 263 | state["hvacTargetTemp"] = "21.5" 264 | 265 | state["time"] = "simulated" 266 | }else{ 267 | state = getJSON() 268 | } 269 | 270 | /*let currentDate = ; 271 | let newDate = new Date((new Date).getTime()+1000); 272 | chargeReached = new Notification() 273 | chargeReached.identifier = "SoCReached" 274 | chargeReached.title = "ID.3 🔋 Geladen" 275 | chargeReached.body = "Die Batterie ist zu " + socThreshold + "% geladen!" 276 | chargeReached.sound = "complete" 277 | chargeReached.setTriggerDate(newDate) 278 | chargeReached.schedule()*/ 279 | 280 | return state 281 | } 282 | 283 | async function getJSON(){ 284 | url = baseURL+"/carStatus.php?key="+apiKey 285 | if(timetravel !== null){ 286 | url += "&at=@"+timetravel 287 | } 288 | req = new Request(url) 289 | req.method = "GET" 290 | apiResult = await req.loadString() 291 | console.log(apiResult) 292 | return JSON.parse(apiResult) 293 | } 294 | 295 | 296 | // get images from local filestore or download them once 297 | // credits: https://gist.github.com/marco79cgn (for example https://gist.github.com/marco79cgn/c3410c8ecc8cb0e9f87409cee7b87338#file-ffp2-masks-availability-js-L234) 298 | async function getImage(imageName, imgUrl){ 299 | let fm = FileManager.local() 300 | let dir = fm.documentsDirectory() 301 | let path = fm.joinPath(dir, imageName) 302 | if(fm.fileExists(path) && !forceImageRefresh){ 303 | return fm.readImage(path) 304 | }else{ 305 | // download once 306 | let iconImage = await loadImage(imgUrl) 307 | fm.writeImage(path, iconImage) 308 | return iconImage 309 | } 310 | } 311 | 312 | async function loadImage(imgUrl){ 313 | console.log("fetching_pic"); 314 | const req = new Request(imgUrl) 315 | return await req.loadImage() 316 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # IDDataLogger 2 | 3 | Welcome to IDDataLogger, a data logger for Volkswagen ID vehicles. 4 | Features include: 5 | - A website displaying current status, history graphs and previous charging sessions. 6 | - An [iOS widget](https://github.com/robske110/IDDataLogger/blob/master/docs/ioswidget.md) (using Scriptable) 7 | - [A Better Route Planner live data integration](https://github.com/robske110/IDDataLogger/wiki/ABRP-integration) 8 | - An easy-to-use API for integration with other systems. If you are interested see [here](https://github.com/robske110/IDDataLogger/wiki/API-reference). 9 | 10 |
11 |
12 |
13 |