Merge pull request #177 from mokusone/upstream-pr-entity-filter

Add optional entity exclude filter with verbose logging
This commit is contained in:
Bluefox
2026-05-16 19:50:10 +02:00
committed by GitHub
18 changed files with 469 additions and 113 deletions
+32
View File
@@ -100,6 +100,36 @@ 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)** **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.
<!-- <!--
Placeholder for the next version (at the beginning of the line): Placeholder for the next version (at the beginning of the line):
### **WORK IN PROGRESS** ### **WORK IN PROGRESS**
@@ -107,9 +137,11 @@ Please check it https://www.smarthomejetzt.de/mit-iobroker-auf-eine-home-assista
## Changelog ## Changelog
### **WORK IN PROGRESS** ### **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
* (@klein0r) Use /core/ instead of /api/ when connecting to supervisor directly (e.g. in ha app) * (@klein0r) Use /core/ instead of /api/ when connecting to supervisor directly (e.g. in ha app)
* (@klein0r) Use ENV var SUPERVISOR_TOKEN as fallback for password * (@klein0r) Use ENV var SUPERVISOR_TOKEN as fallback for password
### 2.0.4 (2026-05-05) ### 2.0.4 (2026-05-05)
* (@GermanBluefox) Tried to keep the custom settings of the objects when updating them with new data from HASS * (@GermanBluefox) Tried to keep the custom settings of the objects when updating them with new data from HASS
+8 -2
View File
@@ -4,5 +4,11 @@
"Password repeat": "Passwort wiederholen", "Password repeat": "Passwort wiederholen",
"Password": "Passwort", "Password": "Passwort",
"Passwords missmatch!": "Passwörter stimmen nicht überein!", "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.",
"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).",
"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."
}
+8 -2
View File
@@ -4,5 +4,11 @@
"Password repeat": "Password repeat", "Password repeat": "Password repeat",
"Password": "Password", "Password": "Password",
"Passwords missmatch!": "Passwords missmatch!", "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.",
"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).",
"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."
}
+8 -2
View File
@@ -4,5 +4,11 @@
"Password repeat": "Repite la contraseña", "Password repeat": "Repite la contraseña",
"Password": "Clave", "Password": "Clave",
"Passwords missmatch!": "¡Las contraseñas no coinciden!", "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.",
"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).",
"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."
}
+12 -6
View File
@@ -1,8 +1,14 @@
{ {
"Home assistant IP": "IP de l'assistant à domicile ", "Home assistant IP": "IP de l'assistant à domicile ",
"Home assistant WS Port": "Port WS de l'assistant domestique ", "Home assistant WS Port": "Port WS de l'assistant domestique ",
"Password repeat": "Répéter le mot de passe ", "Password repeat": "Répéter le mot de passe ",
"Password": "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 ?" "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).",
"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."
}
+8 -2
View File
@@ -4,5 +4,11 @@
"Password repeat": "Ripeti password", "Password repeat": "Ripeti password",
"Password": "Parola d'ordine", "Password": "Parola d'ordine",
"Passwords missmatch!": "Le password non corrispondono!", "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.",
"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).",
"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."
}
+8 -2
View File
@@ -4,5 +4,11 @@
"Password repeat": "Wachtwoord herhalen", "Password repeat": "Wachtwoord herhalen",
"Password": "Wachtwoord", "Password": "Wachtwoord",
"Passwords missmatch!": "Wachtwoorden komen niet overeen!", "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.",
"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).",
"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."
}
+8 -2
View File
@@ -4,5 +4,11 @@
"Password repeat": "Powtórz hasło", "Password repeat": "Powtórz hasło",
"Password": "Hasło", "Password": "Hasło",
"Passwords missmatch!": "Niezgodność haseł!", "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.",
"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).",
"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."
}
+8 -2
View File
@@ -4,5 +4,11 @@
"Password repeat": "Repetição de senha", "Password repeat": "Repetição de senha",
"Password": "Senha", "Password": "Senha",
"Passwords missmatch!": "As senhas não correspondem!", "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.",
"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).",
"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."
}
+8 -2
View File
@@ -4,5 +4,11 @@
"Password repeat": "Повтор пароля", "Password repeat": "Повтор пароля",
"Password": "Пароль", "Password": "Пароль",
"Passwords missmatch!": "Пароли не совпадают!", "Passwords missmatch!": "Пароли не совпадают!",
"Secure": "HTTPS?" "Secure": "HTTPS?",
} "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).",
"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."
}
+8 -2
View File
@@ -4,5 +4,11 @@
"Password repeat": "Повторення пароля", "Password repeat": "Повторення пароля",
"Password": "Пароль", "Password": "Пароль",
"Passwords missmatch!": "Паролі не збігаються!", "Passwords missmatch!": "Паролі не збігаються!",
"Secure": "HTTPS?" "Secure": "HTTPS?",
} "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).",
"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."
}
+8 -2
View File
@@ -4,5 +4,11 @@
"Password repeat": "密码重复:", "Password repeat": "密码重复:",
"Password": "密码:", "Password": "密码:",
"Passwords missmatch!": "密码不匹配!", "Passwords missmatch!": "密码不匹配!",
"Secure": "HTTPS" "Secure": "HTTPS",
} "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)。",
"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."
}
+22
View File
@@ -39,6 +39,28 @@
"sm": 12, "sm": 12,
"md": 6, "md": 6,
"lg": 4 "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.",
"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
},
"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
} }
} }
} }
+27 -18
View File
@@ -231,24 +231,33 @@
"buttons": ["agree", "cancel"] "buttons": ["agree", "cancel"]
} }
] ]
}, }
"native": { ]
"host": "127.0.0.1", },
"port": 8123, "native": {
"password": "", "host": "127.0.0.1",
"secure": false "port": 8123,
}, "password": "",
"protectedNative": ["password"], "secure": false,
"encryptedNative": ["password"], "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_*",
"objects": [], "verboseFilterLog": false,
"instanceObjects": [ "cleanupExcludedOnStart": false
{ },
"_id": "info", "protectedNative": [
"type": "channel", "password"
"common": { ],
"name": "Information" "encryptedNative": [
}, "password"
"native": {} ],
"objects": [],
"instanceObjects": [
{
"_id": "info",
"type": "channel",
"common": {
"name": "Information"
},
"native": {}
}, },
{ {
"_id": "info.connection", "_id": "info.connection",
+66 -69
View File
@@ -1,71 +1,68 @@
{ {
"name": "iobroker.hass", "name": "iobroker.hass",
"version": "2.0.4", "version": "2.0.4",
"description": "Home Assistant", "description": "Home Assistant",
"author": { "author": {
"name": "bluefox", "name": "bluefox",
"email": "dogafox@gmail.com" "email": "dogafox@gmail.com"
}, },
"engines": { "contributors": [
"node": ">=20" {
}, "name": "bluefox",
"contributors": [ "email": "dogafox@gmail.com"
{ }
"name": "bluefox", ],
"email": "dogafox@gmail.com" "homepage": "https://github.com/ioBroker/ioBroker.hass",
} "license": "MIT",
], "keywords": [
"homepage": "https://github.com/ioBroker/ioBroker.hass", "ioBroker",
"license": "MIT", "hass",
"keywords": [ "REST API",
"ioBroker", "IoT"
"hass", ],
"REST API", "repository": {
"IoT" "type": "git",
], "url": "https://github.com/ioBroker/ioBroker.hass"
"repository": { },
"type": "git", "dependencies": {
"url": "https://github.com/ioBroker/ioBroker.hass" "ws": "^8.20.0",
}, "@iobroker/adapter-core": "^3.3.2"
"dependencies": { },
"ws": "^8.20.1", "devDependencies": {
"@iobroker/adapter-core": "^3.3.2" "@alcalzone/release-script": "^5.1.1",
}, "@alcalzone/release-script-plugin-iobroker": "^5.1.2",
"devDependencies": { "@alcalzone/release-script-plugin-license": "^5.1.1",
"@alcalzone/release-script": "^5.2.0", "@iobroker/adapter-dev": "^1.5.0",
"@alcalzone/release-script-plugin-iobroker": "^5.2.0", "@iobroker/build-tools": "^3.0.1",
"@alcalzone/release-script-plugin-license": "^5.2.0", "@iobroker/eslint-config": "^2.2.0",
"@iobroker/adapter-dev": "^1.5.0", "@iobroker/legacy-testing": "^2.0.2",
"@iobroker/build-tools": "^3.0.1", "@iobroker/testing": "^5.2.2",
"@iobroker/eslint-config": "^2.3.4", "@types/ws": "^8.18.1"
"@iobroker/legacy-testing": "^2.0.2", },
"@iobroker/testing": "^5.2.2", "main": "build/main.js",
"@types/ws": "^8.18.1", "files": [
"typescript": "^6.0.3" "admin{,/!(src)/**}/!(tsconfig|tsconfig.*|.eslintrc).{json,json5}",
}, "admin{,/!(src)/**}/*.{html,css,png,svg,jpg,js}",
"main": "build/main.js", "build/",
"files": [ "io-package.json",
"admin{,/!(src)/**}/!(tsconfig|tsconfig.*|.eslintrc).{json,json5}", "LICENSE"
"admin{,/!(src)/**}/*.{html,css,png,svg,jpg,js}", ],
"build/", "scripts": {
"io-package.json", "test:integration": "mocha --exit",
"LICENSE" "test:package": "mocha test/testPackageFiles.js --exit",
], "test:unit": "mocha test/testEntityFilter.js --exit",
"scripts": { "test": "npm run test:integration",
"test:integration": "mocha --exit", "build:tsc": "tsc -p tsconfig.build.json",
"test:package": "mocha test/testPackageFiles.js --exit", "build": "npm run build:tsc",
"test": "npm run test:integration", "lint": "eslint -c eslint.config.mjs",
"build:tsc": "tsc -p tsconfig.build.json", "release": "release-script",
"build": "npm run build:tsc", "release-patch": "release-script patch --yes",
"lint": "eslint -c eslint.config.mjs", "release-minor": "release-script minor --yes",
"release": "release-script", "release-major": "release-script major --yes",
"release-patch": "release-script patch --yes", "translate": "translate-adapter"
"release-minor": "release-script minor --yes", },
"release-major": "release-script major --yes", "bugs": {
"translate": "translate-adapter" "url": "https://github.com/ioBroker/ioBroker.hass/issues"
}, },
"bugs": { "readmeFilename": "README.md"
"url": "https://github.com/ioBroker/ioBroker.hass/issues"
},
"readmeFilename": "README.md"
} }
+25
View File
@@ -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}$`);
}
+158
View File
@@ -1,11 +1,15 @@
import { Adapter, type AdapterOptions } from '@iobroker/adapter-core'; import { Adapter, type AdapterOptions } from '@iobroker/adapter-core';
import HASS from './lib/hass'; import HASS from './lib/hass';
import { isExcluded } from './lib/entityFilter';
interface HassAdapterConfig { interface HassAdapterConfig {
host: string; host: string;
port: number; port: number;
password: string; password: string;
secure: boolean; secure: boolean;
excludePatterns: string;
verboseFilterLog: boolean;
cleanupExcludedOnStart: boolean;
} }
interface HassEntity { interface HassEntity {
@@ -188,6 +192,8 @@ class HassAdapter extends Adapter {
private delayTimeout: ReturnType<typeof setTimeout> | null = null; private delayTimeout: ReturnType<typeof setTimeout> | null = null;
private syncDebounceTimeout: ReturnType<typeof setTimeout> | null = null; private syncDebounceTimeout: ReturnType<typeof setTimeout> | null = null;
private stopped: boolean = false; private stopped: boolean = false;
private excludePatterns: string[] = [];
private initialSyncCompleted: boolean = false;
public constructor(options: Partial<AdapterOptions> = {}) { public constructor(options: Partial<AdapterOptions> = {}) {
super({ super({
@@ -454,6 +460,8 @@ class HassAdapter extends Adapter {
const objs: (ioBroker.ChannelObject | ioBroker.StateObject)[] = []; const objs: (ioBroker.ChannelObject | ioBroker.StateObject)[] = [];
const states: { id: string; lc?: number; ts?: number; val: ioBroker.StateValue; ack: boolean }[] = []; const states: { id: string; lc?: number; ts?: number; val: ioBroker.StateValue; ack: boolean }[] = [];
const expectedObjects = new Set<string>(); const expectedObjects = new Set<string>();
let excludedCount = 0;
const excludedIds: string[] = [];
for (let e = 0; e < entities.length; e++) { for (let e = 0; e < entities.length; e++) {
const entity = entities[e]; const entity = entities[e];
@@ -461,6 +469,14 @@ class HassAdapter extends Adapter {
continue; continue;
} }
if (isExcluded(entity.entity_id, this.excludePatterns)) {
excludedCount++;
if (this.config.verboseFilterLog && !this.initialSyncCompleted) {
excludedIds.push(entity.entity_id);
}
continue;
}
const name = entity.name || entity.attributes?.friendly_name || entity.entity_id; const name = entity.name || entity.attributes?.friendly_name || entity.entity_id;
const desc = entity.attributes?.attribution || undefined; const desc = entity.attributes?.attribution || undefined;
@@ -652,12 +668,149 @@ class HassAdapter extends Adapter {
} }
this.log.info(`Synchronization completed: ${changes.join(', ')}`); this.log.info(`Synchronization completed: ${changes.join(', ')}`);
} }
if (excludedCount > 0) {
this.log.info(
`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 cleanupExcludedObjects(): Promise<void> {
if (!this.config.cleanupExcludedOnStart) {
return;
}
if (this.excludePatterns.length === 0) {
this.log.info('Cleanup skipped: no exclude patterns configured');
return;
}
let allObjects: Record<string, ioBroker.Object>;
try {
allObjects = (await this.getAdapterObjectsAsync()) as Record<string, ioBroker.Object>;
} catch (err) {
this.log.error(`Cleanup: failed to load adapter objects: ${err}`);
return;
}
const prefix = `${this.namespace}.entities.`;
// 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 rest = id.substring(prefix.length);
const parts = rest.split('.');
if (parts.length < 2) {
continue;
}
const entityId = `${parts[0]}.${parts[1]}`;
if (!isExcluded(entityId, this.excludePatterns)) {
continue;
}
const ids = matchedByEntity.get(entityId);
if (ids) {
ids.push(id);
} else {
matchedByEntity.set(entityId, [id]);
}
}
if (matchedByEntity.size === 0) {
this.log.info('Cleanup: no existing objects matched exclude patterns');
return;
}
let deletedEntityCount = 0;
let deletedIdCount = 0;
let keptForCustomCount = 0;
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 entity "${entityId}" — has custom adapter config (history/influxdb/sql); remove it manually if you really want to drop it`,
);
continue;
}
// 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}`);
}
}
if (entityFullyDeleted) {
deletedEntityCount++;
if (this.config.verboseFilterLog) {
this.log.info(
`Cleanup: deleted entity "${entityId}" (${ids.length} object${ids.length === 1 ? '' : 's'})`,
);
}
}
}
this.log.info(
`Cleanup: deleted ${deletedEntityCount} excluded entit${deletedEntityCount === 1 ? 'y' : 'ies'} (${deletedIdCount} object${deletedIdCount === 1 ? '' : 's'} total)` +
(keptForCustomCount > 0
? `, kept ${keptForCustomCount} entit${keptForCustomCount === 1 ? 'y' : 'ies'} with custom config (see warnings above)`
: ''),
);
} }
private async main(): Promise<void> { private async main(): Promise<void> {
this.config.host ||= '127.0.0.1'; this.config.host ||= '127.0.0.1';
this.config.port = parseInt(String(this.config.port), 10) || 8123; this.config.port = parseInt(String(this.config.port), 10) || 8123;
const rawPatterns = (this.config.excludePatterns || '').toString();
this.excludePatterns = rawPatterns
.split(/\r?\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.cleanupExcludedObjects();
await this.setStateAsync('info.connection', false, true); await this.setStateAsync('info.connection', false, true);
this.hass = new HASS(this.config, this.log); this.hass = new HASS(this.config, this.log);
@@ -670,6 +823,11 @@ class HassAdapter extends Adapter {
return; 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 id = `entities.${entity.entity_id}.`;
const lc = entity.last_changed ? new Date(entity.last_changed).getTime() : undefined; const lc = entity.last_changed ? new Date(entity.last_changed).getTime() : undefined;
const ts = entity.last_updated ? new Date(entity.last_updated).getTime() : undefined; const ts = entity.last_updated ? new Date(entity.last_updated).getTime() : undefined;
+47
View File
@@ -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);
});
});