From 9b95ece284c277e5fc4cda120fd48932d2a171e7 Mon Sep 17 00:00:00 2001 From: mokusone <14061880+mokusone@users.noreply.github.com> Date: Thu, 14 May 2026 11:40:28 +0200 Subject: [PATCH 1/9] feat(filter): add isExcluded helper with unit tests Glob-based entity_id matcher used by the upcoming exclude-filter feature. Single wildcard `*` (matches any sequence including dots), all other characters matched literally with regex metacharacters escaped. Case-sensitive, anchored. Empty pattern array always returns false, so the helper is a no-op when the adapter is not configured to filter anything. New mocha unit-test suite invoked via `npm run test:unit` (separate from the existing js-controller-backed test:integration). --- package.json | 1 + src/lib/entityFilter.ts | 25 +++++++++++++++++++++ test/testEntityFilter.js | 47 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 73 insertions(+) create mode 100644 src/lib/entityFilter.ts create mode 100644 test/testEntityFilter.js diff --git a/package.json b/package.json index 09d1fd5..4cc9d1a 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "scripts": { "test:integration": "mocha --exit", "test:package": "mocha test/testPackageFiles.js --exit", + "test:unit": "mocha test/testEntityFilter.js --exit", "test": "npm run test:integration", "build:tsc": "tsc -p tsconfig.build.json", "build": "npm run build:tsc", diff --git a/src/lib/entityFilter.ts b/src/lib/entityFilter.ts new file mode 100644 index 0000000..1f73d15 --- /dev/null +++ b/src/lib/entityFilter.ts @@ -0,0 +1,25 @@ +/** + * Returns true if entityId matches any of the supplied glob patterns. + * Glob syntax: `*` is the only wildcard and matches any sequence of characters + * (including dots). All other characters are matched literally (regex + * metacharacters are escaped). Matching is case-sensitive and anchored to + * the full entity_id. + * + * An empty patterns array always returns false. + */ +export function isExcluded(entityId: string, patterns: string[]): boolean { + if (!patterns || patterns.length === 0) { + return false; + } + for (const pattern of patterns) { + if (globToRegex(pattern).test(entityId)) { + return true; + } + } + return false; +} + +function globToRegex(pattern: string): RegExp { + const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*'); + return new RegExp(`^${escaped}$`); +} diff --git a/test/testEntityFilter.js b/test/testEntityFilter.js new file mode 100644 index 0000000..5dc12a4 --- /dev/null +++ b/test/testEntityFilter.js @@ -0,0 +1,47 @@ +const { expect } = require('chai'); +const { isExcluded } = require('../build/lib/entityFilter'); + +describe('entityFilter.isExcluded', () => { + it('returns false for an empty pattern list', () => { + expect(isExcluded('switch.iob_anything', [])).to.equal(false); + }); + + it('matches a simple glob with leading wildcard', () => { + expect(isExcluded('switch.iob_shelly_xyz', ['*.iob_*'])).to.equal(true); + }); + + it('matches the bridge mirror naming pattern', () => { + expect(isExcluded('light.iob_ha_eg_wz1__e_licht_decke', ['*.iob_*__*'])).to.equal(true); + }); + + it('handles entities that miss a required literal segment', () => { + // *.iob_*__* requires the literal `__` segment — entities without it do not match + expect(isExcluded('switch.iob_', ['*.iob_*__*'])).to.equal(false); + expect(isExcluded('switch.iob_foo', ['*.iob_*__*'])).to.equal(false); + // Naming-Convention safety: similarly named entities without `__` are safe + expect(isExcluded('sensor.scheune_temperatur', ['*.sc_*__*'])).to.equal(false); + }); + + it('matches multiple patterns (OR semantic)', () => { + const patterns = ['*.iob_*__*', '*.knx_*', '*.ha_*__*']; + expect(isExcluded('switch.knx_foo', patterns)).to.equal(true); + expect(isExcluded('switch.ha_eg_wz1__e_licht_decke', patterns)).to.equal(true); + expect(isExcluded('switch.something_else', patterns)).to.equal(false); + }); + + it('is case sensitive', () => { + expect(isExcluded('switch.IOB_foo', ['*.iob_*'])).to.equal(false); + expect(isExcluded('switch.iob_foo', ['*.IOB_*'])).to.equal(false); + }); + + it('treats * as matching any characters including dots', () => { + // We use full entity_id including the leading domain, so this is fine. + expect(isExcluded('switch.iob_foo', ['*foo*'])).to.equal(true); + }); + + it('escapes regex metacharacters in patterns', () => { + // `.` in pattern matches literal `.`, not "any char" + expect(isExcluded('switch.iob_foo', ['switch.iob_foo'])).to.equal(true); + expect(isExcluded('switchXiob_foo', ['switch.iob_foo'])).to.equal(false); + }); +}); From 6889664c2820b65844d0949980e1625f8a762efb Mon Sep 17 00:00:00 2001 From: mokusone <14061880+mokusone@users.noreply.github.com> Date: Thu, 14 May 2026 11:40:28 +0200 Subject: [PATCH 2/9] feat(filter): integrate exclude filter into parseStates and state_changed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adapter now consults the exclude-pattern list (new `native.excludePatterns` config option, multi-line string) at two points: - `parseStates()` skips matching entities entirely. They are not registered as ioBroker objects and no state is written. A summary INFO line reports the count when the filter dropped anything from a sync. - The `state_changed` event handler short-circuits matching live updates with a DEBUG line, so a filtered entity that still emits state changes in HA does not cause any churn in ioBroker. Patterns are parsed once at adapter start: trimmed, empty lines and lines starting with `#` are dropped. An empty resulting list means the filter is inactive — adapter behaviour is then identical to the previous version. Default config ships with example comments only, so existing installations see no functional change after the update. --- io-package.json | 3 ++- src/main.ts | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/io-package.json b/io-package.json index b92e29d..3684fd7 100644 --- a/io-package.json +++ b/io-package.json @@ -252,7 +252,8 @@ "host": "127.0.0.1", "port": 8123, "password": "", - "secure": false + "secure": false, + "excludePatterns": "# Glob patterns, one per line. Lines starting with # are comments.\n# Example: filter out all entities prefixed with `iob_` regardless of domain:\n# *.iob_*" }, "protectedNative": [ "password" diff --git a/src/main.ts b/src/main.ts index 187c129..ce09d7b 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,11 +1,13 @@ import { Adapter, type AdapterOptions } from '@iobroker/adapter-core'; import HASS from './lib/hass'; +import { isExcluded } from './lib/entityFilter'; interface HassAdapterConfig { host: string; port: number; password: string; secure: boolean; + excludePatterns: string; } interface HassEntity { @@ -188,6 +190,7 @@ class HassAdapter extends Adapter { private delayTimeout: ReturnType | null = null; private syncDebounceTimeout: ReturnType | null = null; private stopped: boolean = false; + private excludePatterns: string[] = []; public constructor(options: Partial = {}) { super({ @@ -454,6 +457,7 @@ class HassAdapter extends Adapter { const objs: (ioBroker.ChannelObject | ioBroker.StateObject)[] = []; const states: { id: string; lc?: number; ts?: number; val: ioBroker.StateValue; ack: boolean }[] = []; const expectedObjects = new Set(); + let excludedCount = 0; for (let e = 0; e < entities.length; e++) { const entity = entities[e]; @@ -461,6 +465,11 @@ class HassAdapter extends Adapter { continue; } + if (isExcluded(entity.entity_id, this.excludePatterns)) { + excludedCount++; + continue; + } + const name = entity.name || entity.attributes?.friendly_name || entity.entity_id; const desc = entity.attributes?.attribution || undefined; @@ -652,12 +661,32 @@ class HassAdapter extends Adapter { } this.log.info(`Synchronization completed: ${changes.join(', ')}`); } + + if (excludedCount > 0) { + this.log.info( + `Entity filter excluded ${excludedCount} entit${excludedCount === 1 ? 'y' : 'ies'} from sync`, + ); + } } private async main(): Promise { this.config.host ||= '127.0.0.1'; this.config.port = parseInt(String(this.config.port), 10) || 8123; + const rawPatterns = (this.config.excludePatterns || '').toString(); + this.excludePatterns = rawPatterns + .split('\n') + .map(s => s.trim()) + .filter(line => line.length > 0 && !line.startsWith('#')); + + if (this.excludePatterns.length === 0) { + this.log.info('Entity filter inactive (no exclude patterns configured)'); + } else { + this.log.info( + `Entity filter active: ${this.excludePatterns.length} pattern(s) loaded: ${this.excludePatterns.join(', ')}`, + ); + } + await this.setStateAsync('info.connection', false, true); this.hass = new HASS(this.config, this.log); @@ -670,6 +699,11 @@ class HassAdapter extends Adapter { return; } + if (isExcluded(entity.entity_id, this.excludePatterns)) { + this.log.debug(`Entity filter: ignored state_changed for ${entity.entity_id}`); + return; + } + const id = `entities.${entity.entity_id}.`; const lc = entity.last_changed ? new Date(entity.last_changed).getTime() : undefined; const ts = entity.last_updated ? new Date(entity.last_updated).getTime() : undefined; From 288273005ad15d028d662f829446282f7b243d26 Mon Sep 17 00:00:00 2001 From: mokusone <14061880+mokusone@users.noreply.github.com> Date: Thu, 14 May 2026 11:40:28 +0200 Subject: [PATCH 3/9] feat(admin): add exclude patterns UI field with i18n JSON-Config field `excludePatterns` (multiline textarea, 6 rows by default, full width). Help text explains the syntax: glob with `*`, `#` for comments, empty = filter inactive. i18n translations added for all 11 supported languages so the label and help text are localised consistently with the rest of the admin UI. --- admin/i18n/de.json | 4 +++- admin/i18n/en.json | 4 +++- admin/i18n/es.json | 4 +++- admin/i18n/fr.json | 4 +++- admin/i18n/it.json | 4 +++- admin/i18n/nl.json | 4 +++- admin/i18n/pl.json | 4 +++- admin/i18n/pt.json | 4 +++- admin/i18n/ru.json | 4 +++- admin/i18n/uk.json | 4 +++- admin/i18n/zh-cn.json | 4 +++- admin/jsonConfig.json | 11 +++++++++++ 12 files changed, 44 insertions(+), 11 deletions(-) diff --git a/admin/i18n/de.json b/admin/i18n/de.json index 58113a2..33f7a2b 100644 --- a/admin/i18n/de.json +++ b/admin/i18n/de.json @@ -4,5 +4,7 @@ "Password repeat": "Passwort wiederholen", "Password": "Passwort", "Passwords missmatch!": "Passwörter stimmen nicht überein!", - "Secure": "HTTPS?" + "Secure": "HTTPS?", + "Exclude patterns (one per line)": "Filter-Patterns (eines pro Zeile)", + "Glob patterns, * wildcard. Lines starting with # are comments. Empty = filter off.": "Glob-Patterns mit * als Wildcard. Zeilen mit # sind Kommentare. Leer = Filter aus." } \ No newline at end of file diff --git a/admin/i18n/en.json b/admin/i18n/en.json index da742d4..2a3aede 100644 --- a/admin/i18n/en.json +++ b/admin/i18n/en.json @@ -4,5 +4,7 @@ "Password repeat": "Password repeat", "Password": "Password", "Passwords missmatch!": "Passwords missmatch!", - "Secure": "HTTPS?" + "Secure": "HTTPS?", + "Exclude patterns (one per line)": "Exclude patterns (one per line)", + "Glob patterns, * wildcard. Lines starting with # are comments. Empty = filter off.": "Glob patterns, * wildcard. Lines starting with # are comments. Empty = filter off." } \ No newline at end of file diff --git a/admin/i18n/es.json b/admin/i18n/es.json index 68e495b..379fff8 100644 --- a/admin/i18n/es.json +++ b/admin/i18n/es.json @@ -4,5 +4,7 @@ "Password repeat": "Repite la contraseña", "Password": "Clave", "Passwords missmatch!": "¡Las contraseñas no coinciden!", - "Secure": "¿HTTPS?" + "Secure": "¿HTTPS?", + "Exclude patterns (one per line)": "Patrones de exclusión (uno por línea)", + "Glob patterns, * wildcard. Lines starting with # are comments. Empty = filter off.": "Patrones glob, * como comodín. Las líneas que empiezan con # son comentarios. Vacío = filtro desactivado." } \ No newline at end of file diff --git a/admin/i18n/fr.json b/admin/i18n/fr.json index 18129d7..02f9bb9 100644 --- a/admin/i18n/fr.json +++ b/admin/i18n/fr.json @@ -4,5 +4,7 @@ "Password repeat": "Répéter le mot de passe ", "Password": "Mot de passe", "Passwords missmatch!": "Les mots de passe ne correspondent pas !", - "Secure": "HTTPS ?" + "Secure": "HTTPS ?", + "Exclude patterns (one per line)": "Modèles d'exclusion (un par ligne)", + "Glob patterns, * wildcard. Lines starting with # are comments. Empty = filter off.": "Modèles glob, * comme joker. Les lignes commençant par # sont des commentaires. Vide = filtre désactivé." } \ No newline at end of file diff --git a/admin/i18n/it.json b/admin/i18n/it.json index 8df3f2d..58c43bd 100644 --- a/admin/i18n/it.json +++ b/admin/i18n/it.json @@ -4,5 +4,7 @@ "Password repeat": "Ripeti password", "Password": "Parola d'ordine", "Passwords missmatch!": "Le password non corrispondono!", - "Secure": "HTTPS?" + "Secure": "HTTPS?", + "Exclude patterns (one per line)": "Pattern di esclusione (uno per riga)", + "Glob patterns, * wildcard. Lines starting with # are comments. Empty = filter off.": "Pattern glob, * come wildcard. Le righe che iniziano con # sono commenti. Vuoto = filtro disattivato." } \ No newline at end of file diff --git a/admin/i18n/nl.json b/admin/i18n/nl.json index 0bda09c..b2c9f0c 100644 --- a/admin/i18n/nl.json +++ b/admin/i18n/nl.json @@ -4,5 +4,7 @@ "Password repeat": "Wachtwoord herhalen", "Password": "Wachtwoord", "Passwords missmatch!": "Wachtwoorden komen niet overeen!", - "Secure": "HTTPS?" + "Secure": "HTTPS?", + "Exclude patterns (one per line)": "Uitsluitpatronen (één per regel)", + "Glob patterns, * wildcard. Lines starting with # are comments. Empty = filter off.": "Glob-patronen, * als wildcard. Regels die beginnen met # zijn commentaar. Leeg = filter uit." } \ No newline at end of file diff --git a/admin/i18n/pl.json b/admin/i18n/pl.json index 6fd04e2..979d9e3 100644 --- a/admin/i18n/pl.json +++ b/admin/i18n/pl.json @@ -4,5 +4,7 @@ "Password repeat": "Powtórz hasło", "Password": "Hasło", "Passwords missmatch!": "Niezgodność haseł!", - "Secure": "HTTPS?" + "Secure": "HTTPS?", + "Exclude patterns (one per line)": "Wzorce wykluczeń (jeden na linię)", + "Glob patterns, * wildcard. Lines starting with # are comments. Empty = filter off.": "Wzorce glob, * jako symbol wieloznaczny. Linie zaczynające się od # to komentarze. Puste = filtr wyłączony." } \ No newline at end of file diff --git a/admin/i18n/pt.json b/admin/i18n/pt.json index 79758db..0ba7e37 100644 --- a/admin/i18n/pt.json +++ b/admin/i18n/pt.json @@ -4,5 +4,7 @@ "Password repeat": "Repetição de senha", "Password": "Senha", "Passwords missmatch!": "As senhas não correspondem!", - "Secure": "HTTPS?" + "Secure": "HTTPS?", + "Exclude patterns (one per line)": "Padrões de exclusão (um por linha)", + "Glob patterns, * wildcard. Lines starting with # are comments. Empty = filter off.": "Padrões glob, * como wildcard. Linhas começando com # são comentários. Vazio = filtro desligado." } \ No newline at end of file diff --git a/admin/i18n/ru.json b/admin/i18n/ru.json index 9fa0e29..25e444a 100644 --- a/admin/i18n/ru.json +++ b/admin/i18n/ru.json @@ -4,5 +4,7 @@ "Password repeat": "Повтор пароля", "Password": "Пароль", "Passwords missmatch!": "Пароли не совпадают!", - "Secure": "HTTPS?" + "Secure": "HTTPS?", + "Exclude patterns (one per line)": "Шаблоны исключения (по одному на строку)", + "Glob patterns, * wildcard. Lines starting with # are comments. Empty = filter off.": "Glob-шаблоны, * — подстановочный знак. Строки, начинающиеся с #, являются комментариями. Пусто = фильтр выключен." } \ No newline at end of file diff --git a/admin/i18n/uk.json b/admin/i18n/uk.json index ca2bca5..28ad24f 100644 --- a/admin/i18n/uk.json +++ b/admin/i18n/uk.json @@ -4,5 +4,7 @@ "Password repeat": "Повторення пароля", "Password": "Пароль", "Passwords missmatch!": "Паролі не збігаються!", - "Secure": "HTTPS?" + "Secure": "HTTPS?", + "Exclude patterns (one per line)": "Шаблони виключення (по одному в рядку)", + "Glob patterns, * wildcard. Lines starting with # are comments. Empty = filter off.": "Glob-шаблони, * — підстановочний знак. Рядки, що починаються з #, є коментарями. Порожньо = фільтр вимкнено." } \ No newline at end of file diff --git a/admin/i18n/zh-cn.json b/admin/i18n/zh-cn.json index 61b553f..2f36947 100644 --- a/admin/i18n/zh-cn.json +++ b/admin/i18n/zh-cn.json @@ -4,5 +4,7 @@ "Password repeat": "密码重复:", "Password": "密码:", "Passwords missmatch!": "密码不匹配!", - "Secure": "HTTPS?" + "Secure": "HTTPS?", + "Exclude patterns (one per line)": "排除模式(每行一个)", + "Glob patterns, * wildcard. Lines starting with # are comments. Empty = filter off.": "Glob 模式,* 通配符。以 # 开头的行是注释。为空 = 过滤器关闭。" } \ No newline at end of file diff --git a/admin/jsonConfig.json b/admin/jsonConfig.json index 3e39ad4..fe90cc8 100644 --- a/admin/jsonConfig.json +++ b/admin/jsonConfig.json @@ -39,6 +39,17 @@ "sm": 12, "md": 6, "lg": 4 + }, + "_divider3": { + "type": "divider" + }, + "excludePatterns": { + "type": "text", + "label": "Exclude patterns (one per line)", + "help": "Glob patterns, * wildcard. Lines starting with # are comments. Empty = filter off.", + "multiline": true, + "minRows": 6, + "sm": 12 } } } From fc7efd47689d59228bb400d04fc770a463c871bb Mon Sep 17 00:00:00 2001 From: mokusone <14061880+mokusone@users.noreply.github.com> Date: Thu, 14 May 2026 11:40:28 +0200 Subject: [PATCH 4/9] feat(filter): add verbose filter logging option New `verboseFilterLog` boolean (default `false`). When enabled, the initial sync logs every excluded entity_id individually on INFO level. This is useful when iterating on exclude patterns or verifying that the filter catches the intended entities. The verbose output is intentionally limited to the first sync. Subsequent debouncedSync() calls only emit the aggregate count to avoid log spam during normal operation. Live `state_changed` events for filtered entities remain on DEBUG. Admin UI gets a checkbox below the patterns field; i18n keys added for all 11 supported languages. --- admin/i18n/de.json | 4 +++- admin/i18n/en.json | 4 +++- admin/i18n/es.json | 4 +++- admin/i18n/fr.json | 4 +++- admin/i18n/it.json | 4 +++- admin/i18n/nl.json | 4 +++- admin/i18n/pl.json | 4 +++- admin/i18n/pt.json | 4 +++- admin/i18n/ru.json | 4 +++- admin/i18n/uk.json | 4 +++- admin/i18n/zh-cn.json | 4 +++- admin/jsonConfig.json | 6 ++++++ io-package.json | 3 ++- src/main.ts | 14 ++++++++++++++ 14 files changed, 55 insertions(+), 12 deletions(-) diff --git a/admin/i18n/de.json b/admin/i18n/de.json index 33f7a2b..f0b008d 100644 --- a/admin/i18n/de.json +++ b/admin/i18n/de.json @@ -6,5 +6,7 @@ "Passwords missmatch!": "Passwörter stimmen nicht überein!", "Secure": "HTTPS?", "Exclude patterns (one per line)": "Filter-Patterns (eines pro Zeile)", - "Glob patterns, * wildcard. Lines starting with # are comments. Empty = filter off.": "Glob-Patterns mit * als Wildcard. Zeilen mit # sind Kommentare. Leer = Filter aus." + "Glob patterns, * wildcard. Lines starting with # are comments. Empty = filter off.": "Glob-Patterns mit * als Wildcard. Zeilen mit # sind Kommentare. Leer = Filter aus.", + "Verbose filter logging": "Ausführliches Filter-Logging", + "When enabled, log each excluded entity_id individually during the initial adapter sync (requires loglevel info or debug).": "Wenn aktiv: jede ausgeschlossene entity_id wird beim initialen Adapter-Sync einzeln geloggt (benötigt Loglevel info oder debug)." } \ No newline at end of file diff --git a/admin/i18n/en.json b/admin/i18n/en.json index 2a3aede..307dd24 100644 --- a/admin/i18n/en.json +++ b/admin/i18n/en.json @@ -6,5 +6,7 @@ "Passwords missmatch!": "Passwords missmatch!", "Secure": "HTTPS?", "Exclude patterns (one per line)": "Exclude patterns (one per line)", - "Glob patterns, * wildcard. Lines starting with # are comments. Empty = filter off.": "Glob patterns, * wildcard. Lines starting with # are comments. Empty = filter off." + "Glob patterns, * wildcard. Lines starting with # are comments. Empty = filter off.": "Glob patterns, * wildcard. Lines starting with # are comments. Empty = filter off.", + "Verbose filter logging": "Verbose filter logging", + "When enabled, log each excluded entity_id individually during the initial adapter sync (requires loglevel info or debug).": "When enabled, log each excluded entity_id individually during the initial adapter sync (requires loglevel info or debug)." } \ No newline at end of file diff --git a/admin/i18n/es.json b/admin/i18n/es.json index 379fff8..712e053 100644 --- a/admin/i18n/es.json +++ b/admin/i18n/es.json @@ -6,5 +6,7 @@ "Passwords missmatch!": "¡Las contraseñas no coinciden!", "Secure": "¿HTTPS?", "Exclude patterns (one per line)": "Patrones de exclusión (uno por línea)", - "Glob patterns, * wildcard. Lines starting with # are comments. Empty = filter off.": "Patrones glob, * como comodín. Las líneas que empiezan con # son comentarios. Vacío = filtro desactivado." + "Glob patterns, * wildcard. Lines starting with # are comments. Empty = filter off.": "Patrones glob, * como comodín. Las líneas que empiezan con # son comentarios. Vacío = filtro desactivado.", + "Verbose filter logging": "Registro detallado del filtro", + "When enabled, log each excluded entity_id individually during the initial adapter sync (requires loglevel info or debug).": "Cuando está activado, cada entity_id excluido se registra individualmente durante la sincronización inicial del adaptador (requiere loglevel info o debug)." } \ No newline at end of file diff --git a/admin/i18n/fr.json b/admin/i18n/fr.json index 02f9bb9..9a042de 100644 --- a/admin/i18n/fr.json +++ b/admin/i18n/fr.json @@ -6,5 +6,7 @@ "Passwords missmatch!": "Les mots de passe ne correspondent pas !", "Secure": "HTTPS ?", "Exclude patterns (one per line)": "Modèles d'exclusion (un par ligne)", - "Glob patterns, * wildcard. Lines starting with # are comments. Empty = filter off.": "Modèles glob, * comme joker. Les lignes commençant par # sont des commentaires. Vide = filtre désactivé." + "Glob patterns, * wildcard. Lines starting with # are comments. Empty = filter off.": "Modèles glob, * comme joker. Les lignes commençant par # sont des commentaires. Vide = filtre désactivé.", + "Verbose filter logging": "Journalisation détaillée du filtre", + "When enabled, log each excluded entity_id individually during the initial adapter sync (requires loglevel info or debug).": "Lorsqu'activé, chaque entity_id exclu est journalisé individuellement pendant la synchronisation initiale de l'adaptateur (nécessite loglevel info ou debug)." } \ No newline at end of file diff --git a/admin/i18n/it.json b/admin/i18n/it.json index 58c43bd..d8c812b 100644 --- a/admin/i18n/it.json +++ b/admin/i18n/it.json @@ -6,5 +6,7 @@ "Passwords missmatch!": "Le password non corrispondono!", "Secure": "HTTPS?", "Exclude patterns (one per line)": "Pattern di esclusione (uno per riga)", - "Glob patterns, * wildcard. Lines starting with # are comments. Empty = filter off.": "Pattern glob, * come wildcard. Le righe che iniziano con # sono commenti. Vuoto = filtro disattivato." + "Glob patterns, * wildcard. Lines starting with # are comments. Empty = filter off.": "Pattern glob, * come wildcard. Le righe che iniziano con # sono commenti. Vuoto = filtro disattivato.", + "Verbose filter logging": "Logging dettagliato del filtro", + "When enabled, log each excluded entity_id individually during the initial adapter sync (requires loglevel info or debug).": "Se attivato, registra ogni entity_id escluso individualmente durante la sincronizzazione iniziale dell'adattatore (richiede loglevel info o debug)." } \ No newline at end of file diff --git a/admin/i18n/nl.json b/admin/i18n/nl.json index b2c9f0c..8c69b91 100644 --- a/admin/i18n/nl.json +++ b/admin/i18n/nl.json @@ -6,5 +6,7 @@ "Passwords missmatch!": "Wachtwoorden komen niet overeen!", "Secure": "HTTPS?", "Exclude patterns (one per line)": "Uitsluitpatronen (één per regel)", - "Glob patterns, * wildcard. Lines starting with # are comments. Empty = filter off.": "Glob-patronen, * als wildcard. Regels die beginnen met # zijn commentaar. Leeg = filter uit." + "Glob patterns, * wildcard. Lines starting with # are comments. Empty = filter off.": "Glob-patronen, * als wildcard. Regels die beginnen met # zijn commentaar. Leeg = filter uit.", + "Verbose filter logging": "Uitgebreide filterlogging", + "When enabled, log each excluded entity_id individually during the initial adapter sync (requires loglevel info or debug).": "Indien ingeschakeld, wordt elke uitgesloten entity_id afzonderlijk gelogd tijdens de initiële adapter-synchronisatie (vereist loglevel info of debug)." } \ No newline at end of file diff --git a/admin/i18n/pl.json b/admin/i18n/pl.json index 979d9e3..5c79d78 100644 --- a/admin/i18n/pl.json +++ b/admin/i18n/pl.json @@ -6,5 +6,7 @@ "Passwords missmatch!": "Niezgodność haseł!", "Secure": "HTTPS?", "Exclude patterns (one per line)": "Wzorce wykluczeń (jeden na linię)", - "Glob patterns, * wildcard. Lines starting with # are comments. Empty = filter off.": "Wzorce glob, * jako symbol wieloznaczny. Linie zaczynające się od # to komentarze. Puste = filtr wyłączony." + "Glob patterns, * wildcard. Lines starting with # are comments. Empty = filter off.": "Wzorce glob, * jako symbol wieloznaczny. Linie zaczynające się od # to komentarze. Puste = filtr wyłączony.", + "Verbose filter logging": "Szczegółowe logowanie filtra", + "When enabled, log each excluded entity_id individually during the initial adapter sync (requires loglevel info or debug).": "Gdy włączone, każde wykluczone entity_id jest logowane osobno podczas początkowej synchronizacji adaptera (wymaga poziomu logowania info lub debug)." } \ No newline at end of file diff --git a/admin/i18n/pt.json b/admin/i18n/pt.json index 0ba7e37..8c67662 100644 --- a/admin/i18n/pt.json +++ b/admin/i18n/pt.json @@ -6,5 +6,7 @@ "Passwords missmatch!": "As senhas não correspondem!", "Secure": "HTTPS?", "Exclude patterns (one per line)": "Padrões de exclusão (um por linha)", - "Glob patterns, * wildcard. Lines starting with # are comments. Empty = filter off.": "Padrões glob, * como wildcard. Linhas começando com # são comentários. Vazio = filtro desligado." + "Glob patterns, * wildcard. Lines starting with # are comments. Empty = filter off.": "Padrões glob, * como wildcard. Linhas começando com # são comentários. Vazio = filtro desligado.", + "Verbose filter logging": "Registro detalhado do filtro", + "When enabled, log each excluded entity_id individually during the initial adapter sync (requires loglevel info or debug).": "Quando ativado, cada entity_id excluído é registrado individualmente durante a sincronização inicial do adaptador (requer loglevel info ou debug)." } \ No newline at end of file diff --git a/admin/i18n/ru.json b/admin/i18n/ru.json index 25e444a..7f3f987 100644 --- a/admin/i18n/ru.json +++ b/admin/i18n/ru.json @@ -6,5 +6,7 @@ "Passwords missmatch!": "Пароли не совпадают!", "Secure": "HTTPS?", "Exclude patterns (one per line)": "Шаблоны исключения (по одному на строку)", - "Glob patterns, * wildcard. Lines starting with # are comments. Empty = filter off.": "Glob-шаблоны, * — подстановочный знак. Строки, начинающиеся с #, являются комментариями. Пусто = фильтр выключен." + "Glob patterns, * wildcard. Lines starting with # are comments. Empty = filter off.": "Glob-шаблоны, * — подстановочный знак. Строки, начинающиеся с #, являются комментариями. Пусто = фильтр выключен.", + "Verbose filter logging": "Подробное логирование фильтра", + "When enabled, log each excluded entity_id individually during the initial adapter sync (requires loglevel info or debug).": "Когда включено, каждый исключённый entity_id логируется отдельно во время начальной синхронизации адаптера (требуется loglevel info или debug)." } \ No newline at end of file diff --git a/admin/i18n/uk.json b/admin/i18n/uk.json index 28ad24f..aa15ae0 100644 --- a/admin/i18n/uk.json +++ b/admin/i18n/uk.json @@ -6,5 +6,7 @@ "Passwords missmatch!": "Паролі не збігаються!", "Secure": "HTTPS?", "Exclude patterns (one per line)": "Шаблони виключення (по одному в рядку)", - "Glob patterns, * wildcard. Lines starting with # are comments. Empty = filter off.": "Glob-шаблони, * — підстановочний знак. Рядки, що починаються з #, є коментарями. Порожньо = фільтр вимкнено." + "Glob patterns, * wildcard. Lines starting with # are comments. Empty = filter off.": "Glob-шаблони, * — підстановочний знак. Рядки, що починаються з #, є коментарями. Порожньо = фільтр вимкнено.", + "Verbose filter logging": "Детальне логування фільтра", + "When enabled, log each excluded entity_id individually during the initial adapter sync (requires loglevel info or debug).": "Коли увімкнено, кожен виключений entity_id логується окремо під час початкової синхронізації адаптера (потрібен loglevel info або debug)." } \ No newline at end of file diff --git a/admin/i18n/zh-cn.json b/admin/i18n/zh-cn.json index 2f36947..926adaf 100644 --- a/admin/i18n/zh-cn.json +++ b/admin/i18n/zh-cn.json @@ -6,5 +6,7 @@ "Passwords missmatch!": "密码不匹配!", "Secure": "HTTPS?", "Exclude patterns (one per line)": "排除模式(每行一个)", - "Glob patterns, * wildcard. Lines starting with # are comments. Empty = filter off.": "Glob 模式,* 通配符。以 # 开头的行是注释。为空 = 过滤器关闭。" + "Glob patterns, * wildcard. Lines starting with # are comments. Empty = filter off.": "Glob 模式,* 通配符。以 # 开头的行是注释。为空 = 过滤器关闭。", + "Verbose filter logging": "详细过滤器日志", + "When enabled, log each excluded entity_id individually during the initial adapter sync (requires loglevel info or debug).": "启用后,在适配器初始同步期间单独记录每个被排除的 entity_id(需要日志级别 info 或 debug)。" } \ No newline at end of file diff --git a/admin/jsonConfig.json b/admin/jsonConfig.json index fe90cc8..1355561 100644 --- a/admin/jsonConfig.json +++ b/admin/jsonConfig.json @@ -50,6 +50,12 @@ "multiline": true, "minRows": 6, "sm": 12 + }, + "verboseFilterLog": { + "type": "checkbox", + "label": "Verbose filter logging", + "help": "When enabled, log each excluded entity_id individually during the initial adapter sync (requires loglevel info or debug).", + "sm": 12 } } } diff --git a/io-package.json b/io-package.json index 3684fd7..dd74f9c 100644 --- a/io-package.json +++ b/io-package.json @@ -253,7 +253,8 @@ "port": 8123, "password": "", "secure": false, - "excludePatterns": "# Glob patterns, one per line. Lines starting with # are comments.\n# Example: filter out all entities prefixed with `iob_` regardless of domain:\n# *.iob_*" + "excludePatterns": "# Glob patterns, one per line. Lines starting with # are comments.\n# Example: filter out all entities prefixed with `iob_` regardless of domain:\n# *.iob_*", + "verboseFilterLog": false }, "protectedNative": [ "password" diff --git a/src/main.ts b/src/main.ts index ce09d7b..2826c08 100644 --- a/src/main.ts +++ b/src/main.ts @@ -8,6 +8,7 @@ interface HassAdapterConfig { password: string; secure: boolean; excludePatterns: string; + verboseFilterLog: boolean; } interface HassEntity { @@ -191,6 +192,7 @@ class HassAdapter extends Adapter { private syncDebounceTimeout: ReturnType | null = null; private stopped: boolean = false; private excludePatterns: string[] = []; + private initialSyncCompleted: boolean = false; public constructor(options: Partial = {}) { super({ @@ -458,6 +460,7 @@ class HassAdapter extends Adapter { const states: { id: string; lc?: number; ts?: number; val: ioBroker.StateValue; ack: boolean }[] = []; const expectedObjects = new Set(); let excludedCount = 0; + const excludedIds: string[] = []; for (let e = 0; e < entities.length; e++) { const entity = entities[e]; @@ -467,6 +470,9 @@ class HassAdapter extends Adapter { if (isExcluded(entity.entity_id, this.excludePatterns)) { excludedCount++; + if (this.config.verboseFilterLog && !this.initialSyncCompleted) { + excludedIds.push(entity.entity_id); + } continue; } @@ -667,6 +673,14 @@ class HassAdapter extends Adapter { `Entity filter excluded ${excludedCount} entit${excludedCount === 1 ? 'y' : 'ies'} from sync`, ); } + + if (excludedIds.length > 0) { + for (const id of excludedIds) { + this.log.info(`Entity filter excluded: ${id}`); + } + } + + this.initialSyncCompleted = true; } private async main(): Promise { From 7b5679a753079773dee32d8ce19842d286c49cba Mon Sep 17 00:00:00 2001 From: mokusone <14061880+mokusone@users.noreply.github.com> Date: Thu, 14 May 2026 11:40:28 +0200 Subject: [PATCH 5/9] docs: document entity exclude filter and add changelog entry Adds a new README section explaining the glob syntax, providing examples and covering the verbose-logging toggle. Also adds a WORK IN PROGRESS changelog entry for the next release. --- README.md | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/README.md b/README.md index d0eb1b6..b7cf6fc 100644 --- a/README.md +++ b/README.md @@ -100,12 +100,45 @@ Please check it https://www.smarthomejetzt.de/mit-iobroker-auf-eine-home-assista **Unfortunately only in German, but the [Google Translate works rather good](https://translate.google.com/translate?hl=en&sl=de&tl=en&u=https%3A%2F%2Fwww.smarthomejetzt.de%2Fmit-iobroker-auf-eine-home-assistant-hass-io-installation-und-die-geraete-zugreifen%2F)** +## Entity exclude filter + +Optionally restrict which Home Assistant entities are synchronised into ioBroker. + +Each non-empty, non-comment line in the **Exclude patterns** field is a glob +(only `*` is a wildcard and matches any sequence of characters, including `.`). +Patterns are matched case-sensitively against the full `entity_id` (e.g. +`switch.living_room`). An entity that matches any pattern is: + +- skipped when objects are created or updated (initial sync and re-syncs) +- ignored when its state changes in HASS (no state writes triggered in ioBroker) + +Lines starting with `#` are treated as comments. + +Examples: + +``` +# Drop every entity whose name starts with `iob_`, regardless of domain: +*.iob_* + +# Drop sensors only: +sensor.iob_* +``` + +Tick **Verbose filter logging** to log every excluded `entity_id` individually +during the first sync (requires adapter loglevel `info` or `debug`). Subsequent +re-syncs only emit the aggregate count to keep the log clean. + +An empty pattern list leaves the adapter behaviour identical to previous versions. + ## Changelog +### **WORK IN PROGRESS** +* (mokusone) Added optional entity exclude filter with glob patterns, configurable via the admin UI, plus a verbose-logging toggle for inspecting matches + ### 2.0.4 (2026-05-05) * (@GermanBluefox) Tried to keep the custom settings of the objects when updating them with new data from HASS From a4932a1a8c433a232dd9e35f439d73e1897f54a3 Mon Sep 17 00:00:00 2001 From: mokusone <14061880+mokusone@users.noreply.github.com> Date: Thu, 14 May 2026 22:12:51 +0200 Subject: [PATCH 6/9] fix(admin): remove invalid 'multiline' property from excludePatterns MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The JSON Config schema for type 'text' enforces additionalProperties:false and 'multiline' is not a documented property. Its presence caused the admin UI to fail schema validation and refuse to render the panel. Multi-line behaviour is already provided by 'minRows: 6' alone — the schema explicitly states: "Set this attribute to 2 or more if you want to have a textarea with more than one row." Co-Authored-By: Claude Opus 4.7 (1M context) --- admin/jsonConfig.json | 1 - 1 file changed, 1 deletion(-) diff --git a/admin/jsonConfig.json b/admin/jsonConfig.json index 1355561..c320740 100644 --- a/admin/jsonConfig.json +++ b/admin/jsonConfig.json @@ -47,7 +47,6 @@ "type": "text", "label": "Exclude patterns (one per line)", "help": "Glob patterns, * wildcard. Lines starting with # are comments. Empty = filter off.", - "multiline": true, "minRows": 6, "sm": 12 }, From a40dc58e355ddb03012e8ebaae0eeb9536957462 Mon Sep 17 00:00:00 2001 From: mokusone <14061880+mokusone@users.noreply.github.com> Date: Thu, 14 May 2026 23:34:00 +0200 Subject: [PATCH 7/9] feat(filter): add cleanupExcludedOnStart option to delete stale objects MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When enabled, on each adapter start the adapter scans all existing hass.0.entities.* channels and deletes those whose entity_id matches any of the configured excludePatterns. This complements the existing filter (which only prevents new objects from being created) by also cleaning up objects that were synced before the filter was activated or before the patterns were extended. Safety: - Channels (and any of their sub-states) holding common.custom config (history/influxdb/sql) are kept and a warning is logged so the user can decide manually. - Existing deleteStaleObjects() 50% sanity-guard is intentionally NOT applied here — the user explicitly opted in to the cleanup. - Per-id deletion only logs when verboseFilterLog is also active. Default is false (opt-in). Adds new admin checkbox + i18n keys for all 11 languages (machine translations for non-en/de still pending). --- admin/i18n/de.json | 6 ++- admin/i18n/en.json | 6 ++- admin/i18n/es.json | 6 ++- admin/i18n/fr.json | 14 +++--- admin/i18n/it.json | 6 ++- admin/i18n/nl.json | 6 ++- admin/i18n/pl.json | 6 ++- admin/i18n/pt.json | 6 ++- admin/i18n/ru.json | 6 ++- admin/i18n/uk.json | 6 ++- admin/i18n/zh-cn.json | 6 ++- admin/jsonConfig.json | 6 +++ io-package.json | 3 +- src/main.ts | 102 ++++++++++++++++++++++++++++++++++++++++++ 14 files changed, 158 insertions(+), 27 deletions(-) diff --git a/admin/i18n/de.json b/admin/i18n/de.json index f0b008d..23b0c5f 100644 --- a/admin/i18n/de.json +++ b/admin/i18n/de.json @@ -8,5 +8,7 @@ "Exclude patterns (one per line)": "Filter-Patterns (eines pro Zeile)", "Glob patterns, * wildcard. Lines starting with # are comments. Empty = filter off.": "Glob-Patterns mit * als Wildcard. Zeilen mit # sind Kommentare. Leer = Filter aus.", "Verbose filter logging": "Ausführliches Filter-Logging", - "When enabled, log each excluded entity_id individually during the initial adapter sync (requires loglevel info or debug).": "Wenn aktiv: jede ausgeschlossene entity_id wird beim initialen Adapter-Sync einzeln geloggt (benötigt Loglevel info oder debug)." -} \ No newline at end of file + "When enabled, log each excluded entity_id individually during the initial adapter sync (requires loglevel info or debug).": "Wenn aktiv: jede ausgeschlossene entity_id wird beim initialen Adapter-Sync einzeln geloggt (benötigt Loglevel info oder debug).", + "Cleanup excluded entities on adapter start": "Ausgefilterte Entitäten beim Adapter-Start löschen", + "When enabled, on each adapter start delete existing objects under entities.* whose entity_id matches the exclude patterns. Objects with custom adapter config (history/influxdb/sql) are kept and logged.": "Wenn aktiv: bei jedem Adapter-Start werden vorhandene Objekte unter entities.*, deren entity_id auf die Filter-Patterns matched, gelöscht. Objekte mit Custom-Adapter-Konfiguration (history/influxdb/sql) bleiben erhalten und werden geloggt." +} diff --git a/admin/i18n/en.json b/admin/i18n/en.json index 307dd24..8724b5b 100644 --- a/admin/i18n/en.json +++ b/admin/i18n/en.json @@ -8,5 +8,7 @@ "Exclude patterns (one per line)": "Exclude patterns (one per line)", "Glob patterns, * wildcard. Lines starting with # are comments. Empty = filter off.": "Glob patterns, * wildcard. Lines starting with # are comments. Empty = filter off.", "Verbose filter logging": "Verbose filter logging", - "When enabled, log each excluded entity_id individually during the initial adapter sync (requires loglevel info or debug).": "When enabled, log each excluded entity_id individually during the initial adapter sync (requires loglevel info or debug)." -} \ No newline at end of file + "When enabled, log each excluded entity_id individually during the initial adapter sync (requires loglevel info or debug).": "When enabled, log each excluded entity_id individually during the initial adapter sync (requires loglevel info or debug).", + "Cleanup excluded entities on adapter start": "Cleanup excluded entities on adapter start", + "When enabled, on each adapter start delete existing objects under entities.* whose entity_id matches the exclude patterns. Objects with custom adapter config (history/influxdb/sql) are kept and logged.": "When enabled, on each adapter start delete existing objects under entities.* whose entity_id matches the exclude patterns. Objects with custom adapter config (history/influxdb/sql) are kept and logged." +} diff --git a/admin/i18n/es.json b/admin/i18n/es.json index 712e053..c02c44e 100644 --- a/admin/i18n/es.json +++ b/admin/i18n/es.json @@ -8,5 +8,7 @@ "Exclude patterns (one per line)": "Patrones de exclusión (uno por línea)", "Glob patterns, * wildcard. Lines starting with # are comments. Empty = filter off.": "Patrones glob, * como comodín. Las líneas que empiezan con # son comentarios. Vacío = filtro desactivado.", "Verbose filter logging": "Registro detallado del filtro", - "When enabled, log each excluded entity_id individually during the initial adapter sync (requires loglevel info or debug).": "Cuando está activado, cada entity_id excluido se registra individualmente durante la sincronización inicial del adaptador (requiere loglevel info o debug)." -} \ No newline at end of file + "When enabled, log each excluded entity_id individually during the initial adapter sync (requires loglevel info or debug).": "Cuando está activado, cada entity_id excluido se registra individualmente durante la sincronización inicial del adaptador (requiere loglevel info o debug).", + "Cleanup excluded entities on adapter start": "Cleanup excluded entities on adapter start", + "When enabled, on each adapter start delete existing objects under entities.* whose entity_id matches the exclude patterns. Objects with custom adapter config (history/influxdb/sql) are kept and logged.": "When enabled, on each adapter start delete existing objects under entities.* whose entity_id matches the exclude patterns. Objects with custom adapter config (history/influxdb/sql) are kept and logged." +} diff --git a/admin/i18n/fr.json b/admin/i18n/fr.json index 9a042de..efe89e3 100644 --- a/admin/i18n/fr.json +++ b/admin/i18n/fr.json @@ -1,12 +1,14 @@ { - "Home assistant IP": "IP de l'assistant à domicile ", - "Home assistant WS Port": "Port WS de l'assistant domestique ", - "Password repeat": "Répéter le mot de passe ", + "Home assistant IP": "IP de l'assistant à domicile ", + "Home assistant WS Port": "Port WS de l'assistant domestique ", + "Password repeat": "Répéter le mot de passe ", "Password": "Mot de passe", - "Passwords missmatch!": "Les mots de passe ne correspondent pas !", + "Passwords missmatch!": "Les mots de passe ne correspondent pas !", "Secure": "HTTPS ?", "Exclude patterns (one per line)": "Modèles d'exclusion (un par ligne)", "Glob patterns, * wildcard. Lines starting with # are comments. Empty = filter off.": "Modèles glob, * comme joker. Les lignes commençant par # sont des commentaires. Vide = filtre désactivé.", "Verbose filter logging": "Journalisation détaillée du filtre", - "When enabled, log each excluded entity_id individually during the initial adapter sync (requires loglevel info or debug).": "Lorsqu'activé, chaque entity_id exclu est journalisé individuellement pendant la synchronisation initiale de l'adaptateur (nécessite loglevel info ou debug)." -} \ No newline at end of file + "When enabled, log each excluded entity_id individually during the initial adapter sync (requires loglevel info or debug).": "Lorsqu'activé, chaque entity_id exclu est journalisé individuellement pendant la synchronisation initiale de l'adaptateur (nécessite loglevel info ou debug).", + "Cleanup excluded entities on adapter start": "Cleanup excluded entities on adapter start", + "When enabled, on each adapter start delete existing objects under entities.* whose entity_id matches the exclude patterns. Objects with custom adapter config (history/influxdb/sql) are kept and logged.": "When enabled, on each adapter start delete existing objects under entities.* whose entity_id matches the exclude patterns. Objects with custom adapter config (history/influxdb/sql) are kept and logged." +} diff --git a/admin/i18n/it.json b/admin/i18n/it.json index d8c812b..0bc1bf3 100644 --- a/admin/i18n/it.json +++ b/admin/i18n/it.json @@ -8,5 +8,7 @@ "Exclude patterns (one per line)": "Pattern di esclusione (uno per riga)", "Glob patterns, * wildcard. Lines starting with # are comments. Empty = filter off.": "Pattern glob, * come wildcard. Le righe che iniziano con # sono commenti. Vuoto = filtro disattivato.", "Verbose filter logging": "Logging dettagliato del filtro", - "When enabled, log each excluded entity_id individually during the initial adapter sync (requires loglevel info or debug).": "Se attivato, registra ogni entity_id escluso individualmente durante la sincronizzazione iniziale dell'adattatore (richiede loglevel info o debug)." -} \ No newline at end of file + "When enabled, log each excluded entity_id individually during the initial adapter sync (requires loglevel info or debug).": "Se attivato, registra ogni entity_id escluso individualmente durante la sincronizzazione iniziale dell'adattatore (richiede loglevel info o debug).", + "Cleanup excluded entities on adapter start": "Cleanup excluded entities on adapter start", + "When enabled, on each adapter start delete existing objects under entities.* whose entity_id matches the exclude patterns. Objects with custom adapter config (history/influxdb/sql) are kept and logged.": "When enabled, on each adapter start delete existing objects under entities.* whose entity_id matches the exclude patterns. Objects with custom adapter config (history/influxdb/sql) are kept and logged." +} diff --git a/admin/i18n/nl.json b/admin/i18n/nl.json index 8c69b91..2fa44e1 100644 --- a/admin/i18n/nl.json +++ b/admin/i18n/nl.json @@ -8,5 +8,7 @@ "Exclude patterns (one per line)": "Uitsluitpatronen (één per regel)", "Glob patterns, * wildcard. Lines starting with # are comments. Empty = filter off.": "Glob-patronen, * als wildcard. Regels die beginnen met # zijn commentaar. Leeg = filter uit.", "Verbose filter logging": "Uitgebreide filterlogging", - "When enabled, log each excluded entity_id individually during the initial adapter sync (requires loglevel info or debug).": "Indien ingeschakeld, wordt elke uitgesloten entity_id afzonderlijk gelogd tijdens de initiële adapter-synchronisatie (vereist loglevel info of debug)." -} \ No newline at end of file + "When enabled, log each excluded entity_id individually during the initial adapter sync (requires loglevel info or debug).": "Indien ingeschakeld, wordt elke uitgesloten entity_id afzonderlijk gelogd tijdens de initiële adapter-synchronisatie (vereist loglevel info of debug).", + "Cleanup excluded entities on adapter start": "Cleanup excluded entities on adapter start", + "When enabled, on each adapter start delete existing objects under entities.* whose entity_id matches the exclude patterns. Objects with custom adapter config (history/influxdb/sql) are kept and logged.": "When enabled, on each adapter start delete existing objects under entities.* whose entity_id matches the exclude patterns. Objects with custom adapter config (history/influxdb/sql) are kept and logged." +} diff --git a/admin/i18n/pl.json b/admin/i18n/pl.json index 5c79d78..dba6e9d 100644 --- a/admin/i18n/pl.json +++ b/admin/i18n/pl.json @@ -8,5 +8,7 @@ "Exclude patterns (one per line)": "Wzorce wykluczeń (jeden na linię)", "Glob patterns, * wildcard. Lines starting with # are comments. Empty = filter off.": "Wzorce glob, * jako symbol wieloznaczny. Linie zaczynające się od # to komentarze. Puste = filtr wyłączony.", "Verbose filter logging": "Szczegółowe logowanie filtra", - "When enabled, log each excluded entity_id individually during the initial adapter sync (requires loglevel info or debug).": "Gdy włączone, każde wykluczone entity_id jest logowane osobno podczas początkowej synchronizacji adaptera (wymaga poziomu logowania info lub debug)." -} \ No newline at end of file + "When enabled, log each excluded entity_id individually during the initial adapter sync (requires loglevel info or debug).": "Gdy włączone, każde wykluczone entity_id jest logowane osobno podczas początkowej synchronizacji adaptera (wymaga poziomu logowania info lub debug).", + "Cleanup excluded entities on adapter start": "Cleanup excluded entities on adapter start", + "When enabled, on each adapter start delete existing objects under entities.* whose entity_id matches the exclude patterns. Objects with custom adapter config (history/influxdb/sql) are kept and logged.": "When enabled, on each adapter start delete existing objects under entities.* whose entity_id matches the exclude patterns. Objects with custom adapter config (history/influxdb/sql) are kept and logged." +} diff --git a/admin/i18n/pt.json b/admin/i18n/pt.json index 8c67662..c693c1a 100644 --- a/admin/i18n/pt.json +++ b/admin/i18n/pt.json @@ -8,5 +8,7 @@ "Exclude patterns (one per line)": "Padrões de exclusão (um por linha)", "Glob patterns, * wildcard. Lines starting with # are comments. Empty = filter off.": "Padrões glob, * como wildcard. Linhas começando com # são comentários. Vazio = filtro desligado.", "Verbose filter logging": "Registro detalhado do filtro", - "When enabled, log each excluded entity_id individually during the initial adapter sync (requires loglevel info or debug).": "Quando ativado, cada entity_id excluído é registrado individualmente durante a sincronização inicial do adaptador (requer loglevel info ou debug)." -} \ No newline at end of file + "When enabled, log each excluded entity_id individually during the initial adapter sync (requires loglevel info or debug).": "Quando ativado, cada entity_id excluído é registrado individualmente durante a sincronização inicial do adaptador (requer loglevel info ou debug).", + "Cleanup excluded entities on adapter start": "Cleanup excluded entities on adapter start", + "When enabled, on each adapter start delete existing objects under entities.* whose entity_id matches the exclude patterns. Objects with custom adapter config (history/influxdb/sql) are kept and logged.": "When enabled, on each adapter start delete existing objects under entities.* whose entity_id matches the exclude patterns. Objects with custom adapter config (history/influxdb/sql) are kept and logged." +} diff --git a/admin/i18n/ru.json b/admin/i18n/ru.json index 7f3f987..b4a9107 100644 --- a/admin/i18n/ru.json +++ b/admin/i18n/ru.json @@ -8,5 +8,7 @@ "Exclude patterns (one per line)": "Шаблоны исключения (по одному на строку)", "Glob patterns, * wildcard. Lines starting with # are comments. Empty = filter off.": "Glob-шаблоны, * — подстановочный знак. Строки, начинающиеся с #, являются комментариями. Пусто = фильтр выключен.", "Verbose filter logging": "Подробное логирование фильтра", - "When enabled, log each excluded entity_id individually during the initial adapter sync (requires loglevel info or debug).": "Когда включено, каждый исключённый entity_id логируется отдельно во время начальной синхронизации адаптера (требуется loglevel info или debug)." -} \ No newline at end of file + "When enabled, log each excluded entity_id individually during the initial adapter sync (requires loglevel info or debug).": "Когда включено, каждый исключённый entity_id логируется отдельно во время начальной синхронизации адаптера (требуется loglevel info или debug).", + "Cleanup excluded entities on adapter start": "Cleanup excluded entities on adapter start", + "When enabled, on each adapter start delete existing objects under entities.* whose entity_id matches the exclude patterns. Objects with custom adapter config (history/influxdb/sql) are kept and logged.": "When enabled, on each adapter start delete existing objects under entities.* whose entity_id matches the exclude patterns. Objects with custom adapter config (history/influxdb/sql) are kept and logged." +} diff --git a/admin/i18n/uk.json b/admin/i18n/uk.json index aa15ae0..ecc21c0 100644 --- a/admin/i18n/uk.json +++ b/admin/i18n/uk.json @@ -8,5 +8,7 @@ "Exclude patterns (one per line)": "Шаблони виключення (по одному в рядку)", "Glob patterns, * wildcard. Lines starting with # are comments. Empty = filter off.": "Glob-шаблони, * — підстановочний знак. Рядки, що починаються з #, є коментарями. Порожньо = фільтр вимкнено.", "Verbose filter logging": "Детальне логування фільтра", - "When enabled, log each excluded entity_id individually during the initial adapter sync (requires loglevel info or debug).": "Коли увімкнено, кожен виключений entity_id логується окремо під час початкової синхронізації адаптера (потрібен loglevel info або debug)." -} \ No newline at end of file + "When enabled, log each excluded entity_id individually during the initial adapter sync (requires loglevel info or debug).": "Коли увімкнено, кожен виключений entity_id логується окремо під час початкової синхронізації адаптера (потрібен loglevel info або debug).", + "Cleanup excluded entities on adapter start": "Cleanup excluded entities on adapter start", + "When enabled, on each adapter start delete existing objects under entities.* whose entity_id matches the exclude patterns. Objects with custom adapter config (history/influxdb/sql) are kept and logged.": "When enabled, on each adapter start delete existing objects under entities.* whose entity_id matches the exclude patterns. Objects with custom adapter config (history/influxdb/sql) are kept and logged." +} diff --git a/admin/i18n/zh-cn.json b/admin/i18n/zh-cn.json index 926adaf..cb0785f 100644 --- a/admin/i18n/zh-cn.json +++ b/admin/i18n/zh-cn.json @@ -8,5 +8,7 @@ "Exclude patterns (one per line)": "排除模式(每行一个)", "Glob patterns, * wildcard. Lines starting with # are comments. Empty = filter off.": "Glob 模式,* 通配符。以 # 开头的行是注释。为空 = 过滤器关闭。", "Verbose filter logging": "详细过滤器日志", - "When enabled, log each excluded entity_id individually during the initial adapter sync (requires loglevel info or debug).": "启用后,在适配器初始同步期间单独记录每个被排除的 entity_id(需要日志级别 info 或 debug)。" -} \ No newline at end of file + "When enabled, log each excluded entity_id individually during the initial adapter sync (requires loglevel info or debug).": "启用后,在适配器初始同步期间单独记录每个被排除的 entity_id(需要日志级别 info 或 debug)。", + "Cleanup excluded entities on adapter start": "Cleanup excluded entities on adapter start", + "When enabled, on each adapter start delete existing objects under entities.* whose entity_id matches the exclude patterns. Objects with custom adapter config (history/influxdb/sql) are kept and logged.": "When enabled, on each adapter start delete existing objects under entities.* whose entity_id matches the exclude patterns. Objects with custom adapter config (history/influxdb/sql) are kept and logged." +} diff --git a/admin/jsonConfig.json b/admin/jsonConfig.json index c320740..89b49a3 100644 --- a/admin/jsonConfig.json +++ b/admin/jsonConfig.json @@ -55,6 +55,12 @@ "label": "Verbose filter logging", "help": "When enabled, log each excluded entity_id individually during the initial adapter sync (requires loglevel info or debug).", "sm": 12 + }, + "cleanupExcludedOnStart": { + "type": "checkbox", + "label": "Cleanup excluded entities on adapter start", + "help": "When enabled, on each adapter start delete existing objects under entities.* whose entity_id matches the exclude patterns. Objects with custom adapter config (history/influxdb/sql) are kept and logged.", + "sm": 12 } } } diff --git a/io-package.json b/io-package.json index dd74f9c..938c86c 100644 --- a/io-package.json +++ b/io-package.json @@ -254,7 +254,8 @@ "password": "", "secure": false, "excludePatterns": "# Glob patterns, one per line. Lines starting with # are comments.\n# Example: filter out all entities prefixed with `iob_` regardless of domain:\n# *.iob_*", - "verboseFilterLog": false + "verboseFilterLog": false, + "cleanupExcludedOnStart": false }, "protectedNative": [ "password" diff --git a/src/main.ts b/src/main.ts index 2826c08..875dbba 100644 --- a/src/main.ts +++ b/src/main.ts @@ -9,6 +9,7 @@ interface HassAdapterConfig { secure: boolean; excludePatterns: string; verboseFilterLog: boolean; + cleanupExcludedOnStart: boolean; } interface HassEntity { @@ -683,6 +684,105 @@ class HassAdapter extends Adapter { this.initialSyncCompleted = true; } + private async cleanupExcludedObjects(): Promise { + if (!this.config.cleanupExcludedOnStart) { + return; + } + if (this.excludePatterns.length === 0) { + this.log.info('Cleanup skipped: no exclude patterns configured'); + return; + } + + let allObjects: Record; + try { + allObjects = (await this.getAdapterObjectsAsync()) as Record; + } catch (err) { + this.log.error(`Cleanup: failed to load adapter objects: ${err}`); + return; + } + + const prefix = `${this.namespace}.entities.`; + const channelsToDelete: { id: string; entityId: 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') { + continue; + } + const entityId = (obj.native as Record)?.entity_id; + if (typeof entityId !== 'string') { + continue; + } + if (isExcluded(entityId, this.excludePatterns)) { + channelsToDelete.push({ id, entityId }); + } + } + + if (channelsToDelete.length === 0) { + this.log.info('Cleanup: no existing objects matched exclude patterns'); + return; + } + + let deletedCount = 0; + let keptForCustomCount = 0; + + for (const { id, entityId } of channelsToDelete) { + const channelCustom = (allObjects[id].common as { custom?: Record } | 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 } | undefined + )?.custom; + if (subCustom && Object.keys(subCustom).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`, + ); + 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]; + } + } + deletedCount++; + if (this.config.verboseFilterLog) { + this.log.info(`Cleanup: deleted "${id}" (entity ${entityId})`); + } + } catch (err) { + this.log.error(`Cleanup: failed to delete "${id}": ${err}`); + } + } + + this.log.info( + `Cleanup: deleted ${deletedCount} excluded entit${deletedCount === 1 ? 'y' : 'ies'}` + + (keptForCustomCount > 0 + ? `, kept ${keptForCustomCount} with custom config (see warnings above)` + : ''), + ); + } + private async main(): Promise { this.config.host ||= '127.0.0.1'; this.config.port = parseInt(String(this.config.port), 10) || 8123; @@ -701,6 +801,8 @@ class HassAdapter extends Adapter { ); } + await this.cleanupExcludedObjects(); + await this.setStateAsync('info.connection', false, true); this.hass = new HASS(this.config, this.log); From 0512ee7cd3d76474b06bcab52cb99d99ce16a936 Mon Sep 17 00:00:00 2001 From: mokusone <14061880+mokusone@users.noreply.github.com> Date: Fri, 15 May 2026 00:08:07 +0200 Subject: [PATCH 8/9] fix(filter): cleanup must work for flat-state entities (no parent channel) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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... 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. --- src/main.ts | 92 +++++++++++++++++++++++++++++------------------------ 1 file changed, 50 insertions(+), 42 deletions(-) diff --git a/src/main.ts b/src/main.ts index 875dbba..06899cd 100644 --- a/src/main.ts +++ b/src/main.ts @@ -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(); 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)?.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 } | 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 } | 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 } | 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)` : ''), ); } From 7dd63fa9c8692cfe9211d1da119faebd0d9f3eed Mon Sep 17 00:00:00 2001 From: Bluefox Date: Sat, 16 May 2026 19:49:12 +0200 Subject: [PATCH 9/9] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/main.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.ts b/src/main.ts index a502ca7..94910d5 100644 --- a/src/main.ts +++ b/src/main.ts @@ -797,7 +797,7 @@ class HassAdapter extends Adapter { const rawPatterns = (this.config.excludePatterns || '').toString(); this.excludePatterns = rawPatterns - .split('\n') + .split(/\r?\n/) .map(s => s.trim()) .filter(line => line.length > 0 && !line.startsWith('#'));