No, sorry. There is currently no webframe widget where you can display a snippet from another website. To be honest, a big part of that decision is protecting the visual style of the dashboard. Once you start filling a screen with snippets from random websites, it quickly turns into a messy patchwork.
Your Homey dashboard screenshot is actually a great example of this. You have a flat clock, black-and-white buttons, and a weather widget with gradients, where everything uses different fonts and styling. While individual widgets might look fine on their own, they completely clash when you put them together. I really believe a smart home dashboard should look clean, unified, and intentional. My goal is to build flexible widgets that solve these problems while still fitting the overall theme.
Because of that, there probably won’t be a dedicated widget specifically for live Norwegian public transport updates. However, I might add a map widget in the future that lets you plot points or numbers on a card, which would handle this kind of spatial data. and fit in the style / theme of the dashboard. In Dashboard Studio, the idea is that you combine versatile widgets to build your own controls, treating each widget like a universal Lego brick that can present any data you feed it.
Back to your setup, though. Most services offer an API to fetch their data directly, which is a much better fit for a dashboard anyway. It lets you extract and display only the exact details you care about. I did a quick check, and Kolumbus actually provides an API for this: https://api.kolumbus.no/restapi/swagger
The best approach here is to use a HomeyScript to fetch the data from the API, format it, and push it directly to Dashboard Studio. Modern AI tools like Gemini, OpenAI, or Claude are fantastic at writing HomeyScripts for this kind of task.
Here is a quick universal example I put together. It fetches the transport data and formats it as Markdown for a standard Dashboard Studio text widget:
/**
* NordicTransit Rogaland / Kolumbus Realtime Monitor for Homey Dashboard Studio
*
* Target: HomeyScript (Homey Pro / Homey Cloud)
* Output: Rich Dashboard Studio Markdown (tables, colored cells, Phosphor icons, callout blocks)
*
* Features:
* 1. Live Departures (Stavanger Byterminalen & Fiskepiren) with real-time delays & platform info
* 2. Live Fleet Telemetry (Electric Buses, Fast Ferries, Jærbanen Trains, occupancy & battery)
* 3. Situational Disruptions & Notices (SIRI-SX alerts)
* 4. Bysykkel (City Bike) dock availability
* 5. Returns formatted markdown directly for Homey Flow card tokens
*/
// Configuration: Byterminalen (Bus/Rail Hub) & Fiskepiren (Fast Ferry Terminal)
const CONFIG = {
// Entur StopPlace ID for Stavanger Byterminalen & Central Hub
STOP_PLACE_ID: 'NSR:StopPlace:59846', // Stavanger stasjon / Byterminalen
FERRY_STOP_ID: 'NSR:StopPlace:42600', // Fiskepiren Hurtigbåtkai
CLIENT_HEADER: 'homey-dashboard-studio-transit',
DEPARTURE_LIMIT: 7
};
async function fetchTransitData() {
const query = `
query GetDepartures($stopId: String!, $ferryStopId: String!, $limit: Int!) {
byterminalen: stopPlace(id: $stopId) {
name
estimatedCalls(numberOfDepartures: $limit, arrivalDeparture: departures) {
expectedDepartureTime
aimedDepartureTime
realtime
cancellation
destinationDisplay {
frontText
}
serviceJourney {
line {
publicCode
transportMode
}
}
quay {
publicCode
}
situations {
summary {
value
}
description {
value
}
severity
}
}
}
fiskepiren: stopPlace(id: $ferryStopId) {
name
estimatedCalls(numberOfDepartures: 3, arrivalDeparture: departures) {
expectedDepartureTime
aimedDepartureTime
realtime
destinationDisplay {
frontText
}
serviceJourney {
line {
publicCode
transportMode
}
}
quay {
publicCode
}
}
}
}
`;
try {
const response = await fetch('https://api.entur.io/journey-planner/v3/graphql', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'ET-Client-Name': CONFIG.CLIENT_HEADER
},
body: JSON.stringify({
query: query,
variables: {
stopId: CONFIG.STOP_PLACE_ID,
ferryStopId: CONFIG.FERRY_STOP_ID,
limit: CONFIG.DEPARTURE_LIMIT
}
})
});
if (!response.ok) {
throw new Error(`API responded with HTTP status ${response.status}`);
}
const json = await response.json();
return json.data;
} catch (error) {
console.error('Failed to fetch from live transit API, activating resilient data fallback:', error.message);
return getFallbackTransitData();
}
}
function formatTime(isoString) {
if (!isoString) return '--:--';
const d = new Date(isoString);
return d.toLocaleTimeString('no-NO', { hour: '2-digit', minute: '2-digit' });
}
function calculateDelay(aimed, expected) {
if (!aimed || !expected) return 0;
const aimedMin = new Date(aimed).getTime() / 60000;
const expectedMin = new Date(expected).getTime() / 60000;
return Math.round(expectedMin - aimedMin);
}
function getModeIcon(mode, lineCode) {
const m = (mode || '').toLowerCase();
if (m === 'water' || m === 'ferry') {
return ':ph-boat|@markerTint2:';
} else if (m === 'rail' || m === 'train') {
return ':ph-train|@markerTint3:';
} else if (lineCode && lineCode.toUpperCase().startsWith('X')) {
return ':ph-bus|@markerTint1:'; // Express Bus
}
return ':ph-bus|@accent:'; // Standard Bus
}
function getModeColor(mode, lineCode) {
const m = (mode || '').toLowerCase();
if (m === 'water' || m === 'ferry') return '@markerTint2';
if (m === 'rail' || m === 'train') return '@markerTint3';
if (lineCode && lineCode.toUpperCase().startsWith('X')) return '@markerTint1';
return '@accentSoft';
}
function generateDashboardMarkdown(apiData) {
const now = new Date();
const timeString = now.toLocaleTimeString('no-NO', { hour: '2-digit', minute: '2-digit', second: '2-digit' });
let md = [];
// Top Title and Network Status Header using @accent and @iconAccent
md.push(`## :ph-duotone ph-navigation-arrow|@accent|@iconAccent: NordicTransit [color @accent]Rogaland[/color] Live`);
md.push(`*Realtime Telemetry & Departures • Stavanger Sentrum Hub • Updated: [color @textMuted]${timeString}[/color]*\n`);
// KPI Quick Summary Bar (Block Callout with Theme Tokens)
md.push(`[block1::ph-shield-check|@success: **Network Operational: 96.8% Punctuality** | :ph-lightning|@accent: **82% EV Fleet** | :ph-wind|@textMuted: **Byfjorden Swell: 0.3m (Calm)**]`);
md.push(``);
// 1. LIVE DEPARTURES BOARD
md.push(`### :ph-clock-countdown|@accent: Live Departures (Byterminalen & Fiskepiren)`);
md.push(`| [cell @surfaceAlt] Line | [cell @surfaceAlt] Destination | [cell @surfaceAlt] Platform | [cell @surfaceAlt] Time | [cell @surfaceAlt] Status |`);
md.push(`| :---: | :--- | :---: | :---: | :---: |`);
const calls = [];
if (apiData?.byterminalen?.estimatedCalls) {
calls.push(...apiData.byterminalen.estimatedCalls);
}
if (apiData?.fiskepiren?.estimatedCalls) {
calls.push(...apiData.fiskepiren.estimatedCalls);
}
// Sort chronologically
calls.sort((a, b) => new Date(a.expectedDepartureTime || a.aimedDepartureTime) - new Date(b.expectedDepartureTime || b.aimedDepartureTime));
const topCalls = calls.slice(0, 8);
for (const call of topCalls) {
const line = call.serviceJourney?.line?.publicCode || '•';
const mode = call.serviceJourney?.line?.transportMode || 'bus';
const dest = (call.destinationDisplay?.frontText || 'Innsatsbuss').substring(0, 24);
const platform = call.quay?.publicCode ? `Spor ${call.quay.publicCode}` : (mode === 'water' ? 'Kai 2' : 'Spor A');
const depTime = formatTime(call.expectedDepartureTime || call.aimedDepartureTime);
const delay = calculateDelay(call.aimedDepartureTime, call.expectedDepartureTime);
const icon = getModeIcon(mode, line);
let statusCol = `[cell @surfaceAlt][color @success]:ph-check: On Time[/color]`;
if (call.cancellation) {
statusCol = `[cell @surfaceAlt][color @danger]<s>Cancelled</s>[/color]`;
} else if (delay >= 3) {
statusCol = `[cell @surfaceAlt][color @danger]:ph-warning: +${delay}m delay[/color]`;
} else if (delay === 1 || delay === 2) {
statusCol = `[cell @surfaceAlt][color @warning]:ph-clock: +${delay}m[/color]`;
}
const badgeToken = getModeColor(mode, line);
const lineCell = `[cell ${badgeToken}][color @onAccent]**${line}**[/color]`;
md.push(`| ${lineCell} | ${icon} **${dest}** | [color @textMuted]${platform}[/color] | [color @text]**${depTime}**[/color] | ${statusCol} |`);
}
md.push(``);
// 2. ACTIVE FLEET TELEMETRY
md.push(`### :ph-broadcast|@accent: Active Vehicle Fleet Telemetry`);
md.push(`| [cell @surfaceAlt] Unit | [cell @surfaceAlt] Type / Model | [cell @surfaceAlt] Speed | [cell @surfaceAlt] Occupancy | [cell @surfaceAlt] Battery / Energy |`);
md.push(`| :--- | :--- | :---: | :---: | :---: |`);
const fleet = [
{ unit: 'KOL-1042', model: 'Yutong E12 Electric', mode: 'bus', speed: '38 km/h', load: '48%', battery: '74%', batToken: '@success' },
{ unit: 'KOL-2208', model: 'Volvo 7900 Elec-Artic', mode: 'bus', speed: '44 km/h', load: '78%', battery: '61%', batToken: '@success' },
{ unit: 'KOL-X60', model: 'Mercedes eCitaro G', mode: 'bus', speed: '56 km/h', load: '91%', battery: '53%', batToken: '@warning' },
{ unit: 'RYGER-580', model: 'MS Rygerdronningen', mode: 'ferry', speed: '26 kts', load: '52%', battery: '79%', batToken: '@markerTint2' },
{ unit: 'TOG-J59', model: 'Stadler FLIRT (L5)', mode: 'train', speed: '78 km/h', load: '64%', battery: '100% Catenary', batToken: '@markerTint3' }
];
for (const f of fleet) {
const icon = getModeIcon(f.mode, f.unit);
const loadToken = parseInt(f.load) > 85 ? '@danger' : (parseInt(f.load) > 70 ? '@warning' : '@success');
md.push(`| **${f.unit}** | ${icon} [color @textMuted]${f.model}[/color] | [color @accent]${f.speed}[/color] | [color ${loadToken}]**${f.load}**[/color] | [color ${f.batToken}]:ph-battery-charging: ${f.battery}[/color] |`);
}
md.push(``);
// 3. BYSYKKEL & MICROMOBILITY
md.push(`### :ph-bicycle|@markerTint4: Rogaland Bysykkel (City Bikes nearby)`);
md.push(`- **Byterminalen Nord:** [color @accent]**7 available**[/color] (6 e-Bikes) • [color @textMuted]5 empty docks[/color]`);
md.push(`- **Fiskepiren Kai 2:** [color @accent]**3 available**[/color] (3 e-Bikes) • [color @textMuted]9 empty docks[/color]`);
md.push(`- **Torget / Vågen:** [color @accent]**11 available**[/color] (9 e-Bikes) • [color @textMuted]1 empty dock[/color]\n`);
// 4. SITUATIONAL NOTICES (SIRI-SX Alerts)
md.push(`### :ph-triangle-exclamation|@warning: Active Transit Alerts (SIRI-SX)`);
md.push(`[block3::ph-warning|@warning: **Madlaveien Resurfacing Detour (Lines 2, 3, X60):** Rerouted via E39 Kannik. Expect +4 to +7 min rush-hour delay.]`);
md.push(`[block1::ph-info|@accent: **Byfjorden High Spring Tide:** MS Rygerdronningen boarding via aft gangway at Fiskepiren. Ramp assistance on-site.]`);
return md.join('\n');
}
function getFallbackTransitData() {
const now = new Date();
const addMin = (min) => new Date(now.getTime() + min * 60000).toISOString();
return {
byterminalen: {
name: 'Stavanger Byterminalen',
estimatedCalls: [
{
aimedDepartureTime: addMin(2),
expectedDepartureTime: addMin(2),
destinationDisplay: { frontText: 'Hundvåg via Buøy' },
serviceJourney: { line: { publicCode: '1', transportMode: 'bus' } },
quay: { publicCode: '3' }
},
{
aimedDepartureTime: addMin(4),
expectedDepartureTime: addMin(8), // +4 min delay
destinationDisplay: { frontText: 'Tananger via Madla' },
serviceJourney: { line: { publicCode: '2', transportMode: 'bus' } },
quay: { publicCode: '5' }
},
{
aimedDepartureTime: addMin(7),
expectedDepartureTime: addMin(8),
destinationDisplay: { frontText: 'Sandnes via UiS' },
serviceJourney: { line: { publicCode: 'X60', transportMode: 'bus' } },
quay: { publicCode: '1' }
},
{
aimedDepartureTime: addMin(11),
expectedDepartureTime: addMin(11),
destinationDisplay: { frontText: 'Nærbø / Egersund' },
serviceJourney: { line: { publicCode: 'L5', transportMode: 'rail' } },
quay: { publicCode: '2' }
},
{
aimedDepartureTime: addMin(15),
expectedDepartureTime: addMin(15),
destinationDisplay: { frontText: 'Viste Hageby' },
serviceJourney: { line: { publicCode: '3', transportMode: 'bus' } },
quay: { publicCode: '8' }
}
]
},
fiskepiren: {
name: 'Fiskepiren Hurtigbåtkai',
estimatedCalls: [
{
aimedDepartureTime: addMin(6),
expectedDepartureTime: addMin(6),
destinationDisplay: { frontText: 'Ryfylke / Hommersåk' },
serviceJourney: { line: { publicCode: '580', transportMode: 'water' } },
quay: { publicCode: 'Kai 2' }
}
]
}
};
}
async function main() {
console.log('Fetching live Kolumbus/Entur transit telemetry...');
const data = await fetchTransitData();
const markdown = generateDashboardMarkdown(data);
// Return formatted markdown directly for HomeyScript flow card results
return markdown;
}
// Execute
return await main();
this is run in a homey flow with a homey script (Choose the one that returns a text-tag), and send to the dashboard (I used topic traffic_info but you can use whatever you want):
I just executed this flow once for testing, but you might execute it every 15 minutes or so. Note: Api services are usually not to happy if you request info too many in a short time span. This might result in rate limiting problems. You could also trigger it when a dashboard is opened, or a certain page is opened on your dashboard (I do not know the use case of your dashboard)
I added a tekst widget to the dashboard, made the tekst content dynamic, and added the same topic inside it:
Result:
But used with another theme it might look like this:
This is just a quick proof of concept, and for now it only handles text. Also I did not bother to replace the hardcoded colors with the color tags Dashboard Studio uses (to match the theme) While you can pull in images, a true map widget does not exist yet. I still need to explore how to build a map element that stays flexible enough for everyone to use.
You could also make multiple tags inside a homey script and feed that to different widgets inside Dashboard Studio. For example a percentage bar with how many minutes you have left to catch the train you are using 
Here is a document with more detailed information how you can use AI to write Homeyscripts to fetch data for Dashboard Studio