feat(filter): integrate exclude filter into parseStates and state_changed

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.
This commit is contained in:
mokusone
2026-05-14 11:40:28 +02:00
parent 9b95ece284
commit 6889664c28
2 changed files with 36 additions and 1 deletions
+2 -1
View File
@@ -252,7 +252,8 @@
"host": "127.0.0.1", "host": "127.0.0.1",
"port": 8123, "port": 8123,
"password": "", "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": [ "protectedNative": [
"password" "password"
+34
View File
@@ -1,11 +1,13 @@
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;
} }
interface HassEntity { interface HassEntity {
@@ -188,6 +190,7 @@ 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[] = [];
public constructor(options: Partial<AdapterOptions> = {}) { public constructor(options: Partial<AdapterOptions> = {}) {
super({ super({
@@ -454,6 +457,7 @@ 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;
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 +465,11 @@ class HassAdapter extends Adapter {
continue; continue;
} }
if (isExcluded(entity.entity_id, this.excludePatterns)) {
excludedCount++;
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 +661,32 @@ 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`,
);
}
} }
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('\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); 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 +699,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;