Hi @martijnpoppen, is there any change that you implement a work-around for the broken IsBroken() code?
The code below is a minor adjustment from the original AtHom implementation (found at homey-api - npm, /homey-api/lib/HomeyAPI/HomeyAPIV3/ManagerFlow/Flow.js, starting at line 18.
This is the code interfacing with the HomeyAPIV3Local, running on a my laptop in the same local network. Being a relative novice in JavaScript/TypeScript, I couldn’t get it to work in the API playground, but I think (hope?) this should be fairly easy to implement in your code.
Note that when I run this code on my HomeyPro 2023 (recently migrated from a HomeyPro 2019), it reports 7 broken flows out of 203 flows. However, when I fail to initialise flowTokens (or fail to await the getFlowTokens() call), it reports 70 broken flows. Quite a few false positives. That made me wonder if that is the source of the false positive problem that you reported. (Of course, it could still be something unrelated).
#!/usr/bin/env node
// Log in to Homey and find broken flows.
// Connect to Homey on a local network
import { HomeyAPI } from 'homey-api';
const CONFIG = {
"homey": {
"address": "http://198.51.100.113",
"token": "[YOUR API KEY]" // https://my.homey.app/settings/system/api-keys
}
}
const Homey = await HomeyAPI.createLocalAPI(CONFIG.homey);
// Actual code
const flows = await Homey.flow.getFlows();
// Fill the cache
const flowTokens = await Homey.flowtoken.getFlowTokens();
// To get false positives, replace the above with:
// const flowTokens = {};
// Since filter must be synchronous, run in two steps.
// Store boolean results in an array, and await those result
const asyncFilter = async (arr, predicate) => {
const results = await Promise.all(arr.map(predicate));
return arr.filter((_v, index) => results[index]);
}
const brokenFlows = await asyncFilter(Object.values(flows), flow => IsBroken(flow));
console.log(brokenFlows.length + " of " + Object.values(flows).length + " flows are broken")
console.log(brokenFlows);
async function IsBroken(flow) {
// Array of local & global Token IDs.
// For example [ 'foo', 'homey:x:y|abc' ]
const tokenIds = [];
const checkToken = async tokenId => {
// If this is a global Token, fetch all FlowTokens
if (tokenId.includes('|')) {
for (const flowTokenId of Object.keys(flowTokens)) {
tokenIds.push(flowTokenId);
}
tokenId = tokenId.replace('|', ':');
}
if (!tokenIds.includes(tokenId)) {
throw new Error(`Missing Token: ${tokenId}`);
}
};
const checkTokens = async card => {
// Check droptoken
if (card.droptoken) {
await checkToken(card.droptoken);
}
if (typeof card.args === 'object') {
for (const arg of Object.values(card.args)) {
if (typeof arg !== 'string') continue;
for (const [tokenMatch, tokenId] of arg.matchAll(/\[\[(.*?)\]\]/g)) {
await checkToken(tokenId);
}
}
}
};
// Check Trigger
if (flow.trigger) {
try {
// getFlowCardTriggers
// warning: getFlowCardTrigger() is very slow
const triggerCard = await flow.manager.getFlowCardTrigger({ id: flow.trigger.id });
await checkTokens(flow.trigger);
// Add FlowCardTrigger.tokens to internal tokens cache
if (Array.isArray(triggerCard.tokens)) {
for (const tokenId of Object.keys(triggerCard.tokens)) {
tokenIds.push(tokenId);
}
}
} catch (err) {
flow.error = err.message;
// flow.broken = true;
return true;
}
}
// Check Conditions
if (Array.isArray(flow.conditions)) {
for (const condition of Object.values(flow.conditions)) {
try {
// getFlowCardConditions
// eslint-disable-next-line no-unused-vars
const conditionCard = await flow.manager.getFlowCardCondition({ id: condition.id });
await checkTokens(condition);
} catch (err) {
flow.error = err.message;
// flow.broken = true;
return true;
}
}
}
// Check Actions
if (Array.isArray(flow.actions)) {
for (const action of Object.values(flow.actions)) {
try {
// getFlowCardActions
// eslint-disable-next-line no-unused-vars
const actionCard = await flow.manager.getFlowCardAction({ id: action.id });
await checkTokens(action);
} catch (err) {
flow.error = err.message;
// flow.broken = true;
return true;
}
}
}
return false;
}