iOS Widget for Home Assistant via Scriptable

Thanks, this did the trick!

1 Like

Really nice work!
Is it possible to get other sensors in the same widget?
From different template sensors i have states like: On/Off, Open/Closed, Electricity Price.

Your imagination is the Limit and totally in my case my programming skills - buut its possible

Nice one Flipso!
Haha same here… my javascript skills is zero :sweat_smile:
Can you share your code? I have no problem reading the code and edit, add, remove for my own sensors.

Same here - so till Line 16 everything is like in the original and only changed a bit of the bottom line - but its nearly identical with OPs Code up there - so no real change here only switched naming and sensors


let widget = await createWidget();
if (!config.runsInWidget) {
    await widget.presentSmall();
}

Script.setWidget(widget);
Script.complete();

async function createWidget(items) {



    let req = new Request("https://YOURSERVER/api/states")
    req.headers = { "Authorization": "Bearer YOURTOKEN", "content-type": "application/json" }
    let json = await req.loadJSON();

    /* Parse data received from API */
    let data = {outdoor: {}, child: {}, office: {}, buro:{}}

    data.outdoor = addData(json, data.outdoor, ['binary_sensor.samuel_bett', 'sensor.ble_humidity_aabbccddeeff']);
    data.child = addData(json, data.child, ['sensor.bett_probability', 'sensor.ble_humidity_aabbccddeeff']);
    data.office = addData(json, data.office, ['group.lichter', 'sensor.ble_humidity_aabbccddeeff']);
    data.buro = addData(json, data.buro, ['input_boolean.timer', 'sensor.ble_humidity_aabbccddeeff']);
    /* Create the widget */
    const widget = new ListWidget();
    widget.backgroundColor = new Color("#03a9f4", 1.0);

    /* Design the widget header */
    let headerStack = widget.addStack();

    headerStack.addSpacer(2);
    const titleStack = headerStack.addStack();
    headerStack.addSpacer(7);
    const tempImageStack = headerStack.addStack();
    headerStack.addSpacer(14);




    /* Add the name of this Home Assistant */
    const titleLabel = titleStack.addText("Sensoren");
    titleStack.setPadding(2, 0, 0, 0);
    titleLabel.font = Font.heavyMonospacedSystemFont(12);
    titleLabel.textColor = Color.black();


    widget.addSpacer(5)

    /* Add the sensor entries */
    const bodyStack = widget.addStack();

    /* First, the label column */
    const labelStack = bodyStack.addStack();
    labelStack.setPadding(0, 0, 0, 0);
    labelStack.borderWidth = 0;
    labelStack.layoutVertically();

    addLabel(labelStack, "State:")
    addLabel(labelStack, "Probability:")
    addLabel(labelStack, "Lichter:")
    addLabel(labelStack, "Test:")

    /* Second, the temperature column */
    const tempStack = bodyStack.addStack();
    tempStack.setPadding(0, 5, 0, 0);
    tempStack.borderWidth = 0;
    tempStack.layoutVertically();

    addTemp(tempStack, data.outdoor)
    addTemp(tempStack, data.child)
    addTemp(tempStack, data.office)
    addTemp(tempStack, data.buro)
    
    /* Done: Widget is now ready to be displayed */
    return widget;
}

/* Adds the entries to the label column */
async function addLabel(labelStack, label) {
    const mytext = labelStack.addText(label);
    mytext.font = Font.semiboldSystemFont(10);
    mytext.textColor = Color.black();
}

/* Adds the entries to the temperature column */
async function addTemp(tempStack, data) {
    const mytext = tempStack.addText(data.temp);
    mytext.font = Font.heavyMonospacedSystemFont(10);
    mytext.textColor = Color.white();
}

/*
The following function is "borrowed" from:
https://gist.github.com/marco79cgn/23ce08fd8711ee893a3be12d4543f2d2
Retrieves the image from the local file store or downloads it once
*/


/* Searches for the respective sensor values ('state') in the API response of Home Assistant */
function addData(json, room, sensors) {
    room.temp = "N/A";
    var i;
    for (i = 0; i < json.length; i++) {
        if (json[i]['entity_id'] == sensors[0]) {
            room.temp = (json[i]['state']);
        }

    }
    return room;
}

Many thanks! :pray:

Hi, does somebody knows how to read value of the ipad barometer sensor using scriptable?

This is simple, you can query arbitrary sensors. On your computer (I use a Mac) you can query it (for example) using curl in Terminal.app.

curl --insecure -H 'Authorization: Bearer *--{Your HASS Long-Lived Access Token here}--*' -H 'content-type: application/json' https://*--{HASS IP}--*/api/states -o states.json

You can beautify the JSON output using an online service like: https://jsonformatter.curiousconcept.com

You can then explore the output and search for your sensor of choice.

In the widget you need to edit line 23 to 31 and later (maybe) also line 203 and 206 depending on whether you want to return an integer, float, or string.

1 Like

This is amazing! Thank you!
I’ve been using your example to start building out my main hass dashboard into a native iOS view.


The cars card looks nice, and the small camera preview works well. Next up is to format the larger image to show more rich data.

Thanks again for getting me started on this! Amazing.

2 Likes

Would you mind sharing your script for your camera view there? Looks pretty amazing!

It’s a bit simple and crude to be honest - I have various automations that take regular snapshots and store them locally. So the widget just grabs the locally saved snapshot and uses it for a background image on the widget.


let img = new Request (url + "/local/snapshot_front.jpg")

    let image = await img.loadImage()

    widget.backgroundImage = image

1 Like

EDIT: This worked for (json[i]['attributes']['attributename']);

Can I somehow get attribute values to show as well?

When I paste the code into scriptable, I get:
Error on line 28: SyntaxError: Cannot declare a const variable twice: ‘widget’

I got some errors about line 21 but changing it to what it is now fixed those. (colons to semicolons and remove commas)

let widget = await createWidget();
if (!config.runsInWidget) {
    await widget.presentSmall();
}

Script.setWidget(widget);
Script.complete();

async function createWidget(items) {

    /* Get data from API */
    const tempImg = await getImage('temperature.png');
    const humidImg = await getImage('humidity.png');
    const logoImg = await getImage('hass-favicon.png');

    let req = new Request("https://192.168.1.3/api/states")
    req.headers = { "Authorization": "Bearer my token", "content-type": "application/json" }
    let json = await req.loadJSON();

    /* Parse data received from API */
    let data = office; {} kitchen; {} freezer; {}}

    data.office = addData(json, data.office, ['sensor.ble_temperature_upstairst_and_h', 'sensor.ble_humidity_upstairst_and_h']);
    data.kitchen = addData(json, data.kitchen, ['sensor.ble_temperature_downstairst_and_h', 'sensor.ble_humidity_downstairst_and_h']);
    data.freezer = addData(json, data.freezer, ['sensor.freezer_average', 'sensor.freezer_difference']);

    /* Create the widget */
    const widget = new ListWidget();
    widget.backgroundColor = new Color("#03a9f4", 1.0);

    /* Design the widget header */
    let headerStack = widget.addStack();
    const logoStack = headerStack.addStack();
    headerStack.addSpacer(2);
    const titleStack = headerStack.addStack();
    headerStack.addSpacer(7);
    const tempImageStack = headerStack.addStack();
    headerStack.addSpacer(14);
    const humidImageStack = headerStack.addStack();

    /* Add a logo icon */
    logoStack.backgroundColor = new Color("#03a9f4", 1.0)
    logoStack.cornerRadius = 1
    const wimgLogo = logoStack.addImage(logoImg)
    wimgLogo.imageSize = new Size(20, 20)
    wimgLogo.rightAlignImage()

    /* Add the name of this Home Assistant */
    const titleLabel = titleStack.addText("Booker St");
    titleStack.setPadding(2, 0, 0, 0);
    titleLabel.font = Font.heavyMonospacedSystemFont(12);
    titleLabel.textColor = Color.black();

    /* Add a temperature icon */
    tempImageStack.backgroundColor = new Color("#03a9f4", 1.0)
    tempImageStack.cornerRadius = 1
    const wimgTemp = tempImageStack.addImage(tempImg)
    wimgTemp.imageSize = new Size(20, 20)
    wimgTemp.rightAlignImage()

    /* Add a humid icon */
    humidImageStack.backgroundColor = new Color("#03a9f4", 1.0)
    humidImageStack.cornerRadius = 1
    const wimgHumid = humidImageStack.addImage(humidImg)
    wimgHumid.imageSize = new Size(20, 20)
    wimgHumid.rightAlignImage()

    widget.addSpacer(5)

    /* Add the sensor entries */
    const bodyStack = widget.addStack();

    /* First, the label column */
    const labelStack = bodyStack.addStack();
    labelStack.setPadding(0, 0, 0, 0);
    labelStack.borderWidth = 0;
    labelStack.layoutVertically();

    addLabel(labelStack, "        Office:")
    addLabel(labelStack, "     Kitchen:")
    addLabel(labelStack, "    Freezer:")

    /* Second, the temperature column */
    const tempStack = bodyStack.addStack();
    tempStack.setPadding(0, 3, 0, 0);
    tempStack.borderWidth = 0;
    tempStack.layoutVertically();

    addTemp(tempStack, data.office)
    addTemp(tempStack, data.kitchen)
    addTemp(tempStack, data.freezer)

    /* Third, the humidity column */
    const humidStack = bodyStack.addStack();
    humidStack.setPadding(0, 5, 0, 0);
    humidStack.borderWidth = 0;
    humidStack.layoutVertically();

    addHumid(humidStack, data.office)
    addHumid(humidStack, data.kitchen)
    addHumid(humidStack, data.freezer)

    /* Done: Widget is now ready to be displayed */
    return widget;
}

/* Adds the entries to the label column */
async function addLabel(labelStack, label) {
    const mytext = labelStack.addText(label);
    mytext.font = Font.semiboldSystemFont(10);
    mytext.textColor = Color.black();
}

/* Adds the entries to the temperature column */
async function addTemp(tempStack, data) {
    const mytext = tempStack.addText(data.temp + "°F");
    mytext.font = Font.heavyMonospacedSystemFont(10);
    mytext.textColor = Color.white();
}

/* Adds the entries to the humidity column */
async function addHumid(humidStack, data) {
    const mytext = humidStack.addText("(" + data.humid + "%)");
    mytext.font = Font.mediumMonospacedSystemFont(10);
    mytext.textColor = Color.white();
    mytext.textOpacity = 0.8;
}

/*
The following function is "borrowed" from:
https://gist.github.com/marco79cgn/23ce08fd8711ee893a3be12d4543f2d2
Retrieves the image from the local file store or downloads it once
*/
async function getImage(image) {
    let fm = FileManager.local()
    let dir = fm.documentsDirectory()
    let path = fm.joinPath(dir, image)
    if (fm.fileExists(path)) {
        return fm.readImage(path)
    } else {
        // download once
        let imageUrl
        switch (image) {
            case 'temperature.png':
                imageUrl = "https://uploads-ssl.webflow.com/5c504ac89bf84cb501f4750a/5f3fa61aba10b4edb6d060f0_temperature.png"
                break
            case 'humidity.png':
                imageUrl = "http://bemakoha.com.ar/assets/humedad.faf5dde2fdcb6366ead6b3415aca8471.png"
                break
            case 'hass-favicon.png':
                imageUrl = "https://192.168.1.3/static/icons/favicon-192x192.png"
                break
            default:
                console.log(`Sorry, couldn't find ${image}.`);
        }
        let iconImage = await loadImage(imageUrl)
        fm.writeImage(path, iconImage)
        return iconImage
    }
}

/*
The following function is "borrowed" from:
https://gist.github.com/marco79cgn/23ce08fd8711ee893a3be12d4543f2d2
Downloads an image from a given URL
*/
async function loadImage(imgUrl) {
    const req = new Request(imgUrl)
    return await req.loadImage()
}

/* Searches for the respective sensor values ('state') in the API response of Home Assistant */
function addData(json, room, sensors) {
    room.temp = "N/A";
    room.humid = "N/A";
    var i;
    for (i = 0; i < json.length; i++) {
        if (json[i]['entity_id'] == sensors[0]) {
            room.temp = Math.round(json[i]['state']);
        }
        if (json[i]['entity_id'] == sensors[1]) {
            room.humid = Math.round(json[i]['state']);
        }
    }
    return room;
}```

The code runs without editing the things you mentioned. I’m guessing you tried to copy the code from the HTML version of GitHub, use the RAW version instead.

Thank you that got it working. DO you have any idea of how to get the data through nabu casa instead of locally?

According to: Remote UI

You can use your “https://{replaceme}.ui.nabu.casa/api/states” as URL.

let req = new Request("https://<replaceme>.ui.nabu.casa/api/states")

Thanks! Got it all working

1 Like

Hey there,
Does anyone knows a way to set a server in the widget.url = „homeassistant://navigate/…..“ with URI? Or does anyone know a way to interact with homeassistant by clicking on the widget?

Im trying to get to an overview page for my fuel prices out of the widget:

Greetings to all of you:)

Wow, great script! Is there a way to keep the values updating every few seconds?

While in theory possible, see this link, it would drain your battery and is not recommended by the author of the Scriptable app.

Scriptable API Doc:

const nextRefresh = Date.now() + (1000 * 30); // add 30 second to now
widget.refreshAfterDate = new Date(nextRefresh);

Before you refresh the Widget too often, double check that Home Assistant’s /api/states also updates it’s values fast enough.