[APP][Pro] Dashboard Studio - A completely free-form dashboard designer

Oh, but those are (svg) images you have tinted. Not the build-in phosphor icons Only the color of the build icons can be set using the editor.

Yes , I use external svg library with over 7400 unique icons. Downside: not nicely integrated as Phosphor icons. The Phosphor library contains over 1500 unique icon images.

@Bert_Onraedt could use this external library and define his own color. The color could be variable if Homey script is used.

Thanks for the explanation.
I hope someone is paying you for professional programming:-)

No need to put it to the roadmap. Just wanted to make sure I did not oversee functionality…

Thank you, it’s really a great tool!!

In the meantime I optimized the weather forecast to reduce api calls and homey load. I used homeyscript directly and set two tags to feed dashboard studio. This is better readable and editable.


// --------------------------------------------------
// Configuration
// --------------------------------------------------
const LOCATION = "Heidenheim an der Brenz";
const ICON_SIZE = 30;
const TIMEZONE = "Europe/Berlin";

try {

  // --------------------------------------------------
  // Geocoding
  // --------------------------------------------------
  const geoUrl =
    `https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(LOCATION)}&count=1&format=json`;

  const geoResponse = await fetch(geoUrl);

  if (!geoResponse.ok)
    throw new Error(`Geocoding failed: ${geoResponse.status}`);

  const geoData = await geoResponse.json();

  if (!geoData.results?.length)
    throw new Error(`Location not found: ${LOCATION}`);

  const { latitude: lat, longitude: lon } = geoData.results[0];

  // --------------------------------------------------
  // Weather (alles in EINEM Request)
  // --------------------------------------------------

  const url =
    `https://api.open-meteo.com/v1/forecast?` +
    `latitude=${lat}` +
    `&longitude=${lon}` +
    `&current=temperature_2m,relative_humidity_2m,wind_speed_10m,surface_pressure` +
    `&hourly=temperature_2m,weather_code` +
    `&daily=weather_code,temperature_2m_max,temperature_2m_min,sunrise,sunset` +
    `&timezone=${encodeURIComponent(TIMEZONE)}`;

  const response = await fetch(url);

  if (!response.ok)
    throw new Error(`Weather fetch failed: ${response.status}`);

  const data = await response.json();

  const current = data.current;
  const hourly = data.hourly;
  const daily = data.daily;

  // --------------------------------------------------
  // Icons
  // --------------------------------------------------

  function getWeatherIcon(code) {

    let icon = "ph-cloud";

    if (code === 0) icon = "ph-sun";
    else if ([1,2,3].includes(code)) icon = "ph-cloud-sun";
    else if ([45,48].includes(code)) icon = "ph-cloud-fog";
    else if ([51,53,55,56,57].includes(code)) icon = "ph-cloud-drizzle";
    else if ([61,63,65,66,67,80,81,82].includes(code)) icon = "ph-cloud-rain";
    else if ([71,73,75,77,85,86].includes(code)) icon = "ph-snowflake";
    else if ([95,96,99].includes(code)) icon = "ph-cloud-lightning";

    return `:${icon}|${ICON_SIZE}:`;
  }

  // --------------------------------------------------
  // Aktuelle Stunde finden
  // --------------------------------------------------

  const currentHour = current.time.replace(/:\d\d$/, ":00");

  let startIndex = hourly.time.indexOf(currentHour);

  if (startIndex === -1)
      startIndex = 0;

  // --------------------------------------------------
  // Header
  // --------------------------------------------------

  const windSpeed = current.wind_speed_10m.toFixed(1);
  const humidity = current.relative_humidity_2m;

  const sunrise = daily.sunrise[0].slice(11,16);
  const sunset  = daily.sunset[0].slice(11,16);

  const header =
`\n---
| | |
| :--- | ---: |
| :ph-wind: ${windSpeed} km/h     :ph-drop: ${humidity}%    |:ph-sun: ${sunrise}    :ph-sun-horizon: ${sunset}  |`;

  // ==================================================
  // STUNDENVORHERSAGE
  // ==================================================

  let rowHours="|";
  let rowHoursSep="|";
  let rowHoursIcon="|";
  let rowHoursTemp="|";

  for (let i=0;i<7;i++) {

      const idx=startIndex+i;

      if(idx>=hourly.time.length)
          break;

      const d=new Date(hourly.time[idx]);

      const time=
          String(d.getHours()).padStart(2,"0")+
          ":"+
          String(d.getMinutes()).padStart(2,"0");

      rowHours += ` ${time} |`;
      rowHoursSep += " :---: |";
      rowHoursIcon += ` ${getWeatherIcon(hourly.weather_code[idx])} |`;
      rowHoursTemp += ` ${Math.round(hourly.temperature_2m[idx])}°C |`;
  }

  const weatherHours =
`${rowHours}
${rowHoursSep}
${rowHoursIcon}
${rowHoursTemp}
${header}`;

  // ==================================================
  // TAGESVORHERSAGE
  // ==================================================

  const daysGerman = ["So","Mo","Di","Mi","Do","Fr","Sa"];

  let rowDays="|";
  let rowDaysSep="|";
  let rowDaysIcon="|";
  let rowMax="|";
  let rowMin="|";

  for(let i=0;i<7;i++){

      const d=new Date(daily.time[i]);

      rowDays += ` ${daysGerman[d.getDay()]} |`;
      rowDaysSep += " :---: |";
      rowDaysIcon += ` ${getWeatherIcon(daily.weather_code[i])} |`;
      rowMax += ` ${Math.round(daily.temperature_2m_max[i])}°C |`;
      rowMin += ` ${Math.round(daily.temperature_2m_min[i])}°C |`;

  }

  const weatherDays =
`${rowDays}
${rowDaysSep}
${rowDaysIcon}
${rowMax}
${rowMin}`;

  // --------------------------------------------------
  // Flow Tags
  // --------------------------------------------------

  await tag("WeatherHours", weatherHours);
  await tag("Weatherdays", weatherDays);

  // --------------------------------------------------
  // Ausgabe
  // --------------------------------------------------

  return `Weather updated successfully.`;

}
catch(error){

  return `Error: ${error.message}`;

}

Yes, I use colored png images now. I only need 6, so a 7000-icons-gallery might be a bit overkill :sweat_smile:

I just wanted to test the integrated color-method to make it more lightweight. It also works of course with the ´block´ colors, but i find that a bit overwhelming in my mostly black&white dashboard.

NEW TEST Version V1.11.2

  • New: Text widget - tint inline Phosphor icons in markdown with the same pipe color syntax as images (e.g. :ph-heart|@accent: or :ph-duotone ph-android-logo|@accent|@danger:).
  • New: Text widget - New setting, choose how links open (follow each link (markdown setting), always a new tab, or always the same tab).

You can now color the ph icons. See the markdown documentation for the syntax :+1:

I can’t figure out why there suddenly is a horizontal scrollbar in this markdown table (can’t remember I have seen this before):


When I increase the “Offset Y” value in “Text settings” the horizontal bar (partly) disappears.

{
  "snippetHeader": "Dashboard Studio Snippet",
  "snippetType": "widget-snippet",
  "snippetFormatVersion": 1,
  "version": "1.11.1",
  "source": {
    "dashboardName": "HomeDashV2",
    "page": 28,
    "createdAt": "2026-07-02T09:36:15.202Z"
  },
  "master": {
    "currentPage": 28
  },
  "widgets": {
    "text_0973": {
      "type": "text",
      "overrides": {
        "x": 700,
        "y": 350,
        "width": 1990,
        "height": 1280,
        "page": 28,
        "text": "",
        "typography": {
          "mdParagraph": {
            "fontSize": 50,
            "fontFamily": "'Exo 2', sans-serif",
            "bold": false,
            "color": "@onSurface",
            "italic": false,
            "textAlign": "left",
            "offsetY": 0,
            "offsetX": 20
          },
          "mdH1": {},
          "mdH2": {},
          "mdH3": {
            "fontSize": 12,
            "fontFamily": "'Exo 2', sans-serif",
            "offsetX": 0
          },
          "mdTh": {
            "fontSize": 40,
            "fontFamily": "'Exo 2', sans-serif",
            "textAlign": "table_defined",
            "textTransform": "none",
            "bold": false,
            "color": "@background",
            "colorFilled": "@background",
            "offsetY": 0,
            "offsetX": 0
          },
          "mdTableText": {
            "fontSize": 35,
            "textAlign": "table_defined",
            "offsetY": 0,
            "fontFamily": "'Exo 2', sans-serif",
            "offsetX": 20
          },
          "mdBlockTypography": {}
        },
        "metaName": "Statussen",
        "bgVisible": true,
        "glowEnabled": true,
        "bgOpacityStart": 0,
        "bgOpacityEnd": 0,
        "glowColor": "#000000",
        "glowOpacity": 80,
        "borderColor": "@background",
        "borderOpacity": 30,
        "cornerRadius": 20,
        "textVerticalAlign": "top",
        "mdTableColumnWidthMode": "custom_columns",
        "mdSeparatorOpacity": 0,
        "mdTableStyle": "zebra",
        "mdTableRowAltBgOpacity": 7,
        "mdOverflowScroll": true,
        "mdTableCustomColumnWidths": {
          "columns": [
            40,
            100,
            600,
            600,
            550
          ]
        },
        "mdTableRowAltBg": "#E2E8F0",
        "mdBlockSpacing": 4,
        "mdTextWrap": false,
        "mdAutoFitText": false,
        "padding": 0,
        "textOffX": 0,
        "mdTableBorderRadius": 0,
        "mdTableCellPadding": 0,
        "mdIconWeight": "regular",
        "textOffY": 0
      },
      "bindings": {
        "text": "apparaatStatussen"
      }
    }
  }
}

I did not see any problems on the demo pages with scrollbars. The provided DS snippet does not have the data Amersfoort. (text is dynamic) I can not recreate the problem without that. Can you also provide the data?

Here is the data (icons in an old-fashioned way):

Canon printer iP7250
| | | Kenmerk | Waarde | Laatste wijziging |
| :--- | :---: | :--- | :--- | :--- |
| | <img src="https://api.iconify.design/mdi:alert-circle.svg?color=%23FFBF00&width=60&height=60" width="60" height="60" /> | Netwerkstatus | Offline | 2-7-2026, 13:25 |
| | <img src="https://api.iconify.design/mdi:printer-outline.svg?color=%2380B3FF&width=60&height=60" width="60" height="60" /> | Signaalsterkte | 100 % | 29-6-2026, 9:15 |
| | <img src="https://api.iconify.design/mdi:water.svg?color=%23000000&width=60&height=60" width="60" height="60" /> | Zwart (PGBK) | 100 % | 7-6-2026, 19:36 |
| | <img src="https://api.iconify.design/mdi:water.svg?color=%23666666&width=60&height=60" width="60" height="60" /> | Zwart (BK) | 40 % | 17-3-2026, 14:14 |
| | <img src="https://api.iconify.design/mdi:water.svg?color=%23ffff00&width=60&height=60" width="60" height="60" /> | Geel | 100 % | 13-5-2026, 16:16 |
| | <img src="https://api.iconify.design/mdi:water.svg?color=%2300ffff&width=60&height=60" width="60" height="60" /> | Cyaan | 40 % | 26-4-2026, 9:18 |
| | <img src="https://api.iconify.design/mdi:water.svg?color=%23FD3DB5&width=60&height=60" width="60" height="60" /> | Magenta | 100 % | 17-6-2026, 12:06 |

Thanks. I will fix this problem next version :+1:

@satoer You’re the best !!

Seems to work fine, gonna adapt the calendar script in the weekend… :innocent:

@widameista I also use open meteo in a script. I didn’t have the time to test your script yet, but it’s odd to me that you create only 2 tags? I have 24… some of them are only used in a e-mail & others are sent to Dashboard Studio.
I saw you use ph-cloud-drizzle as an icon, but that one doesn’t work in Dashboard Studio with me… Does it for you?

No the drizzle does not exist. I replaced it with ph-snowflake in the documentation. This icon is a cloud with small drops (not actual snowflakes)

adding additional tags is not a problem, but my intent was not to retrieve single weather details, but only the forecast in a Dashboard studio formatted manner :slight_smile:

Aha, I now tested your script. Didn’t see until now that there is a whole mark-up integrated.

Makes complete sense & looks clean :ok_hand:

Hello,

First of all thank you so much for your work, this is amazing. I’ve started recently so it might be a stupid question. I would like to show a timeline in my dashboard with notifications from Homey. I’ve tried to use the list/log widget. I made a flow that when there’s a notification in the timeline it sends it to the dashboard but I don’t understand how to show it in the dashboard after. Am I missing something obvious ?

Thanks in advance.

Hello Florence,

There’s probably a lot of different ways you can do that with Dashboard Studio.

An option can be to create a text-widget and link the text content to a unique token. In the flow you can use the Dashboard card “Send ‘unique token’ with value (tag-from-this-flow:)‘message’”

This won’t be the greatest lay-out however.

You can also put the content of the timeline messages in a variable in Homey. Create a lay-out in the text widget and then insert the variable in the text widget using the {{variable}}.

A third option, which I’m doing, is to put the (important) messages not only in the timeline, but also send them to a script. That script puts the message in JSON in a variable. It also keeps the 15 latest messages. In the Dashboard a list-widget is linked to that variable. The result looks like:

And there’s probably a lot of other options…

:grinning_face: Me Happy

edit: Now I see 2 of my screenshots 1 below the other… Wondering if this does work with the list-widget too? Gonna try, but probably won’t use it as it will become too much of a “mannekesblad”..

Thank you! It’s exactly what I wanted.

Which one of the three did you go for?

3rd one, it works fine for the messages but I seem to have made a mistake with times because all notifications have the same time (the time of the latest notification I think).

Not sure if you can help but I’ve made the script with the help of AI and I activated “show timestamp” in DashboardStudio. Am I supposed to link timestamp with something? Or is there a problem with the way the script gets the time?

const message = args[0];

const MAX_ENTRIES = 20;

const variables = await Homey.logic.getVariables();

const variable = Object.values(variables).find(v => v.name === “Timeline”);

if (!variable) {

throw new Error("La variable 'Timeline' n'existe pas.");

}

let timeline = [];

try {

timeline = JSON.parse(variable.value);

} catch (e) {

timeline = \[\];

}

const now = new Date();

const date = now.toLocaleDateString(“fr-FR”);

const time = now.toLocaleTimeString(“fr-FR”, {

hour: "2-digit",

minute: "2-digit"

});

timeline.unshift({

datetime: \`${date} ${time}\`,

text: message

});

timeline = timeline.slice(0, MAX_ENTRIES);

await Homey.logic.updateVariable({

id: variable.id,

variable: {

    value: JSON.stringify(timeline)

}

});

return true;

I don’t know **** about scripts, I also use AI to make them… It’s a question of trial and error…

Here’s mine:

// Config
const VARIABLE_NAME = 'dashboard_notifications';
const MAX_ITEMS = 15;

// Input verwacht:
// ph-icon|tekst
const input = args[0] || 'ph-bell|Leeg bericht';

// Splitsen
const parts = input.split('|');

const icon = parts[0] || 'ph-bell';
const text = parts.slice(1).join('|') || 'Leeg bericht';

// Nieuw item
const entry = {
  icon,
  text,
  timestamp: Date.now()
};

// Variabele ophalen
const variables = await Homey.logic.getVariables();

const variable = Object.values(variables).find(
  v => v.name === VARIABLE_NAME
);

if (!variable) {
  throw new Error(`Variabele ${VARIABLE_NAME} niet gevonden`);
}

// JSON lezen
let list = [];

try {
  list = JSON.parse(variable.value || '[]');
} catch (e) {
  list = [];
}

// Nieuw item toevoegen
list.unshift(entry);

// Enkel laatste X bewaren
list = list.slice(0, MAX_ITEMS);

// Opslaan
await Homey.logic.updateVariable({
  id: variable.id,
  variable: {
    value: JSON.stringify(list)
  }
});

return list;

The script is saved as ‘eventlist.js’ To add items with the flow card: