fix(filter): cleanup must work for flat-state entities (no parent channel)
The previous cleanup implementation only iterated objects with type === 'channel' and read entity_id from native. Both assumptions break for entities written by older adapter versions: those exist as flat states under hass.0.entities.<domain>.<entity_id>.<sub> with no parent channel object and often no native.entity_id. The admin UI shows them as folders because sub-states are present, but `iobroker object get hass.0.entities.text.iob_…` returns "not found". So the cleanup found nothing to delete even with valid patterns. Refactor: iterate every object under entities.*, derive entity_id from the first two path components after the namespace prefix, group by entity_id, then delete each id individually (longest first, no recursive flag — there is no parent to recurse into). Custom-config protection still applies per entity group. Works for both old flat-state and new channel+sub-state structures.
This commit is contained in:
+50
-42
@@ -702,83 +702,91 @@ class HassAdapter extends Adapter {
|
||||
}
|
||||
|
||||
const prefix = `${this.namespace}.entities.`;
|
||||
const channelsToDelete: { id: string; entityId: string }[] = [];
|
||||
|
||||
// Group every object under entities.* by its derived entity_id. We extract
|
||||
// the entity_id from the object id (first two path components after the
|
||||
// prefix) instead of native.entity_id — older objects from previous adapter
|
||||
// versions may have been written as flat states without a parent channel
|
||||
// and without native.entity_id, but their id still encodes the entity.
|
||||
const matchedByEntity = new Map<string, string[]>();
|
||||
|
||||
for (const id in allObjects) {
|
||||
if (!Object.prototype.hasOwnProperty.call(allObjects, id) || !id.startsWith(prefix)) {
|
||||
continue;
|
||||
}
|
||||
const obj = allObjects[id];
|
||||
if (obj.type !== 'channel') {
|
||||
const rest = id.substring(prefix.length);
|
||||
const parts = rest.split('.');
|
||||
if (parts.length < 2) {
|
||||
continue;
|
||||
}
|
||||
const entityId = (obj.native as Record<string, any>)?.entity_id;
|
||||
if (typeof entityId !== 'string') {
|
||||
const entityId = `${parts[0]}.${parts[1]}`;
|
||||
if (!isExcluded(entityId, this.excludePatterns)) {
|
||||
continue;
|
||||
}
|
||||
if (isExcluded(entityId, this.excludePatterns)) {
|
||||
channelsToDelete.push({ id, entityId });
|
||||
const ids = matchedByEntity.get(entityId);
|
||||
if (ids) {
|
||||
ids.push(id);
|
||||
} else {
|
||||
matchedByEntity.set(entityId, [id]);
|
||||
}
|
||||
}
|
||||
|
||||
if (channelsToDelete.length === 0) {
|
||||
if (matchedByEntity.size === 0) {
|
||||
this.log.info('Cleanup: no existing objects matched exclude patterns');
|
||||
return;
|
||||
}
|
||||
|
||||
let deletedCount = 0;
|
||||
let deletedEntityCount = 0;
|
||||
let deletedIdCount = 0;
|
||||
let keptForCustomCount = 0;
|
||||
|
||||
for (const { id, entityId } of channelsToDelete) {
|
||||
const channelCustom = (allObjects[id].common as { custom?: Record<string, unknown> } | undefined)
|
||||
?.custom;
|
||||
let hasCustom = !!(channelCustom && Object.keys(channelCustom).length);
|
||||
if (!hasCustom) {
|
||||
const subPrefix = `${id}.`;
|
||||
for (const subId in allObjects) {
|
||||
if (!subId.startsWith(subPrefix)) {
|
||||
continue;
|
||||
}
|
||||
const subCustom = (
|
||||
allObjects[subId].common as { custom?: Record<string, unknown> } | undefined
|
||||
)?.custom;
|
||||
if (subCustom && Object.keys(subCustom).length) {
|
||||
hasCustom = true;
|
||||
break;
|
||||
}
|
||||
for (const [entityId, ids] of matchedByEntity) {
|
||||
// Custom-config protection: scan all ids of the group; if any holds
|
||||
// common.custom (history/influxdb/sql) keep the whole entity.
|
||||
let hasCustom = false;
|
||||
for (const id of ids) {
|
||||
const custom = (allObjects[id].common as { custom?: Record<string, unknown> } | undefined)
|
||||
?.custom;
|
||||
if (custom && Object.keys(custom).length) {
|
||||
hasCustom = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasCustom) {
|
||||
keptForCustomCount++;
|
||||
this.log.warn(
|
||||
`Cleanup: keeping "${id}" (entity ${entityId}) — has custom adapter config (history/influxdb/sql); remove it manually if you really want to drop it`,
|
||||
`Cleanup: keeping entity "${entityId}" — has custom adapter config (history/influxdb/sql); remove it manually if you really want to drop it`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.delObjectAsync(id, { recursive: true });
|
||||
delete this.hassObjects[id];
|
||||
const subPrefix = `${id}.`;
|
||||
for (const cachedId of Object.keys(this.hassObjects)) {
|
||||
if (cachedId.startsWith(subPrefix)) {
|
||||
delete this.hassObjects[cachedId];
|
||||
}
|
||||
// Delete sub-states first (longest ids), then any parent channel last.
|
||||
const sortedIds = [...ids].sort((a, b) => b.length - a.length);
|
||||
let entityFullyDeleted = true;
|
||||
for (const id of sortedIds) {
|
||||
try {
|
||||
await this.delObjectAsync(id);
|
||||
delete this.hassObjects[id];
|
||||
deletedIdCount++;
|
||||
} catch (err) {
|
||||
entityFullyDeleted = false;
|
||||
this.log.error(`Cleanup: failed to delete "${id}": ${err}`);
|
||||
}
|
||||
deletedCount++;
|
||||
}
|
||||
if (entityFullyDeleted) {
|
||||
deletedEntityCount++;
|
||||
if (this.config.verboseFilterLog) {
|
||||
this.log.info(`Cleanup: deleted "${id}" (entity ${entityId})`);
|
||||
this.log.info(
|
||||
`Cleanup: deleted entity "${entityId}" (${ids.length} object${ids.length === 1 ? '' : 's'})`,
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
this.log.error(`Cleanup: failed to delete "${id}": ${err}`);
|
||||
}
|
||||
}
|
||||
|
||||
this.log.info(
|
||||
`Cleanup: deleted ${deletedCount} excluded entit${deletedCount === 1 ? 'y' : 'ies'}` +
|
||||
`Cleanup: deleted ${deletedEntityCount} excluded entit${deletedEntityCount === 1 ? 'y' : 'ies'} (${deletedIdCount} object${deletedIdCount === 1 ? '' : 's'} total)` +
|
||||
(keptForCustomCount > 0
|
||||
? `, kept ${keptForCustomCount} with custom config (see warnings above)`
|
||||
? `, kept ${keptForCustomCount} entit${keptForCustomCount === 1 ? 'y' : 'ies'} with custom config (see warnings above)`
|
||||
: ''),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user