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

Hi,

I think I may have found an issue with the Switch/Button widget in v1.10.2.

I tested with both Yale locks and Hue lights.

  • Homey topic updates correctly
  • Data Stream Explorer updates correctly
  • Text widget updates correctly
  • Switch/Button widget does not update when the topic changes externally (Homey Flow or device state change)

Example:

Topic:
kontor/locked

When the lock is changed by a Homey Flow, the topic value changes immediately in Data Stream Explorer, but the Switch/Button widget keeps its previous visual state until I click it myself.

I can reproduce the same behavior with Hue on/off topics.

Is this a known issue or am I missing a setting?

I was curious if I could put an iframe widget in an SVG, but then I ran into all kinds of browser security measures.
A small prototype of what this might look like in Dashboard Studio.

The weather widget was generated by Gemini as a Homey script that generates an SVG file.

Have you entered the device’s on/off variable in State (on/off)?

Thank you, that did it :blush:

NEW Stable version v1.10.3/4

  • Improved: Light Grid widget - Item Height (px), Item Width (px), and Item Spacing (px) in Grid Layout (0 = auto; same defaults as before).
  • New: Light Grid widget - per-light Output Mode (Light or Switch). Switch mode toggles on click and sends On/Off values to output topics (optional Go to Page or Open URL actions).
  • Improved: Text widget markdown now supports sizing of images and icons.
  • Fixed: Text widget inline topics now parse icons and special markdown syntax.
  • Fixed: Icon Style slideout - Style dropdown no longer clips at the slideout boundary.

Light Grid:

Light Grid items can now be configured as a regular toggle switch:

You can now change the Item Height, Width and Item Spacing in the Grid Layout slideout :+1:

Text Widget:

It now possible to change the size of the icons and images inside the text widget markdown. No tinting though.

This is generated with a Homeyscript that passes markdown data to a Text widget.

I did not write the script myself, it is entirely coded by Gemini. LLM Ai chat (Like Gemini, ChatGPT, Claude) models are incredibly skilled to write Homey scripts, and if you feed it also with the help file it can perfectly generate the markdown for such a widget. In this case I created two Homeyscripts one that generates the 7 day forecast, and one that generates the coming 6 hours.

This was the AI prompt I did for the 7 day forecast:

I am using the Homey app dashboard studio and want to create a 7 day weather widget for “Heidenheim an der Brenz”. Could you help me with the Homeyscript code to generate a horizontal markdown table for the text widget? Inside each cell, I need the day in German, a weather icon like a cloud or sun, the max temperature as just a number with no label, and the min temperature as just a number with no label. This is the help file with the specialised markdown that can be used: [Help file snippet]

In the online help file you can copy all the markdown with the copy button (if you hover above the titles):

I copied this and pasted it below above prompt.
Gemini generated working homeyscript first try, but after some follow up requests (Like adjustable icon size and able to set a location name instead of longitude and latitude) this was the final code for the 7 day forecast:

// --- Configuration ---
const LOCATION = "Heidenheim an der Brenz";
const ICON_SIZE = 50; // Size in pixels
const TIMEZONE = "Europe/Berlin";

try {
  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 fetch failed: ${geoResponse.status}`);
  
  const geoData = await geoResponse.json();
  if (!geoData.results || geoData.results.length === 0) throw new Error(`Location not found: ${LOCATION}`);
  
  const lat = geoData.results[0].latitude;
  const lon = geoData.results[0].longitude;

  const url = `https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lon}&daily=weather_code,temperature_2m_max,temperature_2m_min&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 daily = data.daily;
  
  const daysGerman = ["Sonntag", "Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag"];
  
  function getWeatherIcon(code) {
    let iconName = "cloud";
    if (code === 0) iconName = "sun"; 
    if ([1, 2, 3].includes(code)) iconName = "cloud-sun"; 
    if ([45, 48].includes(code)) iconName = "cloud-fog"; 
    if ([51, 53, 55, 56, 57].includes(code)) iconName = "cloud-snow"; 
    if ([61, 63, 65, 66, 67, 80, 81, 82].includes(code)) iconName = "cloud-rain"; 
    if ([71, 73, 75, 77, 85, 86].includes(code)) iconName = "snowflake"; 
    if ([95, 96, 99].includes(code)) iconName = "cloud-lightning"; 
    
    return `:ph-${iconName}|${ICON_SIZE}:`;
  }

  let rowDays  = "|";
  let rowSeps  = "|";
  let rowIcons = "|";
  let rowMax   = "|";
  let rowMin   = "|";
  
  for (let i = 0; i < 7; i++) {
    const date = new Date(daily.time[i]);
    const dayName = daysGerman[date.getDay()];
    const icon = getWeatherIcon(daily.weather_code[i]);
    const maxTemp = Math.round(daily.temperature_2m_max[i]) + "°C";
    const minTemp = Math.round(daily.temperature_2m_min[i]) + "°C";
    
    rowDays  += ` ${dayName} |`;
    rowSeps  += ` :---: |`; 
    rowIcons += ` ${icon} |`; 
    rowMax   += ` ${maxTemp} |`;
    rowMin   += ` ${minTemp} |`;
  }
  
  return `${rowDays}\n${rowSeps}\n${rowIcons}\n${rowMax}\n${rowMin}\n`;
  
} catch (error) {
  return `Error: ${error.message}`;
}

And with a similar prompt This was the final Homeyscript code for the 6 hour forecast:

// --- Configuration ---
const LOCATION = "Heidenheim an der Brenz";
const ICON_SIZE = 50; // Size in pixels
const TIMEZONE = "Europe/Berlin";

try {
  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 fetch failed: ${geoResponse.status}`);
  
  const geoData = await geoResponse.json();
  if (!geoData.results || geoData.results.length === 0) throw new Error(`Location not found: ${LOCATION}`);
  
  const lat = geoData.results[0].latitude;
  const lon = geoData.results[0].longitude;

  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&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;
  
  function getWeatherIcon(code) {
    let iconName = "ph-cloud"; 
    if (code === 0) iconName = "sun"; 
    if ([1, 2, 3].includes(code)) iconName = "cloud-sun"; 
    if ([45, 48].includes(code)) iconName = "cloud-fog"; 
    if ([51, 53, 55, 56, 57].includes(code)) iconName = "cloud-snow"; 
    if ([61, 63, 65, 66, 67, 80, 81, 82].includes(code)) iconName = "cloud-rain"; 
    if ([71, 73, 75, 77, 85, 86].includes(code)) iconName = "snowflake"; 
    if ([95, 96, 99].includes(code)) iconName = "cloud-lightning"; 

    return `:ph-${iconName}|${ICON_SIZE}:`;
  }

  const now = new Date();
  let startIndex = 0;
  for (let i = 0; i < hourly.time.length; i++) {
    const d = new Date(hourly.time[i]);
    if (d.getTime() > now.getTime() - 60 * 60 * 1000) {
      startIndex = i;
      break;
    }
  }

  const currentTemp = Math.round(current.temperature_2m) + "°C";
  const windSpeed = current.wind_speed_10m.toFixed(1);
  const humidity = current.relative_humidity_2m;
  const pressureMmHg = Math.round(current.surface_pressure * 0.750062);
  
  let markdown = `:ph-thermometer: ${currentTemp} &nbsp;&nbsp;&nbsp;&nbsp; :ph-wind: ${windSpeed} m/s &nbsp;&nbsp;&nbsp;&nbsp; :ph-drop: ${humidity}% &nbsp;&nbsp;&nbsp;&nbsp; :ph-gauge: ${pressureMmHg} mmHg\n---\n`;

  let rowHours = "|";
  let rowSeps  = "|";
  let rowIcons = "|";
  let rowTemps = "|";

  for (let i = 0; i < 6; i++) {
    const idx = startIndex + i;
    if (idx >= hourly.time.length) break;

    const d = new Date(hourly.time[idx]);
    const hours = String(d.getHours()).padStart(2, '0');
    const minutes = String(d.getMinutes()).padStart(2, '0');
    const timeStr = `${hours}:${minutes}`;

    const icon = getWeatherIcon(hourly.weather_code[idx]);
    const temp = Math.round(hourly.temperature_2m[idx]) + "°C";

    rowHours += ` ${timeStr} |`;
    rowSeps  += ` :---: |`;
    rowIcons += ` ${icon} |`; 
    rowTemps += ` ${temp} |`;
  }

  return `${markdown}${rowHours}\n${rowSeps}\n${rowIcons}\n${rowTemps}\n`;

} catch (error) {
  return `Error: ${error.message}`;
}

Then create the following Homey Flow and add the code to the two Homeyscripts (that returns text tags):

Right click the day and time node and press “execute from here” to let it update the topics.
Save and Open the Dashboard studio editor. (Make sure you are on the latest v1.10.3 version, you can see this in the top of the editor)
You can make the “Text Content” setting dynamic and add the topic in there, but since these are two topics, you can also add inline topics inside the text content:

# :ph-thermometer-simple: 7 Tage Wetterdaten für Heidenheim an der Brenz
---
{{weather7day}}

{{weather5hr}}

I also changed these settings:

And… “Zack die Bohne!” A beautiful weather forecast that completely fits the theme of the dashboard. And when you switch themes it completely blends with the theme:

Nice weather over there! :smiling_face_with_sunglasses: :sun_with_face:

WOW, i see you like challenges :slight_smile: now i do not have any reason not to switch, only thing i need is time!

started to play around…
question: what is the widget “label” got for. I can do the same (and more) with text, right?
And how do i realize things like this:

The text widget is way more heavy with the full markdown engine it is using. Sometimes you just need a simple label somewhere, than it is way more efficient to use the label widget. So yes, the text widget can do the same, but uses more system resources.

Easiest way is probably just the text widget:

### 🚙 Wohnmobil 🚙

| | |
| :--- | ---: |
| :ph-thermometer: Temp Womo | 27.7 °C |
| :ph-thermometer-cold: Temp Kühli | 5.7 °C |
| :ph-drop: Wasser | 132 L |
| :ph-gas-pump: Gas vorne | 100 % |
| :ph-gas-pump: Gas hinten | 0 % |
| :ph-battery-charging: Batterie | 199 Ah |
| :ph-solar-panel: Solar heute | 10 Ah |

Replace the values for topics like: {{temperature_womo}}
Play a bit with the table styles and the Table Text typography Vertical Spacing and Line Height.

This is the syntax how to align rows. All available options are explained in is in the Markdown reference help file and more regular info about the text widget in this help section.

I also just written the methods how to create that weather widget in the tips section of the documentation. (Not yet available in the help file inside the editor)

after asking i found out myself :slight_smile: you are really brilliant

Well… that credit goes to user @Amersfoort, Column alignment was his request and idea to do it this way. :rofl:

could you have a look at the two widgets with the titel “Lampen”.
the left one is a light grid, the right one a simple container.
all paddings are set to 0 and text settings for the widget label are identic, but space between top border and text is different.

any idea?

My custom icons are black in a Markdown table. Hopefully, it is possible to set a color (“tinting”) for external icons in a table. Sometimes things that seem impossible are actually possible with some DS magic, as can be read above.

Hi Satoer,

I’ve only just started playing around with the app, but I already wanted to say: impressive work. It’s clear that a lot of effort and passion has gone into this project.

Thanks for sharing it with the Homey community!

Best regards, Jan

NEW TEST version 1.10.5:

  • Fixed: Widget labels line up with the container when padding is zero, and label settings are named and organised the same way on every widget.

This was indeed a bug. I have fixed this. and released as test version this time. The problem is that all current dashboards Container widget labels will shift by this change. I could add a migration rule that adds an X/Y correction to the Typography section, but it is a kind of ugly fix. Better to change the padding. Lets see what the backlash is in the testing community :sweat_smile:

I have never said it is impossible, I just mentioned that it is not supported in the released version (Because I know you requested it and will remind me to it :face_with_tongue: ). Same as the tinting of images. Never said is is impossible. But I only partially fulfilled your request.

Thanks Jan. Glad you like it. :blush:

At first glance, no problems with my screens.

Hi!!! wowww amazing job!!
Ho did you do to get the album image? I am getting crazy and I cannot get it..

I am using Wiim by the way … please! help me!

Retrieving a media player (such as a Wiim) image is quite tricky:
Steps:

  • start music with album art via the Homey Wiim app
  • check in the Homey developer app if you can see the album art
  • retrieve the album art using a Homey script; see my previous script suitable for Sonos, but the essence is the same.

If you have never created a Homey script or JavaScript before, it might not be wise to tackle this case first.

If you need more help, please let me know.

My latest version of the player in Dashboard Studio:

This is a more generic Sonos (I don’t own other devices) script for retrieving the album art. I think other media players have similar functionality:

Note:

  • fill in your own Sonos_id or media player id in const SONOS_ID
  • fill in your own Homey cloud id in HOMEY_API_ID
  • variations in settings in various media players are possible.

The result is an url with album art, which can be used in a Dashboard Studio image.
The media player has to play to see the image.

// 1. Configuration (Constants outside of the logic)
const SONOS_ID = 'your media player id';
// Unique Homey cloud ID used for the API endpoint
const HOMEY_API_ID = 'homey cloud id found in Homey developer tools -> system -> cloudid';
const BASE_URL = "https://" + HOMEY_API_ID + ".homey.athom-prod-euwest1-001.homeypro.net/api/image/";

// 2. Retrieve the specific device
// This object contains extensive metadata and must be cleaned up at the end
let sonos = await Homey.devices.getDevice({ id: SONOS_ID });

// 3. Validation and early exit strategy (Guard Clauses)
if (!sonos) {
    console.error("Configuration error: Sonos device not found.");
    sonos = null; // Clean up immediately upon early exit
    return null;
}

// 4. Check status and retrieve ID directly
// We stop immediately if nothing is playing, which saves system resources.
const isPlaying = sonos.capabilitiesObj?.speaker_playing?.value;
const imageId = isPlaying ? sonos.images[0]?.imageObj?.id : null;

if (!imageId) {
    console.log("No active image available (Sonos is paused or no ID found).");
    sonos = null; // Clean up immediately upon early exit
    return "";
}

// 5. Generate URL via an Array builder to prevent unnecessary string allocations
let urlBuilder = [];
const cacheBuster = Math.floor(Math.random() * 10000) + 1;

urlBuilder.push(BASE_URL);
urlBuilder.push(imageId);
urlBuilder.push("?rand=");
urlBuilder.push(cacheBuster);

// Single concatenation of the URL string
const finalUrl = urlBuilder.join('');
console.log("Generated URL: " + finalUrl);

// 6. EXPLICIT CLEANUP FOR THE GARBAGE COLLECTOR
// Set the heavy device object and temporary array directly to null
sonos = null;
urlBuilder = null;

// 7. Return result
return finalUrl;