Skip to content
This repository has been archived by the owner on Nov 7, 2024. It is now read-only.

Commit

Permalink
feat: support include or exclude by label or integration #210 #109
Browse files Browse the repository at this point in the history
  • Loading branch information
t0bst4r committed Jul 24, 2024
1 parent 4b5db0e commit ea20c38
Show file tree
Hide file tree
Showing 6 changed files with 112 additions and 32 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,20 @@ import {
} from '@/models/index.js';

export function buildState(
entityIds: string[],
states: HomeAssistantEntities,
entityRegistry: HomeAssistantEntityRegistry,
): HomeAssistantMatterEntities {
return Object.fromEntries(
Object.entries(states).map(([entityId, state]) => [entityId, combine(state, entityRegistry[entityId])]),
entityIds.map((entityId) => [entityId, combine(states[entityId], entityRegistry[entityId])]),
);
}

function combine(
state: HomeAssistantEntity,
registry: HomeAssistantEntityRegistryEntry | undefined,
): HomeAssistantMatterEntity {
function combine(state: HomeAssistantEntity, registry?: HomeAssistantEntityRegistryEntry): HomeAssistantMatterEntity {
return {
...state,
hidden: !!registry?.hidden_by,
platform: registry?.platform ?? 'unknown',
labels: registry?.labels ?? [],
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,17 +56,19 @@ export class HomeAssistantClient {

private async init(): Promise<void> {
const entityIds: string[] = [];
const entities = (await getStates(this.connection)).filter((entity) =>
this.patternMatcher.isIncluded(entity.entity_id),
);
const entities = await getStates(this.connection);
entities.forEach((entity) => entityIds.push(entity.entity_id));
this.entityStates = Object.fromEntries(entities.map((e) => [e.entity_id, e]));
this.log.debug('%s entities included', entityIds.length);

this.entityRegistry = await getRegistry(this.connection);
this.log.debug('Registry refreshed');

await this.update();
this.entities = Object.fromEntries(
Object.entries(buildState(entityIds, this.entityStates, this.entityRegistry)).filter(([, entity]) =>
this.patternMatcher.isIncluded(entity),
),
);
this.log.debug('State updated');

this.close = subscribeEntities(
Expand All @@ -80,7 +82,7 @@ export class HomeAssistantClient {
}

private async expensiveUpdate() {
this.entities = buildState(this.entityStates, this.entityRegistry);
this.entities = buildState(Object.keys(this.entities), this.entityStates, this.entityRegistry);
for (const subscriber of this.subscribers) {
await subscriber(this.entities);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,44 +1,87 @@
import globToRegExp from 'glob-to-regexp';

import { logger } from '@/logging/index.js';
import { HomeAssistantMatterEntity } from '@/models/index.js';

export interface PatternMatcherConfig {
readonly includeDomains?: Array<string>;
readonly includePatterns?: Array<string>;
readonly excludeDomains?: Array<string>;
readonly excludePatterns?: Array<string>;
readonly includeDomains?: string[];
readonly excludeDomains?: string[];

readonly includePatterns?: string[];
readonly excludePatterns?: string[];

readonly includePlatforms?: string[];
readonly excludePlatforms?: string[];

readonly includeLabels?: string[];
readonly excludeLabels?: string[];
}

export class PatternMatcher {
private readonly log = logger.child({ service: 'PatternMatcher' });

private readonly includeDomains: Array<string>;
private readonly includeDomains: string[];
private readonly excludeDomains: string[];

private readonly includePatterns: RegExp[];
private readonly excludeDomains: Array<string>;
private readonly excludePatterns: RegExp[];

private readonly includePlatforms: string[];
private readonly excludePlatforms: string[];

private readonly includeLabels: string[];
private readonly excludeLabels: string[];

constructor(config: PatternMatcherConfig) {
this.includeDomains = (config.includeDomains ?? []).map((domain) => `${domain}.`);
this.includePatterns = config.includePatterns?.map((pattern) => globToRegExp(pattern)) ?? [];
this.excludeDomains = (config.excludeDomains ?? []).map((domain) => `${domain}.`);

this.includePatterns = config.includePatterns?.map((pattern) => globToRegExp(pattern)) ?? [];
this.excludePatterns = config.excludePatterns?.map((pattern) => globToRegExp(pattern)) ?? [];

this.includePlatforms = config.includePlatforms ?? [];
this.excludePlatforms = config.excludePlatforms ?? [];

this.includeLabels = config.includeLabels ?? [];
this.excludeLabels = config.excludeLabels ?? [];
}

public isIncluded(entityId: string): boolean {
const domainIncluded = this.includeDomains.some((domain) => entityId.startsWith(domain));
const patternIncluded = this.includePatterns.some((pattern) => pattern.test(entityId));
const included =
this.includeDomains.length + this.includePatterns.length === 0 || domainIncluded || patternIncluded;
const domainExcluded = this.excludeDomains.some((domain) => entityId.startsWith(domain));
const patternExcluded = this.excludePatterns.some((pattern) => pattern.test(entityId));
const excluded = domainExcluded || patternExcluded;
public isIncluded(entity: HomeAssistantMatterEntity): boolean {
const included = this.checkIncluded(entity);
const excluded = this.checkExcluded(entity);
if (excluded) {
this.log.debug('%s is excluded', entityId);
this.log.debug('%s is excluded', entity.entity_id);
} else if (!included) {
this.log.debug('%s is not included', entityId);
this.log.debug('%s is not included', entity.entity_id);
} else {
this.log.debug('%s is included', entityId);
this.log.debug('%s is included', entity.entity_id);
}
return included && !excluded;
}

private checkIncluded(entity: HomeAssistantMatterEntity): boolean {
const domainIncluded = this.includeDomains.some((domain) => entity.entity_id.startsWith(domain));
const patternIncluded = this.includePatterns.some((pattern) => pattern.test(entity.entity_id));
const platformIncluded = this.includePlatforms.includes(entity.platform);
const labelsIncluded = this.includeLabels.some((label) => entity.labels.includes(label));
return (
this.includeDomains.length +
this.includePatterns.length +
this.includePlatforms.length +
this.includeLabels.length ===
0 ||
domainIncluded ||
patternIncluded ||
platformIncluded ||
labelsIncluded
);
}

private checkExcluded(entity: HomeAssistantMatterEntity) {
const domainExcluded = this.excludeDomains.some((domain) => entity.entity_id.startsWith(domain));
const patternExcluded = this.excludePatterns.some((pattern) => pattern.test(entity.entity_id));
const platformExcluded = this.excludePlatforms.includes(entity.platform);
const labelsExcluded = this.excludeLabels.some((label) => entity.labels.includes(label));
return domainExcluded || patternExcluded || platformExcluded || labelsExcluded;
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,26 @@
export interface HomeAssistantEntityRegistryEntry {
entity_id: string;
area_id?: string;
categories: Record<string, unknown>;
config_entry_id?: unknown;
device_id?: string;
disabled_by?: string;
hidden_by?: string;
disabled_by?: unknown;
entity_category?: unknown;
entity_id: string;
has_entity_name: boolean;
hidden_by?: unknown;
icon?: unknown;
id: string;
labels?: string[];
name?: string;
options?: {
conversation?: {
should_expose?: boolean;
};
};
original_name: string;
platform: string;
translation_key?: unknown;
unique_id: string;
}

export type HomeAssistantEntityRegistry = Record<string, HomeAssistantEntityRegistryEntry>;
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { HomeAssistantEntity } from './home-assistant-entity.js';

export interface HomeAssistantMatterEntity extends HomeAssistantEntity {
hidden: boolean;
labels: string[];
platform: string;
}

export type HomeAssistantMatterEntities = Record<string, HomeAssistantMatterEntity>;
22 changes: 21 additions & 1 deletion packages/matterbridge-home-assistant/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,14 +125,34 @@ configuration in it. See [config structure](#config-structure).
"includePatterns": [
"media_player.samsung_tv_*"
],
// optional: include all entities having one of these labels.
// It is important to use the slug of the label. When your label is "My Devices", the slug is most probably "my_devices".
"includeLabels": [
"My Devices"
],
// optional: include all entities having one of the following platforms (= integration)
// It is important to use the slug of the platform / integration.
"includePlatform": [
"hue"
],
// optional: exclude all entities of these domains:
"excludeDomains": [
"lock",
],
// optional: exclude all entities matching these entity_id patterns:
"excludePatterns": [
"media_player.*echo*"
]
],
// optional: exclude all entities having one of these labels.
// It is important to use the slug of the label. When your label is "My Devices", the slug is most probably "my_devices".
"excludeLabels": [
"My Devices"
],
// optional: exclude all entities having one of the following platforms (= integration)
// It is important to use the slug of the platform / integration.
"excludePlatform": [
"hue"
],
}
}
}
Expand Down

0 comments on commit ea20c38

Please sign in to comment.