Skip to content

Commit d0f64c3

Browse files
committed
Add device registry resolver to Flow Doctor
1 parent 1ee54cf commit d0f64c3

8 files changed

Lines changed: 839 additions & 108 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ node_modules/
1111

1212
# Development
1313
*.log
14+
.homey-run-logs/
1415
.DS_Store
1516
node_modules/
1617
npm-debug.log

docs/tools/flow-doctor.html

Lines changed: 114 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ <h3 class="text-lg font-semibold mb-4 text-blue-800" data-i18n="apiSetup.instruc
9898
<li class="flex gap-3"><span class="font-bold text-blue-600">2.</span><span data-i18n="apiSetup.step2">Click "Add client".</span></li>
9999
<li class="flex gap-3"><span class="font-bold text-blue-600">3.</span><span data-i18n="apiSetup.step3">Give it any name (e.g. "Flow Doctor").</span></li>
100100
<li class="flex gap-3"><span class="font-bold text-blue-600">4.</span><span data-i18n="apiSetup.step4">Paste the Redirect URL shown below.</span></li>
101-
<li class="flex gap-3"><span class="font-bold text-blue-600">5.</span><span data-i18n="apiSetup.step5">Select scopes: </span><code class="bg-gray-100 px-1 rounded">homey.flow.readonly</code> · <code class="bg-gray-100 px-1 rounded">homey.app.readonly</code> · <code class="bg-gray-100 px-1 rounded">homey.device.readonly</code> · <code class="bg-gray-100 px-1 rounded">homey.zone.readonly</code></li>
101+
<li class="flex gap-3"><span class="font-bold text-blue-600">5.</span><span data-i18n="apiSetup.step5">Select scopes: </span><code class="bg-gray-100 px-1 rounded">homey.flow.readonly</code> · <code class="bg-gray-100 px-1 rounded">homey.app.readonly</code> · <code class="bg-gray-100 px-1 rounded">homey.app.control</code> · <code class="bg-gray-100 px-1 rounded">homey.device.readonly</code> · <code class="bg-gray-100 px-1 rounded">homey.zone.readonly</code></li>
102102
<li class="flex gap-3"><span class="font-bold text-blue-600">6.</span><span data-i18n="apiSetup.step6">Click "Create" and copy the Client ID and Client Secret below.</span></li>
103103
</ol>
104104
</div>
@@ -729,6 +729,8 @@ <h2 class="text-xl font-bold text-gray-900" data-i18n="bug.title">Report a bug</
729729
};
730730

731731
const FLAGS = { en: '🇬🇧', no: '🇳🇴', de: '🇩🇪', nl: '🇳🇱' };
732+
const SMART_COMPONENTS_APP_ID = 'no.tiwas.booleantoolbox';
733+
const DEVICE_REGISTRY_SETTING_KEY = 'device_registry';
732734

733735
class FlowDoctor {
734736
constructor() {
@@ -737,6 +739,8 @@ <h2 class="text-xl font-bold text-gray-900" data-i18n="bug.title">Report a bug</
737739
this.cloudApi = null;
738740
this.api = null;
739741
this.results = [];
742+
this.deviceRegistry = null;
743+
this.deviceRegistryStatus = 'not_loaded';
740744
}
741745

742746
t(key) { return (I18N[this.lang] && I18N[this.lang][key]) || I18N.en[key] || key; }
@@ -1172,6 +1176,78 @@ <h2 class="text-xl font-bold text-gray-900" data-i18n="bug.title">Report a bug</
11721176
return true;
11731177
}
11741178

1179+
normalizeDeviceRegistry(raw) {
1180+
if (!raw) return null;
1181+
let registry = raw;
1182+
if (registry && typeof registry === 'object' && !registry.entries && Object.prototype.hasOwnProperty.call(registry, 'value')) {
1183+
registry = registry.value;
1184+
}
1185+
if (typeof registry === 'string') {
1186+
try {
1187+
registry = JSON.parse(registry);
1188+
} catch (_) {
1189+
return null;
1190+
}
1191+
}
1192+
if (!registry || typeof registry !== 'object' || !registry.entries || typeof registry.entries !== 'object') {
1193+
return null;
1194+
}
1195+
return registry;
1196+
}
1197+
1198+
async loadDeviceRegistry(installedApps) {
1199+
this.deviceRegistry = null;
1200+
this.deviceRegistryStatus = 'not_available';
1201+
1202+
if (!installedApps || !installedApps[SMART_COMPONENTS_APP_ID]) {
1203+
return null;
1204+
}
1205+
1206+
const appsApi = this.api && this.api.apps;
1207+
if (!appsApi) return null;
1208+
1209+
try {
1210+
let raw = null;
1211+
if (typeof appsApi.getAppSettings === 'function') {
1212+
const settings = await appsApi.getAppSettings({ id: SMART_COMPONENTS_APP_ID });
1213+
raw = settings && settings[DEVICE_REGISTRY_SETTING_KEY];
1214+
} else if (typeof appsApi.getAppSetting === 'function') {
1215+
raw = await appsApi.getAppSetting({
1216+
id: SMART_COMPONENTS_APP_ID,
1217+
name: DEVICE_REGISTRY_SETTING_KEY,
1218+
});
1219+
}
1220+
1221+
const registry = this.normalizeDeviceRegistry(raw);
1222+
if (registry) {
1223+
this.deviceRegistry = registry;
1224+
this.deviceRegistryStatus = 'loaded';
1225+
return registry;
1226+
}
1227+
this.deviceRegistryStatus = 'empty';
1228+
return null;
1229+
} catch (e) {
1230+
this.deviceRegistryStatus = 'error';
1231+
return null;
1232+
}
1233+
}
1234+
1235+
deviceRegistryName(deviceId) {
1236+
const id = String(deviceId || '').trim();
1237+
if (!id || !this.deviceRegistry || !this.deviceRegistry.entries) return null;
1238+
const entry = this.deviceRegistry.entries[id];
1239+
const name = entry && typeof entry.name === 'string' ? entry.name.trim() : '';
1240+
return name && name !== id ? name : null;
1241+
}
1242+
1243+
formatDeviceReference(deviceId, fallbackName = null) {
1244+
const id = String(deviceId || '').trim();
1245+
if (!id) return '';
1246+
const fallback = typeof fallbackName === 'string' ? fallbackName.trim() : '';
1247+
const name = fallback && fallback !== id ? fallback : this.deviceRegistryName(id) || 'N/A';
1248+
return `${name} (ID: ${id})`;
1249+
}
1250+
11751251
buildDebugInfo() {
11761252
// Privacy: no device names, flow names, homey-id, tokens, or any field that
11771253
// could correlate to a specific user. App names are kept (public marketing names).
@@ -1213,6 +1289,12 @@ <h2 class="text-xl font-bold text-gray-900" data-i18n="bug.title">Report a bug</
12131289
apps: { total: apps.length, crashed: nCrashed, update: nUpdate, unused: nUnused, ok: nAppsOk },
12141290
},
12151291
issuesByKey: issueKeyCount,
1292+
deviceRegistry: {
1293+
status: this.deviceRegistryStatus,
1294+
updatedAt: this.deviceRegistry?.updatedAt || null,
1295+
knownCount: this.deviceRegistry?.knownCount || null,
1296+
missingCount: this.deviceRegistry?.missingCount || null,
1297+
},
12161298
apps,
12171299
};
12181300
}
@@ -1292,6 +1374,7 @@ <h2 class="text-xl font-bold text-gray-900" data-i18n="bug.title">Report a bug</
12921374
const installedApps = {};
12931375
Object.values(apps).forEach(a => { installedApps[a.id] = a; });
12941376
const installedAppIds = new Set(Object.keys(installedApps));
1377+
await this.loadDeviceRegistry(installedApps);
12951378
const deviceIds = new Set(Object.values(devices).map(d => d.id));
12961379
const deviceToApp = {};
12971380
Object.values(devices).forEach(d => {
@@ -1337,7 +1420,7 @@ <h2 class="text-xl font-bold text-gray-900" data-i18n="bug.title">Report a bug</
13371420
const title = this.localizeStoreValue(titleRaw);
13381421
if (id.startsWith('homey:device:')) {
13391422
const devId = id.substring('homey:device:'.length).split(':')[0];
1340-
const devName = devices[devId]?.name || devId;
1423+
const devName = this.formatDeviceReference(devId, devices[devId]?.name);
13411424
return title ? `${devName}${title}` : `${devName}${id.split(':').pop()}`;
13421425
}
13431426
if (id.startsWith('homey:app:')) {
@@ -1371,7 +1454,33 @@ <h2 class="text-xl font-bold text-gray-900" data-i18n="bug.title">Report a bug</
13711454
}
13721455

13731456
let def = null;
1374-
if (uri.startsWith('homey:app:') || uri.startsWith('homey:manager:') || uri.startsWith('homey:device:')) {
1457+
const deviceCardId = uri.startsWith('homey:device:')
1458+
? uri.substring('homey:device:'.length).split(':')[0]
1459+
: null;
1460+
1461+
if (deviceCardId) {
1462+
if (deviceIds.size > 0 && !deviceIds.has(deviceCardId)) {
1463+
issues.push({ severity: 'error', key: 'issue.missingDevice', detail: describeCard(card, null), appId });
1464+
return issues;
1465+
}
1466+
1467+
const owningAppId = deviceToApp[deviceCardId];
1468+
if (owningAppId) {
1469+
appId = owningAppId;
1470+
if (!installedAppIds.has(owningAppId)) {
1471+
// Device exists but its owning app isn't installed — orphan device.
1472+
issues.push({ severity: 'warn', key: 'issue.deviceMissingApp', detail: describeCard(card, null) + ' · app: ' + owningAppId, appId: owningAppId });
1473+
}
1474+
}
1475+
1476+
// Device-scoped cards are generated by the device/driver layer and are
1477+
// not always returned by the global flow-card lists. Treat a missing
1478+
// card definition as "not enough metadata", not as a removed card.
1479+
def = lookupCardDef(card, kind);
1480+
if (def && def.deprecated) {
1481+
issues.push({ severity: 'warn', key: 'issue.deprecatedCard', detail: describeCard(card, def), appId });
1482+
}
1483+
} else if (uri.startsWith('homey:app:') || uri.startsWith('homey:manager:')) {
13751484
def = lookupCardDef(card, kind);
13761485
if (!def) {
13771486
issues.push({ severity: 'error', key: 'issue.unknownCard', detail: describeCard(card, null), appId });
@@ -1394,7 +1503,7 @@ <h2 class="text-xl font-bold text-gray-900" data-i18n="bug.title">Report a bug</
13941503
// Device reference check — only for actual device-typed args
13951504
if (argDef.type === 'device' && val && typeof val === 'object' && val.id && typeof val.id === 'string') {
13961505
if (deviceIds.size > 0 && !deviceIds.has(val.id) && val.id.length >= 16) {
1397-
issues.push({ severity: 'error', key: 'issue.missingDevice', detail: describeCard(card, def) + ' · arg "' + argDef.name + '" → ' + (val.name || val.id), appId });
1506+
issues.push({ severity: 'error', key: 'issue.missingDevice', detail: describeCard(card, def) + ' · arg "' + argDef.name + '" → ' + this.formatDeviceReference(val.id, val.name), appId });
13981507
}
13991508
}
14001509
}
@@ -1406,18 +1515,6 @@ <h2 class="text-xl font-bold text-gray-900" data-i18n="bug.title">Report a bug</
14061515
}
14071516
}
14081517

1409-
// Device-card reference (homey:device:<id>)
1410-
if (uri.startsWith('homey:device:')) {
1411-
const devId = uri.substring('homey:device:'.length).split(':')[0];
1412-
if (deviceIds.size > 0 && devId && !deviceIds.has(devId)) {
1413-
issues.push({ severity: 'error', key: 'issue.missingDevice', detail: describeCard(card, def), appId });
1414-
} else if (deviceToApp[devId] && !installedAppIds.has(deviceToApp[devId])) {
1415-
// Device exists but its owning app isn't installed — orphan device.
1416-
const missingApp = deviceToApp[devId];
1417-
issues.push({ severity: 'warn', key: 'issue.deviceMissingApp', detail: describeCard(card, def) + ' · app: ' + missingApp, appId: missingApp });
1418-
}
1419-
}
1420-
14211518
return issues;
14221519
};
14231520

@@ -1644,7 +1741,7 @@ <h2 class="text-xl font-bold text-gray-900" data-i18n="bug.title">Report a bug</
16441741
const typeLabel = r.type === 'advanced' ? this.t('flowType.advanced') : this.t('flowType.standard');
16451742
const issuesHtml = r.issues.map(i => {
16461743
const cls = i.severity === 'error' ? 'badge-err' : i.severity === 'warn' ? 'badge-warn' : 'badge-info';
1647-
return `<div class="text-sm py-1"><span class="badge ${cls}">${i.severity.toUpperCase()}</span> ${this.t(i.key)}${i.detail ? `<code class="ml-2 text-xs bg-white px-1 rounded border border-gray-200">${this.escape(i.detail)}</code>` : ''}</div>`;
1744+
return `<div class="text-sm py-1"><span class="badge ${cls}">${i.severity.toUpperCase()}</span> ${this.t(i.key)}${i.detail ? `<code class="ml-2 text-xs bg-white px-1 rounded border border-gray-200 inline-block max-w-full overflow-x-auto align-middle whitespace-nowrap">${this.escape(i.detail)}</code>` : ''}</div>`;
16481745
}).join('');
16491746

16501747
const url = this.flowUrl(r, r.type);

0 commit comments

Comments
 (0)