iOS 14 Homescreen Widget für Reichweite , SoC etc zum Basteln - auf eigene Gefahr!

  • Also ich finde nichts. Hab alles de auf com geändert aber tut sich nichts. Hab alles neu installiert und trotzdem nicht. Schade

    Komisch, eigtl. sollte es dann wieder gehen. Nachdem du das Widget rausgeschmissen und wieder hinzugefügt hast, hast du dann unter "Widget bearbeiten" auch wieder die vollständige VIN als Parameter mitgegeben?

    Mensch, 1000 Dank für den Tip!!! :0008: IMG_5271.jpg

    Gerne :)

  • Ja habe ich. Allerdings musste ich kein Connected Passwort eingeben. Es kommt der Wartekringel und Fehler No Vin.

  • Ja :)
    strings = {}
    strings['de'] = {
    'What to do ?':'Aktion wählen',
    'Start cabin heating':'Klimatisierung starten',
    'Flash lights':'Lichthupe',
    'Make some noise':'Hupe',
    'Lock the doors':'Türen verriegeln',
    'Nothing, thank you.':'Keine Aktion',
    'head the cabin':'den Innenraum heizen',
    'flash the lights':'die Lichter anmachen',
    'blow the horn':'die Hupe betätigen',
    'lock the doors':'die Türen verriegeln',
    'You want to ':'Du möchtest ',
    'Just to make it clear':'Nur um sicherzugehen',
    'Yes dude !':'Ja',
    'Noooooooo !':'Nein',
    'Its done':'Erledigt',
    'Your request to ':'Deine Anforderung für ',
    ' was sent.':' wurde gesendet.',
    'Thank you !':'Dankeschön',
    'Unable to login':'Login nicht möglich',
    'You want to enter your credentials again ?':'Möchtest Du Deine Zugangsdaten noch mal eintragen?',
    'Yes':'Ja',
    'No, they are fine':'Nein, die sind ok',
    'Save':'Speichern'
    }


    const userKey = Script.name()+'_cd_user'
    const passKey = Script.name()+'_cd_pass'
    const vinKey = Script.name()+'_vin'


    if ((Keychain.contains(userKey)) && Keychain.contains(passKey)) {


    let carData = await accumulateData()
    const mainWdgData = await createWidget(carData) // build the normal widget login get the token vin and data
    const widget = mainWdgData
    const token = carData.token // we need the token and vin for later actions
    const vin = carData.vin


    if (config.runsInWidget) {
    Script.setWidget(widget)
    } else {
    widget.presentMedium()
    let useralert = new Alert()
    useralert.title = localize('What to do ?')
    useralert.addAction(localize('Start cabin heating'))
    useralert.addAction(localize('Flash lights'))
    useralert.addAction(localize('Make some noise'))
    useralert.addAction(localize('Lock the doors'))
    useralert.addCancelAction(localize('Nothing, thank you.'))
    let action = await useralert.present()
    switch (action) {
    case 0:
    proceedAction('head the cabin','RCN',token,vin)
    break
    case 1:
    proceedAction('flash the lights','RLF',token,vin)
    break
    case 2:
    proceedAction('blow the horn','RHB',token,vin)
    break
    case 3:
    proceedAction('lock the doors','RDL',token,vin)
    break
    }
    }
    Script.complete()
    } else {
    await askForUsername()
    await askForPassword()
    }


    function getPostBody(input) {
    let result = ''
    Object.keys(input).map((key)=>{
    result = result + '&' + key + '=' + encodeURIComponent(input[key])
    })
    return result
    }


    function localize(text) {
    let lng = Device.language()
    if ((strings[lng]) && (strings[lng][text])) {
    return strings[lng][text]
    } else {
    return text
    }
    }


    async function accumulateData() {
    let result = {}
    let vin
    let token = await getLoginToken()
    if (token) {
    result.token = token
    vin = getVinbyParameterOrKeyChain()
    if (vin === false) {
    vin = await getVin(token)
    }
    if (vin) {
    result.vin = vin
    result.carImage = await fetchImage(vin)
    let data = await getVehicleStatus(token,vin)
    let dataSOC = await getVehicleStatusSOC(token,vin)
    if (data) {
    result.charging_status = data.charging_status
    result.chargingSystemStatus = data.chargingSystemStatus
    result.chargingTimeRemaining = data.chargingTimeRemaining
    result.chargingLevelHv = data.chargingLevelHv
    result.soc_hv_percent = data.soc_hv_percent
    result.socmax = dataSOC.socmax
    result.soc = dataSOC.soc
    result.beRemainingRangeElectricKm = data.beRemainingRangeElectricKm
    result.door_lock_state = data.door_lock_state
    result.updateTime = data.updateTime
    result.lat = data.gps_lat
    result.lon = data.gps_lng
    let nowDate = new Date()
    let chg = await getlastCharge(token,vin,nowDate)
    if (chg.latest === undefined) {
    // maybe we have a new month and the uses hasnt chargd yet so go one month back
    nowDate.setDate(1)
    let month = nowDate.getMonth()
    if (month === 0) { // Special in january go also one year back
    nowDate.setMonth(11) //
    nowDate.setFullYear(nowDate.getFullYear()-1)
    } else {
    nowDate.setMonth(nowDate.getMonth() - 1)
    }
    chg = await getlastCharge(token,vin,nowDate)
    }
    result.lastCharge = chg.latest


    } else {
    result.error = 'unable to fetch data'
    }
    } else {
    result.error = 'unable to get your vin'
    }
    } else {
    result.loginError = true
    result.error = 'unable to login'
    }
    return result
    }


    async function createWidget(data) {
    let bgColor = '#FFFFFF'
    let fgColor = '#000000'


    // if(Device.isUsingDarkAppearance()) {
    bgColor = '#000000'
    fgColor = '#FFFFFF'
    // }
    let chrglblCol = new Color(fgColor)
    let widget = new ListWidget()
    let canvas
    widget.backgroundColor = new Color(bgColor, 1.0)
    if (data.error === undefined) {
    let carIconCell
    let wideMode = false
    if ((config.widgetFamily === 'medium') || (config.widgetFamily === 'large') || (config.runsInWidget === false)) {
    let row = widget.addStack()
    row.layoutHorizontally()
    canvas = row.addStack()
    canvas.layoutVertically()
    row.addSpacer(8)
    carIconCell = row.addStack()
    wideMode = true
    } else {
    canvas = widget
    }
    // Battery stack
    let batteryStack = canvas.addStack()
    // Battery icon
    let battIcon = SFSymbol.named('bolt.fill.batteryblock');
    let battIconElement = batteryStack.addImage(battIcon.image)
    battIconElement.imageSize = new Size(15, 15)
    batteryStack.addSpacer(8)


    // Set color based on charging state
    if (data.charging_status == 'NOCHARGING') {
    battIconElement.tintColor = new Color(fgColor)
    } else {
    chrglblCol = Color.blue()
    battIconElement.tintColor = chrglblCol
    }
    let batteryText = batteryStack.addText(Math.floor(data.chargingLevelHv) + '% - ' + (Math.floor(data.soc*100)/100) + ' kWh')
    batteryText.textColor = chrglblCol
    batteryText.font = Font.systemFont(12)
    canvas.addSpacer()



    // Range stack
    let rangeStack = canvas.addStack()
    let rangeIcon = SFSymbol.named('gauge');
    let rangeIconElement = rangeStack.addImage(rangeIcon.image)
    rangeIconElement.imageSize = new Size(15, 15)
    rangeIconElement.tintColor = new Color(fgColor)
    rangeStack.addSpacer(8)
    let rangeText = rangeStack.addText(Math.floor(data.beRemainingRangeElectricKm) +'km')
    rangeText.textColor = new Color(fgColor)
    rangeText.font = Font.systemFont(12)
    canvas.addSpacer()



    // MAXSOC stack
    let maxsocStack = canvas.addStack()
    let maxsocIcon = SFSymbol.named('bolt.fill.batteryblock');
    let maxsocIconElement = maxsocStack.addImage(maxsocIcon.image)
    maxsocIconElement.imageSize = new Size(15, 15)
    maxsocIconElement.tintColor = new Color(fgColor)
    maxsocStack.addSpacer(8)
    let maxsocText = maxsocStack.addText('max ' + (Math.floor(data.socmax *100)/100) +' kWh')
    maxsocText.textColor = new Color(fgColor)
    maxsocText.font = Font.systemFont(12)
    canvas.addSpacer()


    // in wide Mode we will add the iamge on the right side
    let carIconStack
    let imgSize
    let paddingBottom = -25
    let paddingTrailing = 0
    if (wideMode===true) {
    imgSize = new Size(150, 150)
    paddingBottom = -20
    paddingTrailing = -20
    carIconStack = carIconCell.addStack()
    } else {
    imgSize = new Size(100, 100)
    carIconStack = canvas.addStack()
    }



    const carImageStack = carIconStack.addStack()
    carIconStack.layoutHorizontally()
    carImageStack.backgroundColor = new Color(bgColor, 1.0)
    carImageStack.cornerRadius = 8
    const wimg = carIconStack.addImage(data.carImage)
    wimg.imageSize = imgSize
    wimg.rightAlignImage()
    wimg.url = 'https://maps.apple.com/?q=i3&ll='+data.lat+','+data.lon
    carIconStack.setPadding(-40,0,paddingBottom,paddingTrailing)
    canvas.addSpacer()



    // Lock State Stack
    let lockStack = canvas.addStack()
    let lockIcon
    if (data.door_lock_state === 'SECURED') {
    lockIcon = SFSymbol.named('lock.circle');
    } else {
    lockIcon = SFSymbol.named('lock.open')
    }
    let lockIconElement = lockStack.addImage(lockIcon.image)
    lockIconElement.imageSize = new Size(15, 15)
    lockIconElement.tintColor = new Color(fgColor)
    lockStack.addSpacer(8)
    let lockText = lockStack.addText(data.door_lock_state)
    lockText.textColor = new Color(fgColor)
    lockText.font = Font.systemFont(12)
    canvas.addSpacer()



    // add the charging data if we are running in a wider mode
    if (wideMode === true) {
    if (data.lastCharge !== undefined) {
    let chargeStack = widget.addStack()
    let lchargeIcon = SFSymbol.named('bolt.car');
    let lchargeIconElement = chargeStack.addImage(lchargeIcon.image)
    lchargeIconElement.imageSize = new Size(15, 15)
    lchargeIconElement.tintColor = new Color(fgColor)
    chargeStack.addSpacer(8)


    let chargeText = chargeStack.addText(data.lastCharge)
    chargeText.font = Font.systemFont(12)
    chargeText.textColor = new Color(fgColor)
    widget.addSpacer()
    }
    }


    // update stack
    let updateStack = widget.addStack()
    let chargingActive = (data.chargingSystemStatus === 'CHARGINGACTIVE')
    let timeIcon = SFSymbol.named((chargingActive === true) ? 'timer' : 'clock');
    let timeIconElement = updateStack.addImage(timeIcon.image)
    timeIconElement.imageSize = new Size(15, 15)
    timeIconElement.tintColor = chrglblCol
    updateStack.addSpacer(8)



    // Use the utc and convert to local time
    let df = new DateFormatter()
    let date


    if (chargingActive === true) {
    let now = new Date() // calculate the charging end time based on the remaining minutes
    date = new Date(now.getTime()+parseInt(data.chargingTimeRemaining)*60000)
    } else {
    df.dateFormat = 'dd.MM.yyyy HH:mm:ss Z'
    date = df.date(data.updateTime)
    }


    df.useShortDateStyle()
    df.useShortTimeStyle()


    let updateText = updateStack.addText(df.string(date))


    updateText.textColor = chrglblCol // make it blue when the the car is charging


    if (wideMode===false) { // in smallmode make the text even smaller
    updateText.font = Font.systemFont(11)
    } else {
    updateText.font = Font.systemFont(12)
    }
    widget.addSpacer()



    } else {
    canvas = widget
    canvas.addText(data.error)
    if ((data.loginError === true) && (config.runsInWidget === false)) {
    showReloginAlert()
    }
    }
    return widget
    }


    async function proceedAction(question,actionType,token,vin) {
    let proceedAlert = new Alert()
    proceedAlert.title = localize('Just to make it clear')
    proceedAlert.message = localize('You want to ') + localize(question) + '?'
    proceedAlert.addAction(localize('Yes dude !'))
    proceedAlert.addCancelAction(localize('Noooooooo !'))
    let action = await proceedAlert.present()
    if (action === 0) {
    await performRemoteAction(actionType,token,vin)
    proceedAlert = new Alert()
    proceedAlert.title = localize('Its done')
    proceedAlert.message = localize('Your request to ') + localize(question) + localize(' was sent.')
    proceedAlert.addAction(localize('Thank you !'))
    await proceedAlert.present()
    }
    }


    async function showReloginAlert() {
    let useralert = new Alert()
    useralert.title = localize('Unable to login')
    useralert.message = localize('You want to enter your credentials again ?')
    useralert.addAction(localize('Yes'))
    useralert.addCancelAction(localize('No, they are fine'))
    let action = await useralert.present()
    if (action === 0) {
    await askForUsername()
    await askForPassword()
    }
    }


    async function askForUsername() {
    let useralert = new Alert()
    useralert.title = 'Connected Drive'
    let cduser = useralert.addTextField('Username')
    useralert.addAction(localize('Save'))
    await useralert.present()
    Keychain.set(userKey,useralert.textFieldValue(0))
    }


    async function askForPassword() {
    let useralert = new Alert()
    useralert.title = 'Connected Drive'
    let cduser = useralert.addSecureTextField('Password')
    useralert.addAction(localize('Save'))
    await useralert.present()
    Keychain.set(passKey,useralert.textFieldValue(0))
    }


    function getVehicleImage(vin) {
    return new Promise(async (resolve,reject)=>{
    let vehicleImageListUrl = 'https://www.bmw-connecteddrive.com/api/vehicle/image/v1/' + vin + '?startAngle=0&stepAngle=10&width=780'
    let vehicleImageListRequest = new Request(vehicleImageListUrl)
    vehicleImageListRequest.method = 'get'
    vehicleImageListRequest.headers = {'Content-Type': 'application/json'}
    let data = await vehicleImageListRequest.loadJSON()
    if ((data) && (data.angleUrls)) {
    resolve(data.angleUrls[5].url)
    } else {
    resolve(false)
    }
    })
    }



    function performRemoteAction(action,token,vin) {
    return new Promise(async(resolve,reject)=>{
    let remoteCommandUrl = 'https://www.bmw-connecteddrive.com/remoteservices/rsapi/v1/' + vin + '/' + action


    let remoteRequest = new Request(remoteCommandUrl)
    remoteRequest.body = '{}'
    remoteRequest.method = 'POST'
    remoteRequest.headers = {
    'Authorization': 'Bearer ' + token,
    'Content-Type': 'application/json'
    }
    remoteRequest.loadJSON().then((data)=>{
    resolve()})
    .catch((e)=>{
    console.error(e)
    resolve()})


    })
    }


    function getVehicleStatus(token,vin) {
    return new Promise(async(resolve,reject)=>{
    let vehicleDataUrl = 'https://www.bmw-connecteddrive.com/api/vehicle/dynamic/v1/' + vin + '?offset=-60'
    let vehicleDataRequest = new Request(vehicleDataUrl)
    vehicleDataRequest.method = 'get'
    vehicleDataRequest.headers = {
    'Authorization': 'Bearer ' + token,
    'Content-Type': 'application/json'
    }
    let data = await vehicleDataRequest.loadJSON()
    if (data) {
    resolve(data.attributesMap)
    } else {
    resolve(false)
    }
    })
    }


    function getVehicleStatusSOC(token,vin) {
    return new Promise(async(resolve,reject)=>{
    let vehicleDataUrl = 'https://www.bmw-connecteddrive.com/api/vehicle/navigation/v1/' + vin + ''
    let vehicleDataRequest = new Request(vehicleDataUrl)
    vehicleDataRequest.method = 'get'
    vehicleDataRequest.headers = {
    'Authorization': 'Bearer ' + token,
    'Content-Type': 'application/json'
    }
    let data = await vehicleDataRequest.loadJSON()
    if (data) {
    resolve(data)
    } else {
    resolve(false)
    }
    })
    }


    function getVinbyParameterOrKeyChain() {
    let parameters = args.widgetParameter
    if (parameters != null && parameters.length > 0) {
    console.log('using vin ' + parameters + ' from widget parameters')
    return parameters
    } else {
    // try the keychain
    if (!Keychain.contains(vinKey)) {
    console.log('there is no stored vin')
    return false
    } else {
    let vin = Keychain.get(vinKey)
    console.log('using vin ' + vin +' from keychain')
    return vin
    }
    }
    }


    function getVin(token) {
    return new Promise(async(resolve,reject)=>{
    console.log('using vin from cd')
    let vehicleListUrl = 'https://www.bmw-connecteddrive.com/api/me/vehicles/v2?all=true&brand=BM'
    let vehicleListRequest = new Request(vehicleListUrl)
    vehicleListRequest.method = 'get'
    vehicleListRequest.headers = {
    'Authorization': 'Bearer ' + token,
    'Content-Type': 'application/json'
    }
    let list = await vehicleListRequest.loadJSON()
    if (list.length > 0) {
    let vin = list[0].vin
    console.log(list[0])
    Keychain.set(vinKey,vin)
    resolve(vin)
    } else {
    console.log(list[0])
    resolve(false)
    }
    })
    }


    function getlastCharge(token,vin,date) {
    let result = {}
    result.latest = undefined
    return new Promise( async(resolve,reject)=>{
    let df = new DateFormatter()
    df.dateFormat = 'yyyy-MM'
    let thisMonth = df.string(date) + '-01T00:00:00.000'
    let chargeSessionUrl = 'https://cocoapi.bmwgroup.com/eadrax-chs/v1/charging-sessions?vin=' + vin + '&date=' + thisMonth
    let vehicleChargeRequest = new Request(chargeSessionUrl)
    vehicleChargeRequest.method = 'get'
    vehicleChargeRequest.headers = {
    'Authorization': 'Bearer ' + token,
    'Content-Type': 'application/json'
    }
    let list = await vehicleChargeRequest.loadJSON()
    if (list.chargingSessions !== undefined) {
    result.nrj = list.chargingSessions.total


    if ((list.chargingSessions.sessions) && (list.chargingSessions.sessions.length > 0)) {
    let latest = list.chargingSessions.sessions[0]
    result.latest = latest.title + '|' + latest.energyCharged
    }
    }
    resolve(result)
    })
    }


    async function getLoginToken() {
    let user
    let pwd
    try {
    if (!Keychain.contains(userKey)) {
    console.error('missing parameters')
    } else {
    user = Keychain.get(userKey)
    pwd = Keychain.get(passKey)
    }
    let url = 'https://customer.bmwgroup.com/gcdm/oauth/authenticate'
    let post_data = {
    'state': 'eyJtYXJrZXQiOiJkZSIsImxhbmd1YWdlIjoiZGUiLCJkZXN0aW5hdGlvbiI6ImxhbmRpbmdQYWdlIn0',
    'username': user,
    'client_id': 'dbf0a542-ebd1-4ff0-a9a7-55172fbfce35',
    'password': pwd,
    'redirect_uri': 'https://www.bmw-connecteddrive.com/app/default/static/external-dispatch.html',
    'response_type': 'token',
    'scope': 'authenticate_user fupo',
    'locale': 'DE-de'
    }
    let lRequest = new Request(url)
    lRequest.method = 'POST'
    lRequest.body = getPostBody(post_data)
    let result = await lRequest.load()
    let tokenUrl = lRequest.response.url
    if (tokenUrl) {
    let match = tokenUrl.match(/&access_token=([a-zA-z0-9]{0,})/)
    if (match != null) {
    let token = match[1]
    return token
    }
    }


    } catch(e) {
    console.error(e)
    }
    return null
    }



    function fetchImage(vin) {
    return new Promise (async(resolve,reject)=>{
    let fm = FileManager.local()
    let dir = fm.documentsDirectory()
    let path = fm.joinPath(dir, vin + '.png')
    if (fm.fileExists(path)) {
    resolve(fm.readImage(path))
    } else {
    let carImageUrl = await getVehicleImage(vin)
    let carImageRequest = new Request(carImageUrl)
    let carImage = await carImageRequest.loadImage()
    fm.writeImage(path, carImage)
    resolve(carImage)
    }
    })


    }

  • Ich hab jetzt das Kopiert und bei mir nachdem ich alles gelöscht habe eingesetzt. Widged funktioniert nicht und bei Script kommt der Fehler no Vin/ Vin from Cd?


    gruss

    Einmal editiert, zuletzt von Clubbi ()

  • Ich hab jetzt das Kopiert und bei mir nachdem ich alles gelöscht habe eingesetzt. Widged funktioniert nicht und bei Script kommt der Fehler no Vin/ Vin from Cd?


    gruss

    Also wenn du das Script in Scriptable direkt ausführst ist der Fehler normal, weil er da die VIN noch gar nicht hat. Die holt er sich erst, wenn du sie beim Widget als Parameter eingegeben hast.

  • So, hab's tatsächlich zum laufen gebracht, hätt' ich mir gar ned zugetraut, aber:


    Ist es so richtig/gewollt, daß beim antippen des Widgets die App "Scriptable" aufgeht, mir kurz die Scripts angezeigt werden, dann das Widget nochmal in einem eigenen Fenster, welches sogleich von einem Auswahlmenü "Aktion wählen" überlagert wird, aufgeht ?(


    Diese Auswahlmenü muß ich dann "mit "keine Aktion" wegtippen, anschl. das Widget über den "close"-Button schließen und zum Schluß noch die "Scriptable"-App wegwischen - seeeehr umständlich das ganze ?(

  • Bei mir IOS wird beim antippen des Widged Scriptable geöffnet und der wartekringel dreht sich Stunden lang, nichts passiert. Bekomme es nicht zum laufen.

  • So, hab's tatsächlich zum laufen gebracht, hätt' ich mir gar ned zugetraut, aber:


    Ist es so richtig/gewollt, daß beim antippen des Widgets die App "Scriptable" aufgeht, mir kurz die Scripts angezeigt werden, dann das Widget nochmal in einem eigenen Fenster, welches sogleich von einem Auswahlmenü "Aktion wählen" überlagert wird, aufgeht ?(


    Diese Auswahlmenü muß ich dann "mit "keine Aktion" wegtippen, anschl. das Widget über den "close"-Button schließen und zum Schluß noch die "Scriptable"-App wegwischen - seeeehr umständlich das ganze ?(

    Denke schon, zumindest ist es bei mir auch so. Außer du klickst auf das Fahrzeug, dann springt er zu Scriptable und dann zu Karten und zeigt dir den Standort vom Fahrzeug an.


    Aber wieso umständlich? Das Widget ist hauptsächlich dazu da, schnell mit einem Wisch auf dem Homescreen die wichtigsten Daten zu sehen. Wieso also überhaupt drauf drücken?

    Bei mir IOS wird beim antippen des Widged Scriptable geöffnet und der wartekringel dreht sich Stunden lang, nichts passiert. Bekomme es nicht zum laufen.

    Also das Skript von oben, den richtigen Connected-Zugangsdaten, als Widget hinzugefügt und dort unter den Parametern die vollständige VIN (nicht nur die letzten 7 Ziffern) eingegeben und es geht noch immer nicht? Dann bin ich mit meinem Latein um ehrlich zu sein auch am Ende :(