Peter, Kim, this is the flow export script with PUSH cards added:
/**
*
* Homey Flow Push, Script and Code Filter Utility
*
* This script fetches all Homey standard and advanced flows and provides filtered views
* based on current settings. It supports filtering to list:
* - Flow cards that run HomeyScript scripts,
* - Flow cards that run HomeyScript code,
* - Or show all flow names unfiltered.
*
* The filtering mode is controlled by the state in oState (SCRIPT, CODE, or OFF).
*
* Key Features:
* - Retrieve all flows and advanced flows combined.
* - Filter cards by push, script/code name presence.
* - Show script card names or excerpt code samples with length limit.
* - Provides detailed logging of filtered results and flow counts.
*
* Variable Naming:
* Uses short Hungarian notation for improved clarity:
* - i for integers or counters
* - a for arrays
* - o for objects
* - s for strings
* - b for booleans
*
* Usage:
* - Set the desired state via `oState.state` in SETTINGS:
* (StateContainer.PUSH, StateContainer.SCRIPT, StateContainer.CODE, StateContainer.OFF).
* - The script outputs filtered flow data to the Output log.
*
* Author: Ruurd
* Assistance: Perplexity AI Assistant
* Date: December 2025 - Version 2025-12-02 - improved formatted output
*
*/
class StateRecord {
constructor(value, description) {
this.value = value;
this.description = description;
}
}
class StateContainer {
static SHOW_OBJ_ID = true;
static CODE = new StateRecord("CODE", "Show CODE samples");
static SCRIPT = new StateRecord("SCRIPT", "Show script names");
static PUSH = new StateRecord("PUSH", "Show Push cards contents");
static OFF = new StateRecord("OFF", "No filter, show all flow names");
static OFF_RX = new StateRecord("OFF_RX", "Regex based filter");
static allStates = [
StateContainer.CODE,
StateContainer.SCRIPT,
StateContainer.PUSH,
StateContainer.OFF,
StateContainer.OFF_RX,
];
}
await applySettings(StateContainer.PUSH); // Change State here for testing (CODE, SCRIPT, PUSH, OFF, OFF_RX)
let iCounter = 0;
let iCounterFilter = 0;
let iCounterDisabled = 0;
let sAllText = "";
for (const oFlow of aAllFlows) {
if (!b_ENABLED_ONLY || oFlow.enabled) {
if (!bFilter) {
if (oState.state.value === StateContainer.OFF_RX.value) {
const sResult = (SHOW_OBJ_ID
? `{name: '${oFlow.name}'`
: `${oFlow.name.replace(sFilter, (match) => aReplacements[match] || "")}`
) + (SHOW_OBJ_ID ? `, id: '${oFlow.id}'}` : "");
sAllText += `${sResult}\n`;
iCounter++;
} else if (oState.state.value === StateContainer.OFF.value) {
sAllText += `${oFlow.name}\n`;
iCounter++;
}
}
if (bFilter && oFlow.cards) {
const sResult = filter(oFlow.cards, oFlow);
if (sResult !== null) {
sAllText += `${sResult}\n`;
iCounterFilter++;
}
}
} else {
iCounterDisabled++;
}
}
const sCounters =
iCounterFilter === 0
? `(Enabled:${iCounter}, Disabled:${iCounterDisabled})`
: `(Filtered:${iCounterFilter})`;
log(
`## State: ${JSON.stringify(oState.state)}\n` +
`## Filter ${b_ENABLED_ONLY ? "Enabled" : ""}: ${bFilter
? typeof sFilter === "string"
? `"${sFilter}"`
: sFilter.toString()
: StateContainer.OFF.value
}\n` +
`## allFlows: ${aAllFlows.length} ${sCounters}\n` +
`## Enabled Flows Only: ${b_ENABLED_ONLY}\n` +
`## Show as Object: ${SHOW_OBJ_ID} (Show as Object with ID)\n\n` +
(SHOW_OBJ_ID ? "const aFlows = [\n" : "") +
sAllText +
(SHOW_OBJ_ID ? "]\n" : "")
);
return iCounter;
async function applySettings(sStateValue) {
globalThis.i_CODE_LEN = 20;
globalThis.b_ENABLED_ONLY = true;
globalThis.aReplacements = {
"\\": "_",
"/": "_",
":": "_",
"*": "_",
"?": "_",
'"': "_",
"<": "_",
">": "_",
"|": "__",
};
globalThis.bScriptCard = false;
globalThis.bFilter = false;
globalThis.sFilter = "";
globalThis.SHOW_OBJ_ID = StateContainer.SHOW_OBJ_ID;
globalThis.oState = {
state: StateContainer.OFF_RX,
evaluate() {
if (sStateValue.value === StateContainer.SCRIPT.value) {
globalThis.bScriptCard = true;
globalThis.bFilter = true;
globalThis.sFilter = "homey:app:com.athom.homeyscript:run";
return StateContainer.SCRIPT;
} else if (sStateValue.value === StateContainer.CODE.value) {
globalThis.bScriptCard = false;
globalThis.bFilter = true;
globalThis.sFilter = "homey:app:com.athom.homeyscript:runCode";
return StateContainer.CODE;
} else if (sStateValue.value === StateContainer.PUSH.value) {
globalThis.bScriptCard = false;
globalThis.bFilter = true;
globalThis.sFilter = "homey:manager:mobile";
return StateContainer.PUSH;
} else if (sStateValue.value === StateContainer.OFF.value) {
globalThis.bFilter = false;
globalThis.sFilter = "";
return StateContainer.OFF;
} else if (sStateValue.value === StateContainer.OFF_RX.value) {
globalThis.bFilter = false;
globalThis.sFilter = new RegExp("[\\\\/:*?\"<>|]", "g");
return StateContainer.OFF_RX;
} else {
console.log("Unknown state");
}
},
};
globalThis.aStandardFlows = Object.values(await Homey.flow.getFlows());
globalThis.aAdvancedFlows = Object.values(await Homey.flow.getAdvancedFlows());
globalThis.aAllFlows = await getAllFlowsRaw();
globalThis.sSep = `\n-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!\n`;
oState.state = oState.evaluate();
}
async function getAllFlowsRaw() {
// Combine standard and advanced flows, remove duplicates by id
const map = new Map();
aStandardFlows.forEach((flow) => map.set(flow.id, flow));
aAdvancedFlows.forEach((flow) => map.set(flow.id, flow));
return Array.from(map.values());
}
function filter(oCards, oFlow = {}) {
const aFilteredEntries = Object.entries(oCards).filter(([sKey, oCard]) => {
if (oState.state.value === StateContainer.SCRIPT.value) {
return oCard.id && oCard.id.includes(sFilter) && oCard.args?.script?.name;
} else if (oState.state.value === StateContainer.CODE.value) {
return oCard.id && oCard.id.includes(sFilter);
}
else if (oState.state.value === StateContainer.PUSH.value) {
const allowlist = ["homey:manager:mobile", "homey:manager:mobile:push_text"]; // mobile push notifications
const blocklist = [
"homey:manager:cron", // cron scheduling
"homey:manager:date", // calendar/date selection
"homey:manager:logic", // logic group cards
"homey:manager:time", // time related cards
"homey:manager:system", // system internal cards
"homey:manager:schedule", // scheduling cards
"homey:manager:event", // event trigger cards
"homey:manager:flow", // flow-related cards
"homey:manager:energy", // energy monitoring cards
];
const id = oCard.id || "";
const allowed = allowlist.some((prefix) => id.startsWith(prefix));
const blocked = blocklist.some((prefix) => id.startsWith(prefix));
return allowed && !blocked;
}
return false;
});
const oFilteredCards = aFilteredEntries.reduce((obj, [sKey, oCard]) => {
obj[sKey] = oCard;
return obj;
}, {});
const iFilteredCount = aFilteredEntries.length;
let sCardNames = "";
let sCode = "";
let sPushContent = "";
if (oState.state.value === StateContainer.SCRIPT.value) {
sCardNames = aFilteredEntries
.map(([_, oCard]) => oCard.args.script.name)
.join(", ")
.replace(/, $/, "");
} else if (oState.state.value === StateContainer.CODE.value) {
let sCodeSamples = "";
let iCounter = 0;
for (const oCard2 of Object.values(oFilteredCards)) {
//if (oCard2.args?.code) {
if (oCard2.id?.includes("homey:app:com.athom.homeyscript:runCode") || (oCard2.args?.code)) { //
iCounter++;
sCodeSamples += (`\n#${iCounter} ${oCard2.args.code.substring(0, i_CODE_LEN).replace(/[\r\n]+|\s{2,}/g, ' ')}`)
// Replace newlines and multiple spaces with single space
}
}
sCode = sCodeSamples !== "" ? sCodeSamples : 'Code not found';
}
else if (oState.state.value === StateContainer.PUSH.value) {
let iCounter = 0;
let rawText = "";
// log(Object.values(oFilteredCards).length)
for (const oCard2 of Object.values(oFilteredCards)) {
iCounter++;
if (oCard2.id?.includes("homey:manager:mobile:push_text") || oCard2.uri?.includes("homey:manager:mobile")) { //
// This is the push card message
rawText = oCard2.args?.text || ""; // || oCard2.args?.message
}/*
else
rawText = `#${iCounter}` + JSON.stringify(oCard2);*/
const cleanText = rawText
.replace(/\[\[.*?\]\]/g, '') // Remove [[...]] tokens
.replace(/[\r\n]+/g, ' ') // Replace newlines with space (keep before collapsing spaces)
// .replace(/\s{2,}/g, ' ') // Collapse multiple spaces into one
.replace(/- /g, '') // Remove "- " sequences
.replace(/\s{2,}/g, ' ') // Collapse multiple spaces into one
.trim();
sPushContent += `#${iCounter} ${cleanText}\n`;
}
}
if (iFilteredCount > 0) {
if (oState.state.value === StateContainer.PUSH.value)
return `${sSep}${oFlow.name}${sSep}${sPushContent.trim()}`;
else if (oState.state.value === StateContainer.SCRIPT.value)
return `${sSep}${oFlow.name}${sSep}${iFilteredCount} ->> ${sCardNames}`;
else return `${sSep}${oFlow.name}${sSep}${sCode.trim()}${sSep}`;
}
return null;
}
output example:
-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!
sensTuin
-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!
#1 Het is buiten nu koeler dan binnen!
#2 Het is buiten nu warmer dan binnen!
#3 Het is voor nu warmer dan in de studio!
#4 Het is voor nu koeler dan in de studio!
#5 De buitentemperatuur is nu < 0℃, zet warmtepomp SPAARSTAND LAGER. (niveau 2 of 1)
…
-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!
Storing
-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!
#1 WP Buiten is offline.
#2 WP Binnen is offline.
#3 :: Ecoflow online: werd weer ingeschakeld.
#4 :: KruipQlima online: werd weer ingeschakeld.
#5 Storing Net@EcoHW OFFLINE zie: sInterim ; bInterim
#6 Storing Net@KruipQ OFFLINE zie: sInterim ; bInterim
#7 Storing Net@Ecoflow OFFLINE zie: sInterim ; bInterim
#8 Storing Net@Ecoflow OFFLINE zie: sInterim ; bInterim
…