Sorry I was hospitalized for a while.
There’s no obvious cause for your problem with the script. Here’s the current version, which prints all push notification cards to the output log, with the variable bShowPushCard set to true. This should help.
/**
* Homey Flow Push, Script and Code Filter Utility
* - Version 2025-12-03
* - Added improved formatted output
* - Added support for variables in Push notification
*
* 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,
* - Flow cards that run push notifications
* - Or show all flow names unfiltered.
*
* The filtering mode is controlled by the state in oState (PUSH, SCRIPT, CODE, OFF).
*
* Key Features:
* - Retrieve all flows and advanced flows combined (deduplicated).
* - Filter cards by push, script/code name presence, or ID patterns.
* - Show script card names or excerpt code samples with configurable length limit.
* - Process push notification text by replacing [[variable tokens]] with actual values.
* - Provides detailed logging of filtered results and flow counts.
*
* Variable Naming:
* Uses short Hungarian notation for improved type clarity:
* - i for integers or counters
* - a for arrays
* - o for objects
* - s for strings
* - b for booleans
*
* Usage:
* - Jump to `applySettings(StateContainer.PUSH)` for testing:
* - StateContainer.PUSH, StateContainer.SCRIPT, StateContainer.CODE,
* - StateContainer.OFF, StateContainer.OFF_RX
* - The script outputs filtered flow data to the Output log.
*
* Author: Ruurd
* Assistance: Perplexity AI Assistant (for comments)
*
* 165 - 206 ... globalThis.oState = { ... added: globalThis.SHOW_OBJ_ID = false/true;
* 361 function replaceUsername(sUserName) {
* 97 -sAllText += `'${oFlow.name}',\n`;
*/
//-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!
// Choose your option: only make one true, else defaults to bPush
//-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!
let bPush = true;
let bScript = false;
let bCode = false;
let bOff = false;
let bOff_rx = false;
const bShowPushCard = true;
//-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!
//-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!
// State management classes
class StateRecord {
constructor(value, description) {
this.value = value;
this.description = description;
}
}
class StateContainer {
static SHOW_OBJ_ID = true; // Toggle object vs string output format
static CODE = new StateRecord("CODE", "Show CODE samples");
static SCRIPT = new StateRecord("SCRIPT", "Show script names");
static PUSH = new StateRecord("PUSH", "Show Push notification 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,
];
}
let sSettings = bPush ? StateContainer.PUSH :
bScript ? StateContainer.SCRIPT :
bCode ? StateContainer.CODE :
bOff ? StateContainer.OFF :
StateContainer.OFF_RX;
/*
Implicit Boolean-to-number coercion (true=1, false=0) makes counting trivial.
*/
let iTrue = (bPush + bScript + bCode + bOff + bOff_rx);
if (iTrue !== 1) {
bPush = bScript = bCode = bOff = bOff_rx = false;
bPush = true; // default
}
// OUTPUT
// console.log(`bPush: ${bPush}, bScript: ${bScript}, bCode: ${bCode}, bOff: ${bOff}, bOff_rx: ${bOff_rx}`);
// Initialize settings and load all Homey data (flows + variables)
await applySettings(sSettings); // StateContainer.PUSH Change State here for testing
let iCounter = 0; // Total processed flows
let iCounterFilter = 0; // Filtered results count
let iCounterDisabled = 0; // Disabled flows count
let sAllText = ""; // Accumulated output
// Main flow processing loop
for (const oFlow of aAllFlows) {
if (!b_ENABLED_ONLY || oFlow.enabled) { // Skip disabled if b_ENABLED_ONLY=true
if (!bFilter) { // Unfiltered modes (OFF, OFF_RX)
if (oState.state.value === StateContainer.OFF_RX.value) {
// OFF_RX: Sanitize filenames using regex replacements
const sResult = (SHOW_OBJ_ID
? `{name: '${oFlow.name}', id: '${oFlow.id}'}`
: `${oFlow.name.replace(sFilter, (match) => aReplacements[match] || "")}`
);
sAllText += `${sResult},\n`;
iCounter++;
} else if (oState.state.value === StateContainer.OFF.value) {
// OFF: Simple flow name list
sAllText += `'${oFlow.name}',\n`;
iCounter++;
}
}
// Filtered mode: process cards through filter() function
if (bFilter && oFlow.cards) {
const sResult = filter(oFlow.cards, oFlow);
if (sResult !== null) {
sAllText += `${sResult}\n`;
iCounterFilter++;
}
}
} else {
iCounterDisabled++; // Count skipped disabled flows
}
}
// matches comma + any trailing whitespace at end
if (globalThis.SHOW_OBJ_ID)
sAllText = sAllText.replace(/,\s*$/, '');
// Generate summary counters
const sCounters =
iCounterFilter === 0
? `(Enabled:${iCounter}, Disabled:${iCounterDisabled})`
: `(Filtered:${iCounterFilter})`;
// Comprehensive logging output with settings summary
log(
`## State: ${JSON.stringify(oState.state)}\n` +
`## Filter ${b_ENABLED_ONLY ? "Enabled" : "All"}: ${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]\n" : "")
);
return iCounter; // Return total processed count
/**
* Initializes global settings and state based on selected mode (SCRIPT/CODE/PUSH/OFF).
* Prepares filtering, variables, and flows for card analysis.
*
* @param {Object} sStateValue - State selector with .value property (e.g. StateContainer.SCRIPT)
* @returns {void}
*/
async function applySettings(sStateValue) {
// Set global constants for code preview length and filename sanitization
globalThis.i_CODE_LEN = 20;
globalThis.b_ENABLED_ONLY = true;
globalThis.aReplacements = {
"\\": "_", // Windows invalid filename chars _ underscores
"/": "_",
":": "_",
"*": "_",
"?": "_",
'"': "_",
"<": "_",
">": "_",
"|": "__",
};
// Reset filtering flags and SHOW_OBJ_ID from state container
globalThis.bScriptCard = false;
globalThis.bFilter = false;
globalThis.sFilter = "";
globalThis.SHOW_OBJ_ID = StateContainer.SHOW_OBJ_ID;
// State machine with evaluate() method that sets mode-specific filters
globalThis.oState = {
state: StateContainer.OFF_RX, // Default initial state
evaluate() {
if (sStateValue.value === StateContainer.SCRIPT.value) {
// SCRIPT mode: filter HomeyScript "run" cards
globalThis.SHOW_OBJ_ID = false;
globalThis.bScriptCard = true;
globalThis.bFilter = true;
globalThis.sFilter = "homey:app:com.athom.homeyscript:run";
return StateContainer.SCRIPT;
} else if (sStateValue.value === StateContainer.CODE.value) {
// CODE mode: filter HomeyScript "runCode" cards
globalThis.SHOW_OBJ_ID = false;
globalThis.bScriptCard = false;
globalThis.bFilter = true;
globalThis.sFilter = "homey:app:com.athom.homeyscript:runCode";
return StateContainer.CODE;
} else if (sStateValue.value === StateContainer.PUSH.value) {
// PUSH mode: filter mobile notifications
globalThis.SHOW_OBJ_ID = false;
globalThis.bScriptCard = false;
globalThis.bFilter = true;
globalThis.sFilter = "homey:manager:mobile";
return StateContainer.PUSH;
} else if (sStateValue.value === StateContainer.OFF.value) {
// OFF mode: disable filtering
globalThis.SHOW_OBJ_ID = true;
globalThis.bFilter = false;
globalThis.sFilter = "";
return StateContainer.OFF;
} else if (sStateValue.value === StateContainer.OFF_RX.value) {
// OFF_RX mode: regex for invalid filename chars only
globalThis.SHOW_OBJ_ID = true;
globalThis.bFilter = false;
globalThis.sFilter = new RegExp("[\\\\/:*?\"<>|]", "g");
return StateContainer.OFF_RX;
} else {
console.log("Unknown state");
return StateContainer.OFF_RX; // Missing return - should default
}
},
};
// Load all Homey data: flows, advanced flows, variables
globalThis.aStandardFlows = Object.values(await Homey.flow.getFlows());
globalThis.aAdvancedFlows = Object.values(await Homey.flow.getAdvancedFlows());
globalThis.aAllFlows = await getAllFlowsRaw(); // Deduplicated combined flows
globalThis.oAllVariables = await Homey.logic.getVariables();
// log(Array.isArray(oAllVariables));
// Flow separator for output formatting
globalThis.sSep = `\n-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!-!\n`;
// Apply evaluated state (incomplete line: globalThis.sInitial ?)
globalThis.oState.state = globalThis.oState.evaluate();
}
/**
* Filters cards from flows based on state (SCRIPT/CODE/PUSH) and generates summary strings.
*
* @param {Object} oCards - Cards object with card data keyed by ID
* @param {Object} [oFlow={}] - Flow metadata (uses .name for output)
* @returns {string|null} Formatted summary string or null if no matching cards
*/
async function getAllFlowsRaw() {
// Combine standard and advanced flows, deduplicate by ID using Map
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()); // Return unique flows array
}
function filter(oCards, oFlow = {}) {
// Filter cards based on current state and search filter
const aFilteredEntries = Object.entries(oCards).filter(([sKey, oCard]) => {
if (oState.state.value === StateContainer.SCRIPT.value) {
// SCRIPT mode: match ID filter + require script name
return oCard.id && oCard.id.includes(sFilter) && oCard.args?.script?.name;
} else if (oState.state.value === StateContainer.CODE.value) {
// CODE mode: match ID filter only
return oCard.id && oCard.id.includes(sFilter);
} else if (oState.state.value === StateContainer.PUSH.value) {
// PUSH mode: only allow mobile push notification cards
const allowlist = ["homey:manager:mobile", "homey:manager:mobile:push_text"];
const id = oCard.id || "";
if (bShowPushCard)
log(JSON.stringify(oCard));
return allowlist.some((prefix) => id.startsWith(prefix));
}
return false;
});
// Rebuild filtered object preserving original keys
const oFilteredCards = aFilteredEntries.reduce((obj, [sKey, oCard]) => {
obj[sKey] = oCard;
return obj;
}, {});
const iFilteredCount = aFilteredEntries.length;
let sCardNames = "", sCode = "", sPushContent = "";
// Generate state-specific summaries
if (oState.state.value === StateContainer.SCRIPT.value) {
// List script names comma-separated
sCardNames = aFilteredEntries
.map(([_, oCard]) => oCard.args.script.name)
.join(", ")
.replace(/, $/, "");
} else if (oState.state.value === StateContainer.CODE.value) {
// Extract code snippets from HomeyScript cards (first i_CODE_LEN chars)
let sCodeSamples = "";
let iCounter = 0;
for (const oCard2 of Object.values(oFilteredCards)) {
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, ' ')}`;
}
}
sCode = sCodeSamples !== "" ? sCodeSamples : 'Code not found';
} else if (oState.state.value === StateContainer.PUSH.value) {
// Process mobile push notifications: extract text, replace [[tokens]] with variable values
let iCounter = 0, sRawText = "", sUser = '', sID = '';
for (const oCard2 of Object.values(oFilteredCards)) {
iCounter++;
if (oCard2.id?.includes("homey:manager:mobile:push_text") || oCard2.uri?.includes("homey:manager:mobile")) {
sRawText = oCard2.args?.text || "";
sUser = replaceUsername(oCard2.args?.user.name || '');
sID = oCard2.args?.user.id || "";
if (sRawText) {
// Replace Homey [[tokens]] with actual variable values using tryVariableValue()
let sProcessedText = sRawText.replace(/\[\[([^\[\]]+?)\]\]/g, (match, sTokenContent) => {
// Extract variable name after last '::' or '|' separator
let iLastColon = sTokenContent.lastIndexOf('::');
let iLastPipe = sTokenContent.lastIndexOf('|');
let iSeparatorPos = Math.max(iLastColon, iLastPipe);
if (iSeparatorPos > -1) {
const sVarName = sTokenContent.substring(iSeparatorPos + 1);
const uVarValue = tryVariableValue(sVarName); // Get real variable value
return `[${uVarValue}]`;
}
return 'unknown'; // Fallback for unparseable tokens
});
// Clean up processed text: normalize whitespace, remove newlines
let sCleanText = sProcessedText
.replace(/[\r\n]+/g, ' ')
.replace(/- /g, '')
.replace(/\s{2,}/g, ' ')
.trim();
sPushContent += `#${iCounter} ${sCleanText} ${sUser}\n`;
}
}
}
}
// Return formatted result or null
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}`;
return `${sSep}${oFlow.name}${sSep}${sCode.trim()}${sSep}`;
}
return null;
}
/**
* Converts username to short @mention format (first 2 chars uppercase).
* Handles empty/missing/single-char usernames gracefully:
* - 2+ chars: first 2 uppercase - "@JO"
* - 1 char: pad with "?" - "@J?"
* - Empty/null/undefined: "??"
*
* @param {string} sUserName - Full username (e.g. "john.doe") or empty/null
* @returns {string} Always 4-char @mention: "@XY"
*
* Examples:
* replaceUsername("john.doe") - "@JO"
* replaceUsername("alice") - "@AL"
* replaceUsername("j") - "@J?"
* replaceUsername("") - "@??"
*/
function replaceUsername(sUserName) {
if (!sUserName) return '@??'; // null/undefined/empty - "@??"
const sClean = sUserName.toString().substring(0, 2).toUpperCase();
const sPadded = sClean.length === 1 ? sClean + '?' : sClean;
return `@${sPadded}`;
}
//
/**
* Attempts to get the value of a HomeyScript variable by its ID.
*
* @param {string} sInput - Input string that might be a variable ID.
* @returns {*} The variable's value if sInput is a valid ID and found,
* otherwise returns the original sInput.
*/
function tryVariableValue(sInput) {
// Check if input is a string and matches UUID format for variable IDs
if (typeof sInput !== 'string' ||
!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(sInput)) {
return sInput; // If not a valid ID, return input unchanged
}
try {
// Search the global variable store for a matching variable by ID
const oVar = Object.values(globalThis.oAllVariables).find(o => o.id === sInput);
if (!oVar) {
console.warn(`Variable with ID "${sInput}" does not exist.`);
return sInput; // Variable not found, return input
}
return oVar.value; // Return the variable's value if found
} catch (oError) {
console.error(`Error getting variable with ID "${sInput}":`, oError);
return sInput; // On error, return the input unchanged or handle otherwise
}
}