diff --git a/packages/backend/.env.sample b/.env.sample similarity index 76% rename from packages/backend/.env.sample rename to .env.sample index e741c6f..6d04efc 100644 --- a/packages/backend/.env.sample +++ b/.env.sample @@ -1,5 +1,4 @@ -NODE_ENV=development HAMH_HOME_ASSISTANT_URL="http://192.168.178.111:8123/" HAMH_HOME_ASSISTANT_ACCESS_TOKEN="" HAMH_STORAGE_LOCATION=$PWD/.local-storage -HAMH_LOG_LEVEL=info \ No newline at end of file +HAMH_LOG_LEVEL=debug \ No newline at end of file diff --git a/apps/docker/package.json b/apps/docker/package.json index 17585a2..211950f 100644 --- a/apps/docker/package.json +++ b/apps/docker/package.json @@ -4,6 +4,7 @@ "private": true, "type": "module", "scripts": { + "cleanup": "rimraf package.tgz", "build": "./build.sh", "retrieve-package": "node retrieve-package.js", "nx-release-publish": "./build.sh --latest --push --all-platforms" diff --git a/apps/home-assistant-matter-hub/.env.sample b/apps/home-assistant-matter-hub/.env.sample deleted file mode 100644 index 87a08ef..0000000 --- a/apps/home-assistant-matter-hub/.env.sample +++ /dev/null @@ -1,3 +0,0 @@ -HAMH_HOME_ASSISTANT_URL="http://192.168.178.111:8123/" -HAMH_HOME_ASSISTANT_ACCESS_TOKEN="long-lived-access-token" -HAMH_STORAGE_LOCATION=$PWD/.local-storage diff --git a/apps/home-assistant-matter-hub/package.json b/apps/home-assistant-matter-hub/package.json index c9f8bb8..13c0978 100644 --- a/apps/home-assistant-matter-hub/package.json +++ b/apps/home-assistant-matter-hub/package.json @@ -41,16 +41,17 @@ "license": "Apache-2.0", "repository": "github:t0bst4r/home-assistant-matter-hub", "scripts": { + "cleanup": "rimraf dist pack", "lint": "eslint .", "lint:fix": "eslint . --fix", "build": "node build.js", "test": "vitest run", - "start": "dotenvx run -- ./dist/backend/cli.js start --log-level=info", + "start": "dotenvx run -f ../../.env -- ./dist/backend/cli.js start", "pack": "mkdir -p pack && npm pack --pack-destination pack --json | jq -r .[0].filename > pack/package-name.txt" }, "dependencies": { - "@project-chip/matter.js": "~0.10.6", - "@project-chip/matter-node.js": "~0.10.6", + "@project-chip/matter.js": "~0.11.2", + "@project-chip/matter-node.js": "~0.11.2", "ajv": "^8.17.1", "chalk": "^5.3.0", "color": "^4.2.3", diff --git a/package-lock.json b/package-lock.json index 3767ac9..f549bce 100644 --- a/package-lock.json +++ b/package-lock.json @@ -50,8 +50,8 @@ "version": "3.0.0-alpha.17", "license": "Apache-2.0", "dependencies": { - "@project-chip/matter-node.js": "~0.10.6", - "@project-chip/matter.js": "~0.10.6", + "@project-chip/matter-node.js": "~0.11.2", + "@project-chip/matter.js": "~0.11.2", "ajv": "^8.17.1", "chalk": "^5.3.0", "color": "^4.2.3", @@ -3242,6 +3242,71 @@ "@lezer/lr": "^1.4.0" } }, + "node_modules/@matter/general": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/@matter/general/-/general-0.11.2.tgz", + "integrity": "sha512-todBWZgYfxJK1SU6d7Foczwixzd0/5CT1LFQM2n0AgAjNGPAEfPBAnSE4uJjTrag+Subu5PalEXXFP8JeteAag==", + "dependencies": { + "@noble/curves": "^1.5.0" + } + }, + "node_modules/@matter/model": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/@matter/model/-/model-0.11.2.tgz", + "integrity": "sha512-W52AhOF5Xqss9AfOUfqpD7IzK/q9LUGmZKY2CV0ztEO8YpJt2jVsCCOI/XrUeUPZJ94WgT8IyoRG1if4SVtRfw==", + "dependencies": { + "@matter/general": "0.11.2", + "@noble/curves": "^1.5.0" + } + }, + "node_modules/@matter/node": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/@matter/node/-/node-0.11.2.tgz", + "integrity": "sha512-OBYciIvsMZbq4bGeN1jCGFvfdOZJurcxAWbk1B7gJ0Ufmhm+JQ7/3H42pcGNdRc0jl2iE7ElNGnfBRzZBJsmDQ==", + "dependencies": { + "@matter/general": "0.11.2", + "@matter/model": "0.11.2", + "@matter/protocol": "0.11.2", + "@matter/types": "0.11.2", + "@noble/curves": "^1.5.0" + } + }, + "node_modules/@matter/nodejs": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/@matter/nodejs/-/nodejs-0.11.2.tgz", + "integrity": "sha512-Kwnm3sHXvKj4X0z9JF1J3PNQ2G+fse2MHwSfMZ2sD/l6iSuWzQ1ICpOuQ7HnJ+IhKbeVJTjYOtp4HCXhOhZgPA==", + "dependencies": { + "@matter/general": "0.11.2", + "@matter/node": "0.11.2", + "@matter/protocol": "0.11.2", + "@matter/types": "0.11.2", + "node-localstorage": "^3.0.5" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@matter/protocol": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/@matter/protocol/-/protocol-0.11.2.tgz", + "integrity": "sha512-5SbpwlIwVGGRWpHbYWrsedkCIx7A4Rtf5q+W33NfX+qbwQO8po4C27a5xeKtRPJt8ZsGpGdZPJV+oC0UQlK9gg==", + "dependencies": { + "@matter/general": "0.11.2", + "@matter/model": "0.11.2", + "@matter/types": "0.11.2", + "@noble/curves": "^1.5.0" + } + }, + "node_modules/@matter/types": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/@matter/types/-/types-0.11.2.tgz", + "integrity": "sha512-cO0oAXeiQxGbyJNozD9xeT3x7teS+D6FguuefFKLNBBR2HTEsXQe3+tHZe75ROrXO9qzzUbKoxFSpLzYpIjWLw==", + "dependencies": { + "@matter/general": "0.11.2", + "@matter/model": "0.11.2", + "@noble/curves": "^1.5.0" + } + }, "node_modules/@mui/base": { "version": "5.0.0-beta.58", "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.58.tgz", @@ -4009,22 +4074,28 @@ } }, "node_modules/@project-chip/matter-node.js": { - "version": "0.10.6", - "resolved": "https://registry.npmjs.org/@project-chip/matter-node.js/-/matter-node.js-0.10.6.tgz", - "integrity": "sha512-i0Yy0dyFiB02IxO91gBRUsr/K9qpMEAed3yFwEvm8QNCEMXh9cKyB15A0ItQS73xFSHCDJJwSMekdd1itcaIew==", + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/@project-chip/matter-node.js/-/matter-node.js-0.11.2.tgz", + "integrity": "sha512-g4LuDPPU70MEQO7Fe9tVXPC3lyOIaKVnkIeDLWAflMYdSzWoYsQYTKhvhEsbDGyGYru580UQhGm5gOo8q0E1bg==", "dependencies": { - "@project-chip/matter.js": "0.10.6", - "node-localstorage": "^3.0.5" + "@matter/general": "0.11.2", + "@matter/nodejs": "0.11.2", + "@project-chip/matter.js": "0.11.2" }, "engines": { "node": ">=18.0.0" } }, "node_modules/@project-chip/matter.js": { - "version": "0.10.6", - "resolved": "https://registry.npmjs.org/@project-chip/matter.js/-/matter.js-0.10.6.tgz", - "integrity": "sha512-N7VKjciD4nLE/YBoQ7melMiN11scKtCawLdLHLGcKw+UGvUkbHmpdRp0YQkUR9g3G4cWRVMPaBkOkCpk/MBVaA==", - "dependencies": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/@project-chip/matter.js/-/matter.js-0.11.2.tgz", + "integrity": "sha512-tG4dJptRLktketVGc/mmhje5Nwbkw/8uuM+sUKiiy13J0dmy8y6jX3vjOf7/zS+6YmG/F8oGcbpTycnjUmhi8g==", + "dependencies": { + "@matter/general": "0.11.2", + "@matter/model": "0.11.2", + "@matter/node": "0.11.2", + "@matter/protocol": "0.11.2", + "@matter/types": "0.11.2", "@noble/curves": "^1.5.0" } }, @@ -5315,7 +5386,7 @@ "version": "22.5.5", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.5.tgz", "integrity": "sha512-Xjs4y5UPO/CLdzpgR6GirZJx36yScjh73+2NlLlkFRSoQN8B0DpfXPdZGnvVmLRLOsqDpOfTNv7D9trgGhmOIA==", - "dev": true, + "devOptional": true, "dependencies": { "undici-types": "~6.19.2" } @@ -12899,7 +12970,7 @@ "version": "5.6.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", - "dev": true, + "devOptional": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -14054,8 +14125,8 @@ "version": "3.0.0-alpha.17", "dependencies": { "@home-assistant-matter-hub/common": "*", - "@project-chip/matter-node.js": "~0.10.6", - "@project-chip/matter.js": "~0.10.6", + "@project-chip/matter-node.js": "~0.11.2", + "@project-chip/matter.js": "~0.11.2", "ajv": "^8.17.1", "chalk": "^5.3.0", "express": "^4.21.1", diff --git a/package.json b/package.json index c2f4571..0bcb302 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "private": true, "type": "module", "scripts": { + "cleanup": "nx run-many -t cleanup --parallel 100 && nx reset && git clean -fx", "lint": "nx run-many -t lint && prettier . --check", "lint:fix": "nx run-many -t lint:fix && prettier . --write", "test": "nx run-many -t test", diff --git a/packages/backend/.env.development b/packages/backend/.env.development new file mode 100644 index 0000000..885be01 --- /dev/null +++ b/packages/backend/.env.development @@ -0,0 +1 @@ + NODE_ENV=development \ No newline at end of file diff --git a/packages/backend/package.json b/packages/backend/package.json index 89bf703..b1d1ffb 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -5,15 +5,16 @@ "type": "module", "destination": "./dist", "scripts": { + "cleanup": "rimraf dist", "lint": "eslint .", "lint:fix": "eslint . --fix", "build": "tsc && node bundle.js", - "serve": "tsx watch --env-file=.env --clear-screen=false src/cli.ts start" + "serve": "tsx watch --env-file=../../.env --env-file=.env.development --clear-screen=false src/cli.ts start" }, "dependencies": { "@home-assistant-matter-hub/common": "*", - "@project-chip/matter.js": "~0.10.6", - "@project-chip/matter-node.js": "~0.10.6", + "@project-chip/matter.js": "~0.11.2", + "@project-chip/matter-node.js": "~0.11.2", "ajv": "^8.17.1", "chalk": "^5.3.0", "express": "^4.21.1", diff --git a/packages/backend/src/home-assistant/api/get-registry.ts b/packages/backend/src/home-assistant/api/get-registry.ts index e83db29..314800f 100644 --- a/packages/backend/src/home-assistant/api/get-registry.ts +++ b/packages/backend/src/home-assistant/api/get-registry.ts @@ -1,26 +1,10 @@ -import { - HomeAssistantEntityRegistry, - HomeAssistantEntityRegistryWithInitialState, - HomeAssistantEntityState, -} from "@home-assistant-matter-hub/common"; -import { Connection, getStates } from "home-assistant-js-websocket"; -import _, { Dictionary } from "lodash"; +import { HomeAssistantEntityRegistry } from "@home-assistant-matter-hub/common"; +import { Connection } from "home-assistant-js-websocket"; export async function getRegistry( connection: Connection, -): Promise> { - const registry = _.keyBy( - await connection.sendMessagePromise({ - type: "config/entity_registry/list", - }), - (r) => r.entity_id, - ); - const states: Dictionary = _.keyBy( - await getStates(connection), - (e) => e.entity_id, - ); - return _.mapValues(registry, (r) => ({ - ...r, - initialState: states[r.entity_id], - })); +): Promise { + return connection.sendMessagePromise({ + type: "config/entity_registry/list", + }); } diff --git a/packages/backend/src/home-assistant/subscribe-entities.ts b/packages/backend/src/home-assistant/api/subscribe-entities.ts similarity index 100% rename from packages/backend/src/home-assistant/subscribe-entities.ts rename to packages/backend/src/home-assistant/api/subscribe-entities.ts diff --git a/packages/backend/src/home-assistant/home-assistant-client.ts b/packages/backend/src/home-assistant/home-assistant-client.ts index 127f083..99ded93 100644 --- a/packages/backend/src/home-assistant/home-assistant-client.ts +++ b/packages/backend/src/home-assistant/home-assistant-client.ts @@ -3,10 +3,11 @@ import { Connection, createConnection, createLongLivedTokenAuth, + getStates, UnsubscribeFunc, } from "home-assistant-js-websocket"; import { - HomeAssistantEntityRegistryWithInitialState, + HomeAssistantEntityRegistry, HomeAssistantEntityState, HomeAssistantFilter, } from "@home-assistant-matter-hub/common"; @@ -15,11 +16,12 @@ import { ServiceBase } from "../utils/service.js"; import _, { Dictionary } from "lodash"; import { Subject } from "rxjs"; import crypto from "node:crypto"; -import { subscribeEntities } from "./subscribe-entities.js"; +import { subscribeEntities } from "./api/subscribe-entities.js"; import { matchEntityFilter } from "./match-entity-filter.js"; import { getRegistry } from "./api/get-registry.js"; import { HomeAssistantActions } from "./home-assistant-actions.js"; import { HassServiceTarget } from "home-assistant-js-websocket/dist/types.js"; +import { isValidEntity } from "./is-valid-entity.js"; export interface HomeAssistantClientProps { readonly url: string; @@ -38,7 +40,9 @@ export class HomeAssistantClient Dictionary >(); private connection?: Connection; - private _registry?: Dictionary; + + private _initialStates?: Dictionary; + private _registry?: Dictionary; private subscriptions: Record = {}; private unsubscribeState?: UnsubscribeFunc; @@ -55,37 +59,14 @@ export class HomeAssistantClient this.connection = await createConnection({ auth: createLongLivedTokenAuth(this.url, this.accessToken), }); - const registry = await getRegistry(this.connection); - this._registry = _.fromPairs( - _.toPairs(registry) - .filter(([, item]) => { - const isHidden = item.hidden_by != undefined; - const isDisabled = item.disabled_by != undefined; - if (isHidden) { - this.log.debug( - "%s is hidden by %s", - item.entity_id, - item.hidden_by, - ); - } - if (isDisabled) { - this.log.debug( - "%s is disabled by %s", - item.entity_id, - item.disabled_by, - ); - } - return !isHidden && !isDisabled; - }) - .filter(([, item]) => { - const hasState = !!item.initialState; - if (!hasState) { - this.log.warn("%s does not have an initial-state", item.entity_id); - } - return hasState; - }), + this._initialStates = _.keyBy( + await getStates(this.connection), + (e) => e.entity_id, + ); + this._registry = _.keyBy( + await getRegistry(this.connection), + (r) => r.entity_id, ); - this.subscriptions = {}; } @@ -98,18 +79,28 @@ export class HomeAssistantClient this.connection = undefined; this._registry = undefined; + this._initialStates = undefined; } registry( filter: HomeAssistantFilter, - ): HomeAssistantEntityRegistryWithInitialState[] { + ): Dictionary { if (!this._registry) { throw new Error("Home Assistant Client is not yet initialized"); } - return _.filter(this._registry, (r) => matchEntityFilter(r, filter)); + return _.pickBy( + this._registry, + (r) => isValidEntity(r) && matchEntityFilter(r, filter), + ); + } + + initialStates(entityIds: string[]): Dictionary { + return _.pickBy(this._initialStates, (e) => + entityIds.includes(e.entity_id), + ); } - states( + subscribeStates( filter: HomeAssistantFilter, cb: (entities: Dictionary) => Promise, ): () => void { diff --git a/packages/backend/src/home-assistant/is-valid-entity.ts b/packages/backend/src/home-assistant/is-valid-entity.ts new file mode 100644 index 0000000..4517d71 --- /dev/null +++ b/packages/backend/src/home-assistant/is-valid-entity.ts @@ -0,0 +1,10 @@ +import { HomeAssistantEntityRegistry } from "@home-assistant-matter-hub/common"; + +export function isValidEntity(registry: HomeAssistantEntityRegistry): boolean { + if (registry.disabled_by != null) { + return false; + } else if (registry.hidden_by != null) { + return false; + } + return true; +} diff --git a/packages/backend/src/home-assistant/match-entity-filter.test.ts b/packages/backend/src/home-assistant/match-entity-filter.test.ts index f7b00b7..46f0df3 100644 --- a/packages/backend/src/home-assistant/match-entity-filter.test.ts +++ b/packages/backend/src/home-assistant/match-entity-filter.test.ts @@ -1,12 +1,11 @@ import { describe, expect, it } from "vitest"; import { testMatcher } from "./match-entity-filter.js"; import { - HomeAssistantEntityRegistryWithInitialState, - HomeAssistantEntityState, + HomeAssistantEntityRegistry, HomeAssistantMatcherType, } from "@home-assistant-matter-hub/common"; -const entity: HomeAssistantEntityRegistryWithInitialState = { +const entity: HomeAssistantEntityRegistry = { id: "id", entity_id: "light.my_entity", categories: {}, @@ -15,7 +14,6 @@ const entity: HomeAssistantEntityRegistryWithInitialState = { unique_id: "unique_id", platform: "hue", labels: ["test_label"], - initialState: undefined as unknown as HomeAssistantEntityState, }; describe("matchEntityFilter.testMatcher", () => { diff --git a/packages/backend/src/home-assistant/match-entity-filter.ts b/packages/backend/src/home-assistant/match-entity-filter.ts index 24e2064..1c4f72c 100644 --- a/packages/backend/src/home-assistant/match-entity-filter.ts +++ b/packages/backend/src/home-assistant/match-entity-filter.ts @@ -1,11 +1,11 @@ import { - HomeAssistantEntityRegistryWithInitialState, + HomeAssistantEntityRegistry, HomeAssistantFilter, HomeAssistantMatcher, } from "@home-assistant-matter-hub/common"; export function matchEntityFilter( - entity: HomeAssistantEntityRegistryWithInitialState, + entity: HomeAssistantEntityRegistry, filter: HomeAssistantFilter, ): boolean { const included = @@ -18,7 +18,7 @@ export function matchEntityFilter( } export function testMatcher( - entity: HomeAssistantEntityRegistryWithInitialState, + entity: HomeAssistantEntityRegistry, matcher: HomeAssistantMatcher, ): boolean { switch (matcher.type) { diff --git a/packages/backend/src/matter/behaviors/basic-information-server.ts b/packages/backend/src/matter/behaviors/basic-information-server.ts index 682575f..82c1919 100644 --- a/packages/backend/src/matter/behaviors/basic-information-server.ts +++ b/packages/backend/src/matter/behaviors/basic-information-server.ts @@ -1,31 +1,35 @@ -import { BridgedDeviceBasicInformationServer as Base } from "@project-chip/matter.js/behavior/definitions/bridged-device-basic-information"; -import { - BridgeBasicInformation, - HomeAssistantEntityState, -} from "@home-assistant-matter-hub/common"; -import { Behavior } from "@project-chip/matter.js/behavior"; +import { BridgedDeviceBasicInformationServer as Base } from "@project-chip/matter.js/behaviors/bridged-device-basic-information"; import crypto from "node:crypto"; +import { HomeAssistantBehavior } from "../custom-behaviors/home-assistant-behavior.js"; +import { VendorId } from "@project-chip/matter.js/datatype"; -export class BasicInformationServer extends Base {} - -export namespace BasicInformationServer { - export function createState( - basicInformation: BridgeBasicInformation, - state: HomeAssistantEntityState, - ): Behavior.Options { - return { - vendorId: basicInformation.vendorId, - vendorName: maxLengthOrHash(basicInformation.vendorName, 32), - productName: maxLengthOrHash(basicInformation.productName, 32), - productLabel: maxLengthOrHash(basicInformation.productLabel, 64), - hardwareVersion: basicInformation.hardwareVersion, - softwareVersion: basicInformation.softwareVersion, - nodeLabel: maxLengthOrHash( - state.attributes.friendly_name ?? "Unknown Entity", - 32, - ), - reachable: true, - }; +export class BasicInformationServer extends Base { + override async initialize(): Promise { + await super.initialize(); + const homeAssistant = await this.agent.load(HomeAssistantBehavior); + const homeAssistantInfo = homeAssistant.state; + this.state.vendorId = VendorId(homeAssistantInfo.basicInformation.vendorId); + this.state.vendorName = maxLengthOrHash( + homeAssistantInfo.basicInformation.vendorName, + 32, + ); + this.state.productName = maxLengthOrHash( + homeAssistantInfo.basicInformation.productName, + 32, + ); + this.state.productLabel = maxLengthOrHash( + homeAssistantInfo.basicInformation.productLabel, + 64, + ); + this.state.hardwareVersion = + homeAssistantInfo.basicInformation.hardwareVersion; + this.state.softwareVersion = + homeAssistantInfo.basicInformation.softwareVersion; + this.state.nodeLabel = maxLengthOrHash( + homeAssistantInfo.entity.attributes.friendly_name ?? "Unknown Entity", + 32, + ); + this.state.reachable = true; } } diff --git a/packages/backend/src/matter/behaviors/boolean-state-server.ts b/packages/backend/src/matter/behaviors/boolean-state-server.ts index e2ee686..b93cee3 100644 --- a/packages/backend/src/matter/behaviors/boolean-state-server.ts +++ b/packages/backend/src/matter/behaviors/boolean-state-server.ts @@ -1,13 +1,17 @@ -import { haMixin } from "../mixins/ha-mixin.js"; import { BooleanStateServer as Base } from "@project-chip/matter.js/behaviors/boolean-state"; import { HomeAssistantEntityState } from "@home-assistant-matter-hub/common"; -import { Behavior } from "@project-chip/matter.js/behavior"; +import { HomeAssistantBehavior } from "../custom-behaviors/home-assistant-behavior.js"; export function BooleanStateServer(inverted: boolean) { - return class Type extends haMixin("BooleanState", Base) { - override initialize(options?: {}) { - this.endpoint.entityState.subscribe(this.update.bind(this)); - return super.initialize(options); + return class Type extends Base { + override async initialize() { + super.initialize(); + const homeAssistant = await this.agent.load(HomeAssistantBehavior); + this.state.stateValue = getStateValue( + inverted, + homeAssistant.state.entity, + ); + homeAssistant.onUpdate((s) => this.update(s)); } private async update(state: HomeAssistantEntityState) { @@ -29,14 +33,3 @@ function getStateValue( const isOn = state.state !== "off"; return inverted ? !isOn : isOn; } - -export namespace BooleanStateServer { - export function createState( - inverted: boolean, - state: HomeAssistantEntityState, - ): Behavior.Options { - return { - stateValue: getStateValue(inverted, state), - }; - } -} diff --git a/packages/backend/src/matter/behaviors/color-temperature-control-server.ts b/packages/backend/src/matter/behaviors/color-temperature-control-server.ts index ed6e32b..0f5d1c2 100644 --- a/packages/backend/src/matter/behaviors/color-temperature-control-server.ts +++ b/packages/backend/src/matter/behaviors/color-temperature-control-server.ts @@ -1,4 +1,3 @@ -import { haMixin } from "../mixins/ha-mixin.js"; import { ColorConverter, HomeAssistantEntityState, @@ -7,15 +6,37 @@ import { } from "@home-assistant-matter-hub/common"; import { ColorControlServer as Base } from "@project-chip/matter.js/behaviors/color-control"; import { ColorControl } from "@project-chip/matter.js/cluster"; -import { Behavior } from "@project-chip/matter.js/behavior"; +import { HomeAssistantBehavior } from "../custom-behaviors/home-assistant-behavior.js"; -export class ColorTemperatureControlServer extends haMixin( - "ColorControl", - Base.with("ColorTemperature"), +export class ColorTemperatureControlServer extends Base.with( + "ColorTemperature", ) { - override initialize() { - super.initialize(); - this.endpoint.entityState.subscribe(this.update.bind(this)); + override async initialize() { + await super.initialize(); + + const homeAssistant = await this.agent.load(HomeAssistantBehavior); + const state = homeAssistant.state + .entity as HomeAssistantEntityState; + const minKelvin = state.attributes.min_color_temp_kelvin ?? 1500; + + const maxKelvin = state.attributes.max_color_temp_kelvin ?? 8000; + this.state.coupleColorTempToLevelMinMireds = + ColorConverter.temperatureKelvinToMireds(maxKelvin); + this.state.colorTempPhysicalMinMireds = + ColorConverter.temperatureKelvinToMireds(maxKelvin); + this.state.colorTempPhysicalMaxMireds = + ColorConverter.temperatureKelvinToMireds(minKelvin); + this.state.startUpColorTemperatureMireds = + ColorConverter.temperatureKelvinToMireds( + state.attributes.color_temp_kelvin ?? maxKelvin, + ); + if (state.attributes.color_temp_kelvin) { + this.state.colorTemperatureMireds = + ColorConverter.temperatureKelvinToMireds( + state.attributes.color_temp_kelvin, + ); + } + homeAssistant.onUpdate((s) => this.update(s)); } protected async update( @@ -41,6 +62,7 @@ export class ColorTemperatureControlServer extends haMixin( override async moveToColorTemperature( request: ColorControl.MoveToColorTemperatureRequest, ) { + const homeAssistant = this.agent.get(HomeAssistantBehavior); const targetKelvin = ColorConverter.temperatureMiredsToKelvin( request.colorTemperatureMireds, ); @@ -48,40 +70,15 @@ export class ColorTemperatureControlServer extends haMixin( ...request, transitionTime: request.transitionTime ?? 1, }); - await this.callAction( + await homeAssistant.callAction( "light", "turn_on", { color_temp_kelvin: targetKelvin, }, { - entity_id: this.entity.entity_id, + entity_id: homeAssistant.state.entity.entity_id, }, ); } } - -export namespace ColorTemperatureControlServer { - export function createState( - state: HomeAssistantEntityState, - ): Behavior.Options { - const minKelvin = state.attributes.min_color_temp_kelvin ?? 1500; - const maxKelvin = state.attributes.max_color_temp_kelvin ?? 8000; - return { - coupleColorTempToLevelMinMireds: - ColorConverter.temperatureKelvinToMireds(maxKelvin), - colorTempPhysicalMinMireds: - ColorConverter.temperatureKelvinToMireds(maxKelvin), - colorTempPhysicalMaxMireds: - ColorConverter.temperatureKelvinToMireds(minKelvin), - startUpColorTemperatureMireds: ColorConverter.temperatureKelvinToMireds( - state.attributes.color_temp_kelvin ?? maxKelvin, - ), - colorTemperatureMireds: state.attributes.color_temp_kelvin - ? ColorConverter.temperatureKelvinToMireds( - state.attributes.color_temp_kelvin, - ) - : undefined, - }; - } -} diff --git a/packages/backend/src/matter/behaviors/extended-color-control-server.ts b/packages/backend/src/matter/behaviors/extended-color-control-server.ts index 652100c..cacb299 100644 --- a/packages/backend/src/matter/behaviors/extended-color-control-server.ts +++ b/packages/backend/src/matter/behaviors/extended-color-control-server.ts @@ -1,21 +1,41 @@ -import { haMixin } from "../mixins/ha-mixin.js"; import { + ColorConverter, HomeAssistantEntityState, LightDeviceAttributes, LightDeviceColorMode, } from "@home-assistant-matter-hub/common"; -import { ColorConverter } from "@home-assistant-matter-hub/common"; import { ColorControlServer as Base } from "@project-chip/matter.js/behaviors/color-control"; import { ColorControl } from "@project-chip/matter.js/cluster"; -import { Behavior } from "@project-chip/matter.js/behavior"; +import { HomeAssistantBehavior } from "../custom-behaviors/home-assistant-behavior.js"; -export class ExtendedColorControlServer extends haMixin( - "ColorControl", - Base.with("ColorTemperature", "HueSaturation"), +export class ExtendedColorControlServer extends Base.with( + "ColorTemperature", + "HueSaturation", ) { - override initialize() { - super.initialize(); - this.endpoint.entityState.subscribe(this.update.bind(this)); + override async initialize() { + await super.initialize(); + const homeAssistant = await this.agent.load(HomeAssistantBehavior); + const state = homeAssistant.state + .entity as HomeAssistantEntityState; + const minKelvin = state.attributes.min_color_temp_kelvin ?? 1500; + const maxKelvin = state.attributes.max_color_temp_kelvin ?? 8000; + this.state.coupleColorTempToLevelMinMireds = + ColorConverter.temperatureKelvinToMireds(maxKelvin); + this.state.colorTempPhysicalMinMireds = + ColorConverter.temperatureKelvinToMireds(maxKelvin); + this.state.colorTempPhysicalMaxMireds = + ColorConverter.temperatureKelvinToMireds(minKelvin); + this.state.startUpColorTemperatureMireds = + ColorConverter.temperatureKelvinToMireds( + state.attributes.color_temp_kelvin ?? maxKelvin, + ); + if (state.attributes.color_temp_kelvin) { + this.state.colorTemperatureMireds = + ColorConverter.temperatureKelvinToMireds( + state.attributes.color_temp_kelvin, + ); + } + homeAssistant.onUpdate((s) => this.update(s)); } protected async update( @@ -55,6 +75,7 @@ export class ExtendedColorControlServer extends haMixin( override async moveToColorTemperature( request: ColorControl.MoveToColorTemperatureRequest, ) { + const homeAssistant = this.agent.get(HomeAssistantBehavior); const targetKelvin = ColorConverter.temperatureMiredsToKelvin( request.colorTemperatureMireds, ); @@ -62,14 +83,14 @@ export class ExtendedColorControlServer extends haMixin( ...request, transitionTime: request.transitionTime ?? 1, }); - await this.callAction( + await homeAssistant.callAction( "light", "turn_on", { color_temp_kelvin: targetKelvin, }, { - entity_id: this.entity.entity_id, + entity_id: homeAssistant.state.entity.entity_id, }, ); } @@ -77,20 +98,21 @@ export class ExtendedColorControlServer extends haMixin( override async moveToHueAndSaturation( request: ColorControl.MoveToHueAndSaturationRequest, ) { + const homeAssistant = this.agent.get(HomeAssistantBehavior); await super.moveToHueAndSaturation({ ...request, transitionTime: request.transitionTime ?? 1, }); const color = ColorConverter.fromMatterHS(request.hue, request.saturation); const [hue, saturation] = ColorConverter.toHomeAssistantHS(color); - await this.callAction( + await homeAssistant.callAction( "light", "turn_on", { hs_color: [hue, saturation], }, { - entity_id: this.entity.entity_id, + entity_id: homeAssistant.state.entity.entity_id, }, ); } @@ -117,28 +139,3 @@ export class ExtendedColorControlServer extends haMixin( return undefined; } } - -export namespace ExtendedColorControlServer { - export function createState( - state: HomeAssistantEntityState, - ): Behavior.Options { - const minKelvin = state.attributes.min_color_temp_kelvin ?? 1500; - const maxKelvin = state.attributes.max_color_temp_kelvin ?? 8000; - return { - coupleColorTempToLevelMinMireds: - ColorConverter.temperatureKelvinToMireds(maxKelvin), - colorTempPhysicalMinMireds: - ColorConverter.temperatureKelvinToMireds(maxKelvin), - colorTempPhysicalMaxMireds: - ColorConverter.temperatureKelvinToMireds(minKelvin), - startUpColorTemperatureMireds: ColorConverter.temperatureKelvinToMireds( - state.attributes.color_temp_kelvin ?? maxKelvin, - ), - colorTemperatureMireds: state.attributes.color_temp_kelvin - ? ColorConverter.temperatureKelvinToMireds( - state.attributes.color_temp_kelvin, - ) - : undefined, - }; - } -} diff --git a/packages/backend/src/matter/behaviors/humidity-measurement-server.ts b/packages/backend/src/matter/behaviors/humidity-measurement-server.ts index 76d497f..8fe86db 100644 --- a/packages/backend/src/matter/behaviors/humidity-measurement-server.ts +++ b/packages/backend/src/matter/behaviors/humidity-measurement-server.ts @@ -3,16 +3,14 @@ import { HomeAssistantEntityState, SensorDeviceAttributes, } from "@home-assistant-matter-hub/common"; -import { Behavior } from "@project-chip/matter.js/behavior"; -import { haMixin } from "../mixins/ha-mixin.js"; +import { HomeAssistantBehavior } from "../custom-behaviors/home-assistant-behavior.js"; -export class HumidityMeasurementServer extends haMixin( - "HumidityMeasurement", - Base, -) { - override initialize(options?: {}) { - this.endpoint.entityState.subscribe(this.update.bind(this)); - return super.initialize(options); +export class HumidityMeasurementServer extends Base { + override async initialize() { + await super.initialize(); + const homeAssistant = await this.agent.load(HomeAssistantBehavior); + this.state.measuredValue = getHumidity(homeAssistant.state.entity); + homeAssistant.onUpdate((s) => this.update(s)); } private async update(state: HomeAssistantEntityState) { @@ -26,16 +24,6 @@ export class HumidityMeasurementServer extends haMixin( } } -export namespace HumidityMeasurementServer { - export function createState( - state: HomeAssistantEntityState, - ): Behavior.Options { - return { - measuredValue: getHumidity(state), - }; - } -} - function getHumidity({ state, }: HomeAssistantEntityState): number | null { diff --git a/packages/backend/src/matter/behaviors/identify-server.ts b/packages/backend/src/matter/behaviors/identify-server.ts index c50535e..1863624 100644 --- a/packages/backend/src/matter/behaviors/identify-server.ts +++ b/packages/backend/src/matter/behaviors/identify-server.ts @@ -1,8 +1,3 @@ -import { IdentifyServer as Base } from "@project-chip/matter.js/behavior/definitions/identify"; -import { haMixin } from "../mixins/ha-mixin.js"; +import { IdentifyServer as Base } from "@project-chip/matter.js/behaviors/identify"; -export class IdentifyServer extends haMixin("Identify", Base) { - override triggerEffect() { - this.logger.info(`Identifying ${this.entity.entity_id}`); - } -} +export class IdentifyServer extends Base {} diff --git a/packages/backend/src/matter/behaviors/level-control-server.ts b/packages/backend/src/matter/behaviors/level-control-server.ts index 52f872b..2b80ad9 100644 --- a/packages/backend/src/matter/behaviors/level-control-server.ts +++ b/packages/backend/src/matter/behaviors/level-control-server.ts @@ -1,11 +1,10 @@ import { LevelControlServer as Base } from "@project-chip/matter.js/behaviors/level-control"; import { HomeAssistantEntityState } from "@home-assistant-matter-hub/common"; -import { haMixin } from "../mixins/ha-mixin.js"; import { LevelControl } from "@project-chip/matter.js/cluster"; -import { Behavior } from "@project-chip/matter.js/behavior"; +import { HomeAssistantBehavior } from "../custom-behaviors/home-assistant-behavior.js"; export interface LevelControlConfig { - getValue: (state: HomeAssistantEntityState) => number | null | undefined; + getValue: (state: HomeAssistantEntityState) => number | null; getMinValue?: (state: HomeAssistantEntityState) => number | undefined; getMaxValue?: (state: HomeAssistantEntityState) => number | undefined; moveToLevel: { @@ -15,10 +14,19 @@ export interface LevelControlConfig { } export function LevelControlServer(config: LevelControlConfig) { - return class ThisType extends haMixin("LevelControl", Base) { - override initialize() { + return class ThisType extends Base { + override async initialize() { super.initialize(); - this.endpoint.entityState.subscribe(this.update.bind(this)); + const homeAssistant = await this.agent.load(HomeAssistantBehavior); + const state = homeAssistant.state.entity; + this.state.currentLevel = config.getValue(state); + this.state.minLevel = config.getMinValue?.(state); + this.state.maxLevel = config.getMaxValue?.(state); + this.state.managedTransitionTimeHandling = false; + this.state.onTransitionTime = 1; + this.state.offTransitionTime = 1; + this.state.onOffTransitionTime = 1; + homeAssistant.onUpdate((s) => this.update(s)); } protected async update(state: HomeAssistantEntityState) { @@ -50,32 +58,16 @@ export function LevelControlServer(config: LevelControlConfig) { } private async handleMoveToLevel(request: LevelControl.MoveToLevelRequest) { + const homeAssistant = this.agent.get(HomeAssistantBehavior); const [domain, action] = config.moveToLevel.action.split("."); - await this.callAction( + await homeAssistant.callAction( domain, action, config.moveToLevel.data(request.level), { - entity_id: this.entity.entity_id, + entity_id: homeAssistant.state.entity.entity_id, }, ); } }; } - -export namespace LevelControlServer { - export function createState( - config: LevelControlConfig, - state: HomeAssistantEntityState, - ): Behavior.Options { - return { - currentLevel: config.getValue(state), - minLevel: config.getMinValue?.(state), - maxLevel: config.getMaxValue?.(state), - managedTransitionTimeHandling: false, - onTransitionTime: 1, - offTransitionTime: 1, - onOffTransitionTime: 1, - }; - } -} diff --git a/packages/backend/src/matter/behaviors/lock-server.ts b/packages/backend/src/matter/behaviors/lock-server.ts index 22a6cfd..334a590 100644 --- a/packages/backend/src/matter/behaviors/lock-server.ts +++ b/packages/backend/src/matter/behaviors/lock-server.ts @@ -1,13 +1,24 @@ -import { haMixin } from "../mixins/ha-mixin.js"; -import { DoorLockServer } from "@project-chip/matter.js/behavior/definitions/door-lock"; +import { DoorLockServer as Base } from "@project-chip/matter.js/behaviors/door-lock"; import { HomeAssistantEntityState } from "@home-assistant-matter-hub/common"; -import { Behavior } from "@project-chip/matter.js/behavior"; import { DoorLock } from "@project-chip/matter.js/cluster"; +import { HomeAssistantBehavior } from "../custom-behaviors/home-assistant-behavior.js"; -export class LockServer extends haMixin("LockServer", DoorLockServer) { - override initialize() { - super.initialize(); - this.endpoint.entityState.subscribe(this.update.bind(this)); +export class LockServer extends Base { + override async initialize() { + await super.initialize(); + const homeAssistant = await this.agent.load(HomeAssistantBehavior); + this.state.lockState = getMatterLockState(homeAssistant.state.entity); + this.state.lockType = DoorLock.LockType.DeadBolt; + this.state.operatingMode = DoorLock.OperatingMode.Normal; + this.state.actuatorEnabled = true; + this.state.supportedOperatingModes = { + noRemoteLockUnlock: false, + normal: true, + passage: false, + privacy: false, + vacation: false, + }; + homeAssistant.onUpdate((s) => this.update(s)); } private async update(state: HomeAssistantEntityState) { @@ -22,39 +33,21 @@ export class LockServer extends haMixin("LockServer", DoorLockServer) { override async lockDoor() { super.lockDoor(); - await this.callAction("lock", "lock", undefined, { - entity_id: this.entity.entity_id, + const homeAssistant = this.agent.get(HomeAssistantBehavior); + await homeAssistant.callAction("lock", "lock", undefined, { + entity_id: homeAssistant.state.entity.entity_id, }); } override async unlockDoor() { super.unlockDoor(); - await this.callAction("lock", "unlock", undefined, { - entity_id: this.entity.entity_id, + const homeAssistant = this.agent.get(HomeAssistantBehavior); + await homeAssistant.callAction("lock", "unlock", undefined, { + entity_id: homeAssistant.state.entity.entity_id, }); } } -export namespace LockServer { - export function createState( - state: HomeAssistantEntityState, - ): Behavior.Options { - return { - lockState: getMatterLockState(state), - lockType: DoorLock.LockType.DeadBolt, - operatingMode: DoorLock.OperatingMode.Normal, - actuatorEnabled: true, - supportedOperatingModes: { - noRemoteLockUnlock: false, - normal: true, - passage: false, - privacy: false, - vacation: false, - }, - }; - } -} - function getMatterLockState(state: HomeAssistantEntityState) { return mapHAState[state.state] ?? DoorLock.LockState.NotFullyLocked; } diff --git a/packages/backend/src/matter/behaviors/occupancy-sensing-server.ts b/packages/backend/src/matter/behaviors/occupancy-sensing-server.ts index 86089cf..c26490a 100644 --- a/packages/backend/src/matter/behaviors/occupancy-sensing-server.ts +++ b/packages/backend/src/matter/behaviors/occupancy-sensing-server.ts @@ -1,13 +1,21 @@ -import { haMixin } from "../mixins/ha-mixin.js"; import { OccupancySensingServer as Base } from "@project-chip/matter.js/behaviors/occupancy-sensing"; import { HomeAssistantEntityState } from "@home-assistant-matter-hub/common"; -import { Behavior } from "@project-chip/matter.js/behavior"; import { OccupancySensing } from "@project-chip/matter.js/cluster"; +import { HomeAssistantBehavior } from "../custom-behaviors/home-assistant-behavior.js"; -export class OccupancySensingServer extends haMixin("OccupancySensing", Base) { - override initialize(options?: {}) { - this.endpoint.entityState.subscribe(this.update.bind(this)); - return super.initialize(options); +export class OccupancySensingServer extends Base { + override async initialize() { + await super.initialize(); + const homeAssistant = await this.agent.load(HomeAssistantBehavior); + this.state.occupancy = { occupied: isOccupied(homeAssistant.state.entity) }; + this.state.occupancySensorType = + OccupancySensing.OccupancySensorType.PhysicalContact; + this.state.occupancySensorTypeBitmap = { + pir: false, + physicalContact: true, + ultrasonic: false, + }; + homeAssistant.onUpdate((s) => this.update(s)); } private async update(state: HomeAssistantEntityState) { @@ -24,19 +32,3 @@ export class OccupancySensingServer extends haMixin("OccupancySensing", Base) { function isOccupied(state: HomeAssistantEntityState): boolean { return state.state !== "off"; } - -export namespace OccupancySensingServer { - export function createState( - state: HomeAssistantEntityState, - ): Behavior.Options { - return { - occupancy: { occupied: isOccupied(state) }, - occupancySensorType: OccupancySensing.OccupancySensorType.PhysicalContact, - occupancySensorTypeBitmap: { - pir: false, - physicalContact: true, - ultrasonic: false, - }, - }; - } -} diff --git a/packages/backend/src/matter/behaviors/on-off-server.ts b/packages/backend/src/matter/behaviors/on-off-server.ts index 36ac7cd..4c45bd3 100644 --- a/packages/backend/src/matter/behaviors/on-off-server.ts +++ b/packages/backend/src/matter/behaviors/on-off-server.ts @@ -1,15 +1,16 @@ import { OnOffServer as Base } from "@project-chip/matter.js/behaviors/on-off"; import { HomeAssistantEntityState } from "@home-assistant-matter-hub/common"; -import { haMixin } from "../mixins/ha-mixin.js"; -import { Behavior } from "@project-chip/matter.js/behavior"; +import { HomeAssistantBehavior } from "../custom-behaviors/home-assistant-behavior.js"; -export class OnOffServer extends haMixin("OnOff", Base) { - override initialize() { +export class OnOffServer extends Base { + override async initialize() { super.initialize(); - this.endpoint.entityState.subscribe(this.update.bind(this)); + const homeAssistant = await this.agent.load(HomeAssistantBehavior); + this.state.onOff = homeAssistant.state.entity.state !== "off"; + homeAssistant.onUpdate((s) => this.update(s)); } - protected async update(state: HomeAssistantEntityState) { + private async update(state: HomeAssistantEntityState) { const current = this.endpoint.stateOf(OnOffServer); const isOn = state.state !== "off"; if (isOn !== current.onOff) { @@ -19,25 +20,17 @@ export class OnOffServer extends haMixin("OnOff", Base) { override async on() { await super.on(); - await this.callAction("homeassistant", "turn_on", undefined, { - entity_id: this.entity.entity_id, + const homeAssistant = this.agent.get(HomeAssistantBehavior); + await homeAssistant.callAction("homeassistant", "turn_on", undefined, { + entity_id: homeAssistant.state.entity.entity_id, }); } override async off() { await super.off(); - await this.callAction("homeassistant", "turn_off", undefined, { - entity_id: this.entity.entity_id, + const homeAssistant = this.agent.get(HomeAssistantBehavior); + await homeAssistant.callAction("homeassistant", "turn_off", undefined, { + entity_id: homeAssistant.state.entity.entity_id, }); } } - -export namespace OnOffServer { - export function createState( - state: HomeAssistantEntityState, - ): Behavior.Options { - return { - onOff: state.state !== "off", - }; - } -} diff --git a/packages/backend/src/matter/behaviors/temperature-measurement-server.ts b/packages/backend/src/matter/behaviors/temperature-measurement-server.ts index 1efbd69..94658ed 100644 --- a/packages/backend/src/matter/behaviors/temperature-measurement-server.ts +++ b/packages/backend/src/matter/behaviors/temperature-measurement-server.ts @@ -3,16 +3,14 @@ import { HomeAssistantEntityState, SensorDeviceAttributes, } from "@home-assistant-matter-hub/common"; -import { Behavior } from "@project-chip/matter.js/behavior"; -import { haMixin } from "../mixins/ha-mixin.js"; +import { HomeAssistantBehavior } from "../custom-behaviors/home-assistant-behavior.js"; -export class TemperatureMeasurementServer extends haMixin( - "TemperatureMeasurement", - Base, -) { - override initialize(options?: {}) { - this.endpoint.entityState.subscribe(this.update.bind(this)); - return super.initialize(options); +export class TemperatureMeasurementServer extends Base { + override async initialize() { + const homeAssistant = await this.agent.load(HomeAssistantBehavior); + this.state.measuredValue = getTemperature(homeAssistant.state.entity); + homeAssistant.onUpdate((s) => this.update(s)); + await super.initialize(); } private async update(state: HomeAssistantEntityState) { @@ -26,16 +24,6 @@ export class TemperatureMeasurementServer extends haMixin( } } -export namespace TemperatureMeasurementServer { - export function createState( - state: HomeAssistantEntityState, - ): Behavior.Options { - return { - measuredValue: getTemperature(state), - }; - } -} - function getTemperature({ state, attributes, diff --git a/packages/backend/src/matter/behaviors/thermostat-server.ts b/packages/backend/src/matter/behaviors/thermostat-server.ts index e1cf1b1..5fe58bf 100644 --- a/packages/backend/src/matter/behaviors/thermostat-server.ts +++ b/packages/backend/src/matter/behaviors/thermostat-server.ts @@ -1,20 +1,18 @@ import { HeatingAndCoolingThermostatServer } from "./thermostat/heating-and-cooling-thermostat-server.js"; import { HeatingThermostatServer } from "./thermostat/heating-thermostat-server.js"; import { CoolingThermostatServer } from "./thermostat/cooling-thermostat-server.js"; -import { DefaultThermostatServer } from "./thermostat/default-thermostat-server.js"; export function ThermostatServer( supportsCooling: boolean, supportsHeating: boolean, - supportsAuto: boolean, ) { - if (supportsAuto || (supportsCooling && supportsHeating)) { - return HeatingAndCoolingThermostatServer(supportsAuto); + if (supportsCooling && supportsHeating) { + return HeatingAndCoolingThermostatServer; } else if (supportsHeating) { return HeatingThermostatServer; } else if (supportsCooling) { return CoolingThermostatServer; } else { - return DefaultThermostatServer; + return undefined; } } diff --git a/packages/backend/src/matter/behaviors/thermostat/cooling-thermostat-server.ts b/packages/backend/src/matter/behaviors/thermostat/cooling-thermostat-server.ts index 4b8675b..0356eb6 100644 --- a/packages/backend/src/matter/behaviors/thermostat/cooling-thermostat-server.ts +++ b/packages/backend/src/matter/behaviors/thermostat/cooling-thermostat-server.ts @@ -1,27 +1,31 @@ -import { ThermostatServer as Base } from "@project-chip/matter.js/behavior/definitions/thermostat"; +import { ThermostatServer as Base } from "@project-chip/matter.js/behaviors/thermostat"; import { ThermostatBaseServer } from "./thermostat-base-server.js"; -import { - ClimateDeviceAttributes, - HomeAssistantEntityState, -} from "@home-assistant-matter-hub/common"; +import { HomeAssistantEntityState } from "@home-assistant-matter-hub/common"; import { Behavior } from "@project-chip/matter.js/behavior"; import { Thermostat } from "@project-chip/matter.js/cluster"; export class CoolingThermostatServer extends ThermostatBaseServer( Base.with("Cooling"), ) { - override initialize(options?: {}) { + override async initialize() { + await super.initialize(); + this.state.localTemperature = this.internal.currentTemperature; + this.state.systemMode = this.internal.systemMode; + this.state.occupiedCoolingSetpoint = this.internal.targetTemperature; + this.state.minCoolSetpointLimit = this.internal.minTemperature; + this.state.maxCoolSetpointLimit = this.internal.maxTemperature; + this.state.controlSequenceOfOperation = + Thermostat.ControlSequenceOfOperation.CoolingOnly; this.endpoint .eventsOf(CoolingThermostatServer) .occupiedCoolingSetpoint$Changed.on( this.targetTemperatureChanged.bind(this), ); - return super.initialize(options); } protected override async update(state: HomeAssistantEntityState) { await super.update(state); - const expectedState = this.currentState; + const expectedState = this.internal; if (!expectedState) { return; } @@ -49,20 +53,3 @@ export class CoolingThermostatServer extends ThermostatBaseServer( await this.endpoint.setStateOf(CoolingThermostatServer, patch); } } - -export namespace CoolingThermostatServer { - export function createState( - entity: HomeAssistantEntityState, - ): Behavior.Options { - const state = ThermostatBaseServer.createState(entity); - return { - localTemperature: state.currentTemperature, - systemMode: state.systemMode, - occupiedCoolingSetpoint: state.targetTemperature, - minCoolSetpointLimit: state.minTemperature, - maxCoolSetpointLimit: state.maxTemperature, - controlSequenceOfOperation: - Thermostat.ControlSequenceOfOperation.CoolingOnly, - }; - } -} diff --git a/packages/backend/src/matter/behaviors/thermostat/default-thermostat-server.ts b/packages/backend/src/matter/behaviors/thermostat/default-thermostat-server.ts deleted file mode 100644 index fe15aa7..0000000 --- a/packages/backend/src/matter/behaviors/thermostat/default-thermostat-server.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { ThermostatBaseServer } from "./thermostat-base-server.js"; -import { ThermostatServer as Base } from "@project-chip/matter.js/behavior/definitions/thermostat"; -import { - ClimateDeviceAttributes, - HomeAssistantEntityState, -} from "@home-assistant-matter-hub/common"; -import { Behavior } from "@project-chip/matter.js/behavior"; -import { Thermostat } from "@project-chip/matter.js/cluster"; - -export class DefaultThermostatServer extends ThermostatBaseServer(Base) {} - -export namespace DefaultThermostatServer { - export function createState( - entity: HomeAssistantEntityState, - ): Behavior.Options { - const state = ThermostatBaseServer.createState(entity); - return { - localTemperature: state.currentTemperature, - systemMode: state.systemMode, - controlSequenceOfOperation: - Thermostat.ControlSequenceOfOperation.CoolingAndHeating, - }; - } -} diff --git a/packages/backend/src/matter/behaviors/thermostat/heating-and-cooling-thermostat-server.ts b/packages/backend/src/matter/behaviors/thermostat/heating-and-cooling-thermostat-server.ts index 2060cdf..72fbcd9 100644 --- a/packages/backend/src/matter/behaviors/thermostat/heating-and-cooling-thermostat-server.ts +++ b/packages/backend/src/matter/behaviors/thermostat/heating-and-cooling-thermostat-server.ts @@ -1,36 +1,42 @@ -import { ThermostatServer as Base } from "@project-chip/matter.js/behavior/definitions/thermostat"; +import { ThermostatServer as Base } from "@project-chip/matter.js/behaviors/thermostat"; import { ThermostatBaseServer } from "./thermostat-base-server.js"; -import { - ClimateDeviceAttributes, - HomeAssistantEntityState, -} from "@home-assistant-matter-hub/common"; +import { HomeAssistantEntityState } from "@home-assistant-matter-hub/common"; import { Behavior } from "@project-chip/matter.js/behavior"; import { Thermostat } from "@project-chip/matter.js/cluster"; -class HeatingAndCoolingThermostatServerBase extends ThermostatBaseServer( +export class HeatingAndCoolingThermostatServer extends ThermostatBaseServer( Base.with("Heating", "Cooling"), ) { - override initialize(options?: {}) { - this.endpoint - .eventsOf(HeatingAndCoolingThermostatServerBase) - .occupiedHeatingSetpoint$Changed.on( - this.targetTemperatureChanged.bind(this), - ); - return super.initialize(options); + override async initialize() { + await super.initialize(); + this.state.localTemperature = this.internal.currentTemperature; + this.state.systemMode = this.internal.systemMode; + this.state.occupiedHeatingSetpoint = this.internal.targetTemperature; + this.state.occupiedCoolingSetpoint = this.internal.targetTemperature; + this.state.minHeatSetpointLimit = this.internal.minTemperature; + this.state.minCoolSetpointLimit = this.internal.minTemperature; + this.state.maxHeatSetpointLimit = this.internal.maxTemperature; + this.state.maxCoolSetpointLimit = this.internal.maxTemperature; + this.state.controlSequenceOfOperation = + Thermostat.ControlSequenceOfOperation.CoolingAndHeating; + + this.events.occupiedHeatingSetpoint$Changed.on( + this.targetTemperatureChanged.bind(this), + ); } protected override async update(state: HomeAssistantEntityState) { await super.update(state); - const expectedState = this.currentState; + const expectedState = this.internal; if (!expectedState) { return; } const actualState = this.endpoint.stateOf( - HeatingAndCoolingThermostatServerBase, + HeatingAndCoolingThermostatServer, ); const patch: Behavior.PatchStateOf< - typeof HeatingAndCoolingThermostatServerBase + typeof HeatingAndCoolingThermostatServer > = {}; if ( expectedState.targetTemperature !== actualState.occupiedHeatingSetpoint @@ -66,38 +72,6 @@ class HeatingAndCoolingThermostatServerBase extends ThermostatBaseServer( ) { patch.maxCoolSetpointLimit = expectedState.maxTemperature; } - await this.endpoint.setStateOf( - HeatingAndCoolingThermostatServerBase, - patch, - ); + await this.endpoint.setStateOf(HeatingAndCoolingThermostatServer, patch); } } - -export function HeatingAndCoolingThermostatServer(supportsAuto: boolean) { - const result = supportsAuto - ? HeatingAndCoolingThermostatServerBase.with( - "AutoMode", - "Cooling", - "Heating", - ) - : HeatingAndCoolingThermostatServerBase; - return Object.assign(result, { - createState( - entity: HomeAssistantEntityState, - ): Behavior.Options { - const state = ThermostatBaseServer.createState(entity); - return { - localTemperature: state.currentTemperature, - systemMode: state.systemMode, - occupiedHeatingSetpoint: state.targetTemperature, - occupiedCoolingSetpoint: state.targetTemperature, - minHeatSetpointLimit: state.minTemperature, - minCoolSetpointLimit: state.minTemperature, - maxHeatSetpointLimit: state.maxTemperature, - maxCoolSetpointLimit: state.maxTemperature, - controlSequenceOfOperation: - Thermostat.ControlSequenceOfOperation.CoolingAndHeating, - }; - }, - }); -} diff --git a/packages/backend/src/matter/behaviors/thermostat/heating-thermostat-server.ts b/packages/backend/src/matter/behaviors/thermostat/heating-thermostat-server.ts index 69d9698..0d812a0 100644 --- a/packages/backend/src/matter/behaviors/thermostat/heating-thermostat-server.ts +++ b/packages/backend/src/matter/behaviors/thermostat/heating-thermostat-server.ts @@ -1,27 +1,32 @@ -import { ThermostatServer as Base } from "@project-chip/matter.js/behavior/definitions/thermostat"; +import { ThermostatServer as Base } from "@project-chip/matter.js/behaviors/thermostat"; import { ThermostatBaseServer } from "./thermostat-base-server.js"; -import { - ClimateDeviceAttributes, - HomeAssistantEntityState, -} from "@home-assistant-matter-hub/common"; +import { HomeAssistantEntityState } from "@home-assistant-matter-hub/common"; import { Behavior } from "@project-chip/matter.js/behavior"; import { Thermostat } from "@project-chip/matter.js/cluster"; export class HeatingThermostatServer extends ThermostatBaseServer( Base.with("Heating"), ) { - override initialize(options?: {}) { + override async initialize() { + await super.initialize(); + this.state.localTemperature = this.internal.currentTemperature; + this.state.systemMode = this.internal.systemMode; + this.state.occupiedHeatingSetpoint = this.internal.targetTemperature; + this.state.minHeatSetpointLimit = this.internal.minTemperature; + this.state.maxHeatSetpointLimit = this.internal.maxTemperature; + this.state.controlSequenceOfOperation = + Thermostat.ControlSequenceOfOperation.HeatingOnly; + this.endpoint .eventsOf(HeatingThermostatServer) .occupiedHeatingSetpoint$Changed.on( this.targetTemperatureChanged.bind(this), ); - return super.initialize(options); } protected override async update(state: HomeAssistantEntityState) { await super.update(state); - const expectedState = this.currentState; + const expectedState = this.internal; if (!expectedState) { return; } @@ -49,20 +54,3 @@ export class HeatingThermostatServer extends ThermostatBaseServer( await this.endpoint.setStateOf(HeatingThermostatServer, patch); } } - -export namespace HeatingThermostatServer { - export function createState( - entity: HomeAssistantEntityState, - ): Behavior.Options { - const state = ThermostatBaseServer.createState(entity); - return { - localTemperature: state.currentTemperature, - systemMode: state.systemMode, - occupiedHeatingSetpoint: state.targetTemperature, - minHeatSetpointLimit: state.minTemperature, - maxHeatSetpointLimit: state.maxTemperature, - controlSequenceOfOperation: - Thermostat.ControlSequenceOfOperation.HeatingOnly, - }; - } -} diff --git a/packages/backend/src/matter/behaviors/thermostat/thermostat-base-server.ts b/packages/backend/src/matter/behaviors/thermostat/thermostat-base-server.ts index f429687..d2c39cb 100644 --- a/packages/backend/src/matter/behaviors/thermostat/thermostat-base-server.ts +++ b/packages/backend/src/matter/behaviors/thermostat/thermostat-base-server.ts @@ -4,35 +4,38 @@ import { HomeAssistantEntityState, Type, } from "@home-assistant-matter-hub/common"; -import { ThermostatServer as Base } from "@project-chip/matter.js/behavior/definitions/thermostat"; +import { ThermostatServer as Base } from "@project-chip/matter.js/behaviors/thermostat"; import { Thermostat } from "@project-chip/matter.js/cluster"; -import { haMixin } from "../../mixins/ha-mixin.js"; import { Behavior } from "@project-chip/matter.js/behavior"; import _ from "lodash"; +import { HomeAssistantBehavior } from "../../custom-behaviors/home-assistant-behavior.js"; -export interface ThermostatState { - systemMode: Thermostat.SystemMode; - minTemperature: number | undefined; - maxTemperature: number | undefined; - targetTemperature: number; - currentTemperature: number | null; -} - -function createBaseServer>(type: T) { - return class ThermostatBaseServer extends haMixin("ThermostatServer", type) { - protected currentState?: ThermostatState; +export function ThermostatBaseServer>(type: T) { + return class ThermostatBaseServer extends type { + protected declare internal: ThermostatBaseServer.Internal; - override initialize(options?: {}) { - this.endpoint.entityState.subscribe(this.update.bind(this)); + override async initialize() { + await super.initialize(); + const homeAssistant = await this.agent.load(HomeAssistantBehavior); + Object.assign( + this.internal, + this.getState( + homeAssistant.state + .entity as HomeAssistantEntityState, + ), + ); this.endpoint .eventsOf(Base) .systemMode$Changed.on(this.systemModeChanged.bind(this)); - return super.initialize(options); + homeAssistant.onUpdate((s) => this.update(s)); } protected async update(state: HomeAssistantEntityState) { - this.currentState = getState( - state as HomeAssistantEntityState, + Object.assign( + this.internal, + this.getState( + state as HomeAssistantEntityState, + ), ); const actualState = this.endpoint.stateOf(Base); const actualLocalTemperature = actualState.localTemperature; @@ -41,13 +44,13 @@ function createBaseServer>(type: T) { const patch: Behavior.PatchStateOf = {}; if ( - this.currentState.currentTemperature != null && - this.currentState.currentTemperature !== actualLocalTemperature + this.internal.currentTemperature != null && + this.internal.currentTemperature !== actualLocalTemperature ) { - patch.localTemperature = this.currentState.currentTemperature; + patch.localTemperature = this.internal.currentTemperature; } - if (actualSystemMode !== this.currentState.systemMode) { - patch.systemMode = this.currentState.systemMode; + if (actualSystemMode !== this.internal.systemMode) { + patch.systemMode = this.internal.systemMode; } if (_.size(patch) > 0) { @@ -58,91 +61,105 @@ function createBaseServer>(type: T) { override async setpointRaiseLower( request: Thermostat.SetpointRaiseLowerRequest, ) { - const targetTemperature = this.currentState?.targetTemperature; + const homeAssistant = this.agent.get(HomeAssistantBehavior); + const targetTemperature = this.internal?.targetTemperature; if (targetTemperature == null) { return; } await super.setpointRaiseLower(request); const newTargetTemperature = targetTemperature / 100 + request.amount / 10; - await this.callAction( + await homeAssistant.callAction( "climate", "set_temperature", { temperature: newTargetTemperature }, - { entity_id: this.entity.entity_id }, + { entity_id: homeAssistant.state.entity.entity_id }, ); } protected async targetTemperatureChanged(value: number) { - if (value === this.currentState?.targetTemperature) { + const homeAssistant = this.agent.get(HomeAssistantBehavior); + if (value === this.internal?.targetTemperature) { return; } - await this.callAction( + await homeAssistant.callAction( "climate", "set_temperature", { temperature: value / 100 }, - { entity_id: this.entity.entity_id }, + { entity_id: homeAssistant.state.entity.entity_id }, ); } private async systemModeChanged(systemMode: Thermostat.SystemMode) { - if (systemMode === this.currentState?.systemMode) { + const homeAssistant = this.agent.get(HomeAssistantBehavior); + if (systemMode === this.internal?.systemMode) { return; } - await this.callAction( + await homeAssistant.callAction( "climate", "set_hvac_mode", - { hvac_mode: getHvacMode(systemMode) }, - { entity_id: this.entity.entity_id }, + { hvac_mode: this.getHvacMode(systemMode) }, + { entity_id: homeAssistant.state.entity.entity_id }, ); } - }; -} -export const ThermostatBaseServer = Object.assign(createBaseServer, { - createState: getState, -}); + private getState( + entity: HomeAssistantEntityState, + ): ThermostatBaseServer.Internal { + return { + systemMode: this.getSystemMode(entity.state), + minTemperature: + this.getTemperature(entity.attributes.min_temp) ?? undefined, + maxTemperature: + this.getTemperature(entity.attributes.max_temp) ?? undefined, + currentTemperature: this.getTemperature( + entity.attributes.current_temperature, + ), + targetTemperature: + this.getTemperature(entity.attributes.temperature) ?? 2100, + }; + } -function getState( - entity: HomeAssistantEntityState, -): ThermostatState { - return { - systemMode: getSystemMode(entity.state), - minTemperature: getTemperature(entity.attributes.min_temp) ?? undefined, - maxTemperature: getTemperature(entity.attributes.max_temp) ?? undefined, - currentTemperature: getTemperature(entity.attributes.current_temperature), - targetTemperature: getTemperature(entity.attributes.temperature) ?? 2100, - }; -} + private getSystemMode(state: string | undefined): Thermostat.SystemMode { + switch (state ?? "off") { + case "heat": + return Thermostat.SystemMode.Heat; + case "cool": + return Thermostat.SystemMode.Cool; + } + return Thermostat.SystemMode.Off; + } -function getSystemMode(state: string | undefined): Thermostat.SystemMode { - switch (state ?? "off") { - case "heat": - return Thermostat.SystemMode.Heat; - case "cool": - return Thermostat.SystemMode.Cool; - } - return Thermostat.SystemMode.Off; -} + private getHvacMode(systemMode: Thermostat.SystemMode): ClimateHvacMode { + switch (systemMode) { + case Thermostat.SystemMode.Cool: + return ClimateHvacMode.cool; + case Thermostat.SystemMode.Heat: + return ClimateHvacMode.heat; + default: + return ClimateHvacMode.off; + } + } -function getHvacMode(systemMode: Thermostat.SystemMode): ClimateHvacMode { - switch (systemMode) { - case Thermostat.SystemMode.Cool: - return ClimateHvacMode.cool; - case Thermostat.SystemMode.Heat: - return ClimateHvacMode.heat; - default: - return ClimateHvacMode.off; - } + private getTemperature( + value: number | string | null | undefined, + ): number | null { + const current = value != null ? +value : null; + if (current == null || isNaN(current)) { + return null; + } else { + return current * 100; + } + } + }; } -function getTemperature( - value: number | string | null | undefined, -): number | null { - const current = value != null ? +value : null; - if (current == null || isNaN(current)) { - return null; - } else { - return current * 100; +export namespace ThermostatBaseServer { + export class Internal { + systemMode!: Thermostat.SystemMode; + minTemperature!: number | undefined; + maxTemperature!: number | undefined; + targetTemperature!: number; + currentTemperature!: number | null; } } diff --git a/packages/backend/src/matter/behaviors/window-covering-server.ts b/packages/backend/src/matter/behaviors/window-covering-server.ts index b93dafe..aef7fda 100644 --- a/packages/backend/src/matter/behaviors/window-covering-server.ts +++ b/packages/backend/src/matter/behaviors/window-covering-server.ts @@ -1,13 +1,10 @@ -import { WindowCoveringServer as MBase } from "@project-chip/matter.js/behavior/definitions/window-covering"; -import { haMixin } from "../mixins/ha-mixin.js"; +import { WindowCoveringServer as Base } from "@project-chip/matter.js/behaviors/window-covering"; import { CoverDeviceAttributes, HomeAssistantEntityState, } from "@home-assistant-matter-hub/common"; -import { Behavior } from "@project-chip/matter.js/behavior"; import { WindowCovering } from "@project-chip/matter.js/cluster"; - -const Base = MBase.with("Lift", "PositionAwareLift", "AbsolutePosition"); +import { HomeAssistantBehavior } from "../custom-behaviors/home-assistant-behavior.js"; export interface WindowCoveringServerConfig { lift?: { @@ -19,39 +16,71 @@ export interface WindowCoveringServerConfig { } export function WindowCoveringServer(config?: WindowCoveringServerConfig) { - return class ThisType extends haMixin("WindowCovering", Base) { - override initialize(options?: {}) { - this.endpoint.entityState.subscribe(this.update.bind(this)); - return super.initialize(options); + return class ThisType extends Base.with( + "Lift", + "PositionAwareLift", + "AbsolutePosition", + ) { + override async initialize() { + await super.initialize(); + + const homeAssistant = await this.agent.load(HomeAssistantBehavior); + const initialPercentage = convertLiftValue( + (homeAssistant.state.entity.attributes as CoverDeviceAttributes) + .current_position, + config?.lift, + ); + const initialValue = initialPercentage ? initialPercentage * 100 : null; + this.state.type = WindowCovering.WindowCoveringType.Rollershade; + this.state.configStatus = { + operational: true, + onlineReserved: true, + liftPositionAware: true, + liftMovementReversed: false, + }; + this.state.targetPositionLiftPercent100ths = initialValue; + this.state.currentPositionLiftPercent100ths = initialValue; + this.state.installedOpenLimitLift = 0; + this.state.installedClosedLimitLift = 10000; + this.state.operationalStatus = { + global: WindowCovering.MovementStatus.Stopped, + lift: WindowCovering.MovementStatus.Stopped, + }; + this.state.endProductType = WindowCovering.EndProductType.RollerShade; + this.state.mode = {}; + homeAssistant.onUpdate((s) => this.update(s)); } override async upOrOpen() { await super.upOrOpen(); - await this.callAction( + const homeAssistant = this.agent.get(HomeAssistantBehavior); + await homeAssistant.callAction( "cover", "open_cover", {}, - { entity_id: this.entity.entity_id }, + { entity_id: homeAssistant.state.entity.entity_id }, ); } override async downOrClose() { await super.downOrClose(); - await this.callAction( + const homeAssistant = this.agent.get(HomeAssistantBehavior); + await homeAssistant.callAction( "cover", "close_cover", {}, - { entity_id: this.entity.entity_id }, + { entity_id: homeAssistant.state.entity.entity_id }, ); } override async stopMotion() { super.stopMotion(); - await this.callAction( + const homeAssistant = this.agent.get(HomeAssistantBehavior); + await homeAssistant.callAction( "cover", "stop_cover", {}, - { entity_id: this.entity.entity_id }, + { entity_id: homeAssistant.state.entity.entity_id }, ); } @@ -61,14 +90,15 @@ export function WindowCoveringServer(config?: WindowCoveringServerConfig) { super.goToLiftPercentage(request); const position = this.state.currentPositionLiftPercent100ths!; const targetPosition = convertLiftValue(position / 100, config?.lift); - await this.callAction( + const homeAssistant = this.agent.get(HomeAssistantBehavior); + await homeAssistant.callAction( "cover", "set_cover_position", { position: targetPosition, }, { - entity_id: this.entity.entity_id, + entity_id: homeAssistant.state.entity.entity_id, }, ); } @@ -136,35 +166,3 @@ function convertLiftValue( } return result; } - -export namespace WindowCoveringServer { - export function createState( - state: HomeAssistantEntityState, - config: WindowCoveringServerConfig | undefined, - ): Behavior.Options { - const initialPercentage = convertLiftValue( - state.attributes.current_position, - config?.lift, - ); - const initialValue = initialPercentage ? initialPercentage * 100 : null; - return { - type: WindowCovering.WindowCoveringType.Rollershade, - configStatus: { - operational: true, - onlineReserved: true, - liftPositionAware: true, - liftMovementReversed: false, - }, - targetPositionLiftPercent100ths: initialValue, - currentPositionLiftPercent100ths: initialValue, - installedOpenLimitLift: 0, - installedClosedLimitLift: 10000, - operationalStatus: { - global: WindowCovering.MovementStatus.Stopped, - lift: WindowCovering.MovementStatus.Stopped, - }, - endProductType: WindowCovering.EndProductType.RollerShade, - mode: {}, - }; - } -} diff --git a/packages/backend/src/matter/bridge.ts b/packages/backend/src/matter/bridge.ts index a54cd0a..d8f9f0c 100644 --- a/packages/backend/src/matter/bridge.ts +++ b/packages/backend/src/matter/bridge.ts @@ -84,23 +84,32 @@ export class Bridge extends ServiceBase { await this.createDevices(aggregator); - this.unsubscribeEntities = this.homeAssistant.states( + this.unsubscribeEntities = this.homeAssistant.subscribeStates( this.filter, this.updateDevices.bind(this), ); } private async createDevices(aggregator: Endpoint) { - for (const entity of this.homeAssistant.registry(this.filter)) { + const registryItems = this.homeAssistant.registry(this.filter); + const initialStates = this.homeAssistant.initialStates( + _.keys(registryItems), + ); + + const validItems = _.pickBy( + registryItems, + (r) => initialStates[r.entity_id], + ); + for (const registry of _.values(validItems)) { const device = createDevice( - this.basicInformation, - entity, - this.log, this.homeAssistant, + this.basicInformation, + registry, + initialStates[registry.entity_id], ); if (device) { await aggregator.add(device); - this.matterDevices[entity.entity_id] = device; + this.matterDevices[registry.entity_id] = device; } } } diff --git a/packages/backend/src/matter/create-device.test.ts b/packages/backend/src/matter/create-device.test.ts index 329d29d..e1f33fe 100644 --- a/packages/backend/src/matter/create-device.test.ts +++ b/packages/backend/src/matter/create-device.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it, Mocked, vi } from "vitest"; +import { describe, expect, it, vi } from "vitest"; import { BinarySensorDeviceAttributes, BinarySensorDeviceClass, @@ -9,22 +9,22 @@ import { CoverDeviceAttributes, FanDeviceAttributes, HomeAssistantDomain, - HomeAssistantEntityRegistryWithInitialState, + HomeAssistantEntityRegistry, + HomeAssistantEntityState, LightDeviceAttributes, LightDeviceColorMode, SensorDeviceAttributes, SensorDeviceClass, } from "@home-assistant-matter-hub/common"; import { createDevice } from "./create-device.js"; -import { Logger } from "winston"; -import { HomeAssistantClient } from "../home-assistant/home-assistant-client.js"; import { MatterDevice } from "./matter-device.js"; import { deviceToJson } from "../utils/json/device-to-json.js"; import _ from "lodash"; +import { HomeAssistantActions } from "../home-assistant/home-assistant-actions.js"; const testEntities: Record< HomeAssistantDomain, - HomeAssistantEntityRegistryWithInitialState[] + [HomeAssistantEntityRegistry, HomeAssistantEntityState][] > = { [HomeAssistantDomain.binary_sensor]: [ createEntity("binary_sensor.bs1", "on", { @@ -109,25 +109,15 @@ const basicInformation: BridgeBasicInformation = { hardwareVersion: 4, }; -const MockLogger = vi.fn( - () => - ({ - child: vi.fn(() => new MockLogger()), - defaultMeta: [], - }) as unknown as Mocked, -); - -const MockHomeAssistantClient = vi.fn( - () => ({}) as unknown as Mocked, -); +const mockedActions: HomeAssistantActions = { + callAction: vi.fn(), +}; describe("createDevice", () => { it("should not use any unknown clusterId", () => { - const logger = new MockLogger(); - const homeAssistant = new MockHomeAssistantClient(); const entities = Object.values(testEntities).flat(); - const devices = entities.map((entity) => - createDevice(basicInformation, entity, logger, homeAssistant), + const devices = entities.map(([registry, entity]) => + createDevice(mockedActions, basicInformation, registry, entity), ); const actual = _.uniq( devices @@ -144,8 +134,8 @@ function createEntity( entityId: string, state: string, attributes?: T, -): HomeAssistantEntityRegistryWithInitialState { - return { +): [HomeAssistantEntityRegistry, HomeAssistantEntityState] { + const registry = { categories: {}, entity_id: entityId, has_entity_name: false, @@ -153,13 +143,14 @@ function createEntity( original_name: entityId, platform: "test", unique_id: entityId, - initialState: { - entity_id: entityId, - state, - context: { id: "context" }, - last_changed: "any-change", - last_updated: "any-update", - attributes: attributes ?? {}, - }, }; + const entity = { + entity_id: entityId, + state, + context: { id: "context" }, + last_changed: "any-change", + last_updated: "any-update", + attributes: attributes ?? {}, + }; + return [registry, entity]; } diff --git a/packages/backend/src/matter/create-device.ts b/packages/backend/src/matter/create-device.ts index 898a900..369e635 100644 --- a/packages/backend/src/matter/create-device.ts +++ b/packages/backend/src/matter/create-device.ts @@ -1,57 +1,49 @@ import { BridgeBasicInformation, type HomeAssistantDomain, - HomeAssistantEntityRegistryWithInitialState, + HomeAssistantEntityRegistry, + HomeAssistantEntityState, } from "@home-assistant-matter-hub/common"; -import { createChildLogger } from "../logging/create-child-logger.js"; -import { MatterDevice, MatterDeviceProps } from "./matter-device.js"; +import { MatterDevice } from "./matter-device.js"; import { LightDevice } from "./devices/light-device.js"; import { SwitchDevice } from "./devices/switch-device.js"; import { LockDevice } from "./devices/lock-device.js"; import { FanDevice } from "./devices/fan-device.js"; import { BinarySensorDevice } from "./devices/binary-sensor-device.js"; import { SensorDevice } from "./devices/sensor-device.js"; -import { CoverDevice, CoverDeviceConfig } from "./devices/cover-device.js"; +import { CoverDevice } from "./devices/cover-device.js"; import { ClimateDevice } from "./devices/climate-device.js"; -import { Logger } from "winston"; -import { HomeAssistantClient } from "../home-assistant/home-assistant-client.js"; +import { HomeAssistantBehavior } from "./custom-behaviors/home-assistant-behavior.js"; +import { HomeAssistantActions } from "../home-assistant/home-assistant-actions.js"; export function createDevice( + actions: HomeAssistantActions, basicInformation: BridgeBasicInformation, - entity: HomeAssistantEntityRegistryWithInitialState, - logger: Logger, - homeAssistant: HomeAssistantClient, + registry: HomeAssistantEntityRegistry, + entity: HomeAssistantEntityState, ): MatterDevice | undefined { const domain = entity.entity_id.split(".")[0] as HomeAssistantDomain; const factory = deviceCtrs[domain]; if (!factory) { return undefined; } - return factory(basicInformation, { - logger: createChildLogger(logger, entity.entity_id), - actions: homeAssistant, - entity, - }); + return factory({ actions, basicInformation, registry, entity }); } const deviceCtrs: Record< HomeAssistantDomain, - ( - basicInformation: BridgeBasicInformation, - props: MatterDeviceProps, - ) => MatterDevice | undefined + (homeAssistant: HomeAssistantBehavior.State) => MatterDevice | undefined > = { - light: (b, p) => new LightDevice(b, p), - switch: (b, p) => new SwitchDevice(b, p), - lock: (b, p) => new LockDevice(b, p), - fan: (b, p) => new FanDevice(b, p), - binary_sensor: (b, p) => new BinarySensorDevice(b, p), - sensor: (b, p) => SensorDevice(b, p), - cover: (b, p) => - new CoverDevice(b, p as MatterDeviceProps), - climate: (b, p) => new ClimateDevice(b, p), - input_boolean: (b, p) => new SwitchDevice(b, p), - automation: (b, p) => new SwitchDevice(b, p), - script: (b, p) => new SwitchDevice(b, p), - scene: (b, p) => new SwitchDevice(b, p), + light: (h) => new LightDevice(h), + switch: (h) => new SwitchDevice(h), + lock: (h) => new LockDevice(h), + fan: (h) => new FanDevice(h), + binary_sensor: (h) => new BinarySensorDevice(h), + sensor: (h) => SensorDevice(h), + cover: (h) => new CoverDevice(h), + climate: (h) => ClimateDevice(h), + input_boolean: (h) => new SwitchDevice(h), + automation: (h) => new SwitchDevice(h), + script: (h) => new SwitchDevice(h), + scene: (h) => new SwitchDevice(h), }; diff --git a/packages/backend/src/matter/custom-behaviors/home-assistant-behavior.ts b/packages/backend/src/matter/custom-behaviors/home-assistant-behavior.ts new file mode 100644 index 0000000..8211012 --- /dev/null +++ b/packages/backend/src/matter/custom-behaviors/home-assistant-behavior.ts @@ -0,0 +1,55 @@ +import { Behavior } from "@project-chip/matter.js/behavior"; +import { + BridgeBasicInformation, + HomeAssistantEntityRegistry, + HomeAssistantEntityState, +} from "@home-assistant-matter-hub/common"; +import type { HassServiceTarget } from "home-assistant-js-websocket/dist/types.js"; +import { EventEmitter, MaybePromise } from "@project-chip/matter.js/util"; +import { AsyncObservable } from "../../utils/async-observable.js"; +import { HomeAssistantActions } from "../../home-assistant/home-assistant-actions.js"; + +export class HomeAssistantBehavior extends Behavior { + static override readonly id = "homeAssistant"; + declare state: HomeAssistantBehavior.State; + declare events: HomeAssistantBehavior.Events; + + get entity(): HomeAssistantEntityState { + return this.state.entity; + } + + public onUpdate( + callback: (entity: HomeAssistantEntityState) => MaybePromise, + ) { + this.events.entity$Changed.on(callback); + } + + async callAction( + domain: string, + action: string, + data: object | undefined, + target: HassServiceTarget, + returnResponse?: boolean, + ): Promise { + return this.state.actions.callAction( + domain, + action, + data, + target, + returnResponse, + ); + } +} + +export namespace HomeAssistantBehavior { + export class State { + actions!: HomeAssistantActions; + basicInformation!: BridgeBasicInformation; + registry!: HomeAssistantEntityRegistry; + entity!: HomeAssistantEntityState; + } + + export class Events extends EventEmitter { + entity$Changed = AsyncObservable(); + } +} diff --git a/packages/backend/src/matter/devices/binary-sensor-device.ts b/packages/backend/src/matter/devices/binary-sensor-device.ts index 20541e5..78930ae 100644 --- a/packages/backend/src/matter/devices/binary-sensor-device.ts +++ b/packages/backend/src/matter/devices/binary-sensor-device.ts @@ -1,20 +1,12 @@ -import { MatterDevice, MatterDeviceProps } from "../matter-device.js"; +import { MatterDevice } from "../matter-device.js"; import { BinarySensorDeviceAttributes, BinarySensorDeviceClass, - BridgeBasicInformation, - HomeAssistantEntityRegistryWithInitialState, + HomeAssistantEntityState, } from "@home-assistant-matter-hub/common"; -import { EndpointType } from "@project-chip/matter.js/endpoint/type"; -import { Endpoint } from "@project-chip/matter.js/endpoint"; -import { - contactSensorOptions, - ContactSensorType, -} from "./binary-sensor/contact-sensor.js"; -import { - occupancySensorOptions, - OccupancySensorType, -} from "./binary-sensor/occupancy-sensor.js"; +import { ContactSensorType } from "./binary-sensor/contact-sensor.js"; +import { OccupancySensorType } from "./binary-sensor/occupancy-sensor.js"; +import { HomeAssistantBehavior } from "../custom-behaviors/home-assistant-behavior.js"; const contactTypes: Array = [ BinarySensorDeviceClass.Door, @@ -30,28 +22,19 @@ const occupancyTypes: Array = [ ]; const defaultDeviceType = ContactSensorType; -const defaultDeviceOptions = contactSensorOptions; export class BinarySensorDevice extends MatterDevice { - constructor( - basicInformation: BridgeBasicInformation, - props: MatterDeviceProps, - ) { + constructor(homeAssistant: HomeAssistantBehavior.State) { const entity = - props.entity as HomeAssistantEntityRegistryWithInitialState; - const deviceClass = entity.initialState.attributes.device_class; + homeAssistant.entity as HomeAssistantEntityState; + const deviceClass = entity.attributes.device_class; + + const type = contactTypes.includes(deviceClass) + ? ContactSensorType + : occupancyTypes.includes(deviceClass) + ? OccupancySensorType + : defaultDeviceType; - let type: EndpointType, options: Endpoint.Options; - if (contactTypes.includes(deviceClass)) { - type = ContactSensorType; - options = contactSensorOptions(basicInformation, props); - } else if (occupancyTypes.includes(deviceClass)) { - type = OccupancySensorType; - options = occupancySensorOptions(basicInformation, props); - } else { - type = defaultDeviceType; - options = defaultDeviceOptions(basicInformation, props); - } - super(type, options, props); + super(type, homeAssistant); } } diff --git a/packages/backend/src/matter/devices/binary-sensor/contact-sensor.ts b/packages/backend/src/matter/devices/binary-sensor/contact-sensor.ts index 3673137..34db30c 100644 --- a/packages/backend/src/matter/devices/binary-sensor/contact-sensor.ts +++ b/packages/backend/src/matter/devices/binary-sensor/contact-sensor.ts @@ -1,29 +1,12 @@ -import { MatterDeviceProps } from "../../matter-device.js"; import { ContactSensorDevice } from "@project-chip/matter.js/devices/ContactSensorDevice"; import { BasicInformationServer } from "../../behaviors/basic-information-server.js"; import { IdentifyServer } from "../../behaviors/identify-server.js"; -import { BridgeBasicInformation } from "@home-assistant-matter-hub/common"; -import { Endpoint } from "@project-chip/matter.js/endpoint"; import { BooleanStateServer } from "../../behaviors/boolean-state-server.js"; +import { HomeAssistantBehavior } from "../../custom-behaviors/home-assistant-behavior.js"; export const ContactSensorType = ContactSensorDevice.with( BasicInformationServer, IdentifyServer, + HomeAssistantBehavior, BooleanStateServer(true), ); - -export function contactSensorOptions( - basicInformation: BridgeBasicInformation, - props: MatterDeviceProps, -): Endpoint.Options { - return { - booleanState: BooleanStateServer.createState( - true, - props.entity.initialState, - ), - bridgedDeviceBasicInformation: BasicInformationServer.createState( - basicInformation, - props.entity.initialState, - ), - }; -} diff --git a/packages/backend/src/matter/devices/binary-sensor/occupancy-sensor.ts b/packages/backend/src/matter/devices/binary-sensor/occupancy-sensor.ts index dfdc63d..cd475c5 100644 --- a/packages/backend/src/matter/devices/binary-sensor/occupancy-sensor.ts +++ b/packages/backend/src/matter/devices/binary-sensor/occupancy-sensor.ts @@ -1,28 +1,12 @@ import { OccupancySensorDevice } from "@project-chip/matter.js/devices/OccupancySensorDevice"; import { BasicInformationServer } from "../../behaviors/basic-information-server.js"; import { IdentifyServer } from "../../behaviors/identify-server.js"; -import { BridgeBasicInformation } from "@home-assistant-matter-hub/common"; -import { MatterDeviceProps } from "../../matter-device.js"; -import { Endpoint } from "@project-chip/matter.js/endpoint"; import { OccupancySensingServer } from "../../behaviors/occupancy-sensing-server.js"; +import { HomeAssistantBehavior } from "../../custom-behaviors/home-assistant-behavior.js"; export const OccupancySensorType = OccupancySensorDevice.with( BasicInformationServer, IdentifyServer, + HomeAssistantBehavior, OccupancySensingServer, ); - -export function occupancySensorOptions( - basicInformation: BridgeBasicInformation, - props: MatterDeviceProps, -): Endpoint.Options { - return { - occupancySensing: OccupancySensingServer.createState( - props.entity.initialState, - ), - bridgedDeviceBasicInformation: BasicInformationServer.createState( - basicInformation, - props.entity.initialState, - ), - }; -} diff --git a/packages/backend/src/matter/devices/climate-device.ts b/packages/backend/src/matter/devices/climate-device.ts index 59f6600..c67876f 100644 --- a/packages/backend/src/matter/devices/climate-device.ts +++ b/packages/backend/src/matter/devices/climate-device.ts @@ -1,19 +1,19 @@ -import { MatterDevice, MatterDeviceProps } from "../matter-device.js"; +import { MatterDevice } from "../matter-device.js"; import { ThermostatDevice } from "@project-chip/matter.js/devices/ThermostatDevice"; import { BasicInformationServer } from "../behaviors/basic-information-server.js"; import { IdentifyServer } from "../behaviors/identify-server.js"; import { - BridgeBasicInformation, ClimateDeviceAttributes, ClimateHvacMode, HomeAssistantEntityState, } from "@home-assistant-matter-hub/common"; import { ThermostatServer } from "../behaviors/thermostat-server.js"; -import { Endpoint } from "@project-chip/matter.js/endpoint"; +import { HomeAssistantBehavior } from "../custom-behaviors/home-assistant-behavior.js"; const ClimateDeviceType = ThermostatDevice.with( BasicInformationServer, IdentifyServer, + HomeAssistantBehavior, ); const coolingModes: ClimateHvacMode[] = [ @@ -25,35 +25,21 @@ const heatingModes: ClimateHvacMode[] = [ ClimateHvacMode.heat, ]; -export class ClimateDevice extends MatterDevice { - constructor( - basicInformation: BridgeBasicInformation, - props: MatterDeviceProps, - ) { - const entity = props.entity - .initialState as HomeAssistantEntityState; - const supportsCooling = coolingModes.some((mode) => - entity.attributes.hvac_modes.includes(mode), - ); - const supportsHeating = heatingModes.some((mode) => - entity.attributes.hvac_modes.includes(mode), - ); - const supportsAuto = entity.attributes.hvac_modes.includes( - ClimateHvacMode.auto, - ); - const thermostat = ThermostatServer( - supportsCooling, - supportsHeating, - supportsAuto, - ); - const type = ClimateDeviceType.with(thermostat); - const options: Endpoint.Options = { - bridgedDeviceBasicInformation: BasicInformationServer.createState( - basicInformation, - entity, - ), - thermostat: thermostat.createState(entity), - }; - super(type, options, props); +export function ClimateDevice( + homeAssistant: HomeAssistantBehavior.State, +): MatterDevice | undefined { + const entity = + homeAssistant.entity as HomeAssistantEntityState; + const supportsCooling = coolingModes.some((mode) => + entity.attributes.hvac_modes.includes(mode), + ); + const supportsHeating = heatingModes.some((mode) => + entity.attributes.hvac_modes.includes(mode), + ); + const thermostat = ThermostatServer(supportsCooling, supportsHeating); + if (!thermostat) { + return undefined; } + const type = ClimateDeviceType.with(thermostat); + return new MatterDevice(type, homeAssistant); } diff --git a/packages/backend/src/matter/devices/cover-device.ts b/packages/backend/src/matter/devices/cover-device.ts index 09a7014..d6e76dd 100644 --- a/packages/backend/src/matter/devices/cover-device.ts +++ b/packages/backend/src/matter/devices/cover-device.ts @@ -1,4 +1,4 @@ -import { MatterDevice, MatterDeviceProps } from "../matter-device.js"; +import { MatterDevice } from "../matter-device.js"; import { WindowCoveringDevice } from "@project-chip/matter.js/devices/WindowCoveringDevice"; import { BasicInformationServer } from "../behaviors/basic-information-server.js"; import { IdentifyServer } from "../behaviors/identify-server.js"; @@ -6,33 +6,19 @@ import { WindowCoveringServer, WindowCoveringServerConfig, } from "../behaviors/window-covering-server.js"; -import { BridgeBasicInformation } from "@home-assistant-matter-hub/common"; -import { Endpoint } from "@project-chip/matter.js/endpoint"; +import { HomeAssistantBehavior } from "../custom-behaviors/home-assistant-behavior.js"; const CoverDeviceType = WindowCoveringDevice.with( BasicInformationServer, IdentifyServer, + HomeAssistantBehavior, ); export interface CoverDeviceConfig extends WindowCoveringServerConfig {} export class CoverDevice extends MatterDevice { - constructor( - basicInformation: BridgeBasicInformation, - props: MatterDeviceProps, - ) { - const type = CoverDeviceType.with(WindowCoveringServer(props.deviceConfig)); - const options: Endpoint.Options = { - bridgedDeviceBasicInformation: BasicInformationServer.createState( - basicInformation, - props.entity.initialState, - ), - windowCovering: WindowCoveringServer.createState( - props.entity.initialState, - props.deviceConfig, - ), - }; - - super(type, options, props); + constructor(homeAssistant: HomeAssistantBehavior.State) { + const type = CoverDeviceType.with(WindowCoveringServer()); + super(type, homeAssistant); } } diff --git a/packages/backend/src/matter/devices/fan-device.ts b/packages/backend/src/matter/devices/fan-device.ts index a53202c..27cbbbd 100644 --- a/packages/backend/src/matter/devices/fan-device.ts +++ b/packages/backend/src/matter/devices/fan-device.ts @@ -1,6 +1,5 @@ -import { MatterDevice, MatterDeviceProps } from "../matter-device.js"; +import { MatterDevice } from "../matter-device.js"; import { - BridgeBasicInformation, FanDeviceAttributes, HomeAssistantEntityState, } from "@home-assistant-matter-hub/common"; @@ -12,6 +11,7 @@ import { LevelControlConfig, LevelControlServer, } from "../behaviors/level-control-server.js"; +import { HomeAssistantBehavior } from "../custom-behaviors/home-assistant-behavior.js"; const fanLevelConfig: LevelControlConfig = { getValue: (state: HomeAssistantEntityState) => { @@ -32,28 +32,12 @@ const FanDeviceType = OnOffPlugInUnitDevice.with( IdentifyServer, BasicInformationServer, OnOffServer, + HomeAssistantBehavior, LevelControlServer(fanLevelConfig), ); export class FanDevice extends MatterDevice { - constructor( - basicInformation: BridgeBasicInformation, - props: MatterDeviceProps, - ) { - super( - FanDeviceType, - { - onOff: OnOffServer.createState(props.entity.initialState), - levelControl: LevelControlServer.createState( - fanLevelConfig, - props.entity.initialState, - ), - bridgedDeviceBasicInformation: BasicInformationServer.createState( - basicInformation, - props.entity.initialState, - ), - }, - props, - ); + constructor(homeAssistant: HomeAssistantBehavior.State) { + super(FanDeviceType, homeAssistant); } } diff --git a/packages/backend/src/matter/devices/light-device.ts b/packages/backend/src/matter/devices/light-device.ts index be9a746..ba112fd 100644 --- a/packages/backend/src/matter/devices/light-device.ts +++ b/packages/backend/src/matter/devices/light-device.ts @@ -1,28 +1,14 @@ -import { MatterDevice, MatterDeviceProps } from "../matter-device.js"; +import { MatterDevice } from "../matter-device.js"; import { - BridgeBasicInformation, - HomeAssistantEntityRegistryWithInitialState, + HomeAssistantEntityState, LightDeviceAttributes, LightDeviceColorMode, } from "@home-assistant-matter-hub/common"; -import { - extendedColorLightOptions, - ExtendedColorLightType, -} from "./light/extended-color-light.js"; -import { EndpointType } from "@project-chip/matter.js/endpoint/type"; -import { Endpoint } from "@project-chip/matter.js/endpoint"; -import { - colorTemperatureLightOptions, - ColorTemperatureLightType, -} from "./light/color-temperature-light.js"; -import { - dimmableLightOptions, - DimmableLightType, -} from "./light/dimmable-light.js"; -import { - onOffLightOptions, - OnOffLightType, -} from "./light/on-off-light-device.js"; +import { ExtendedColorLightType } from "./light/extended-color-light.js"; +import { ColorTemperatureLightType } from "./light/color-temperature-light.js"; +import { DimmableLightType } from "./light/dimmable-light.js"; +import { OnOffLightType } from "./light/on-off-light-device.js"; +import { HomeAssistantBehavior } from "../custom-behaviors/home-assistant-behavior.js"; const brightnessModes: LightDeviceColorMode[] = Object.values( LightDeviceColorMode, @@ -39,14 +25,11 @@ const colorModes: LightDeviceColorMode[] = [ ]; export class LightDevice extends MatterDevice { - constructor( - basicInformation: BridgeBasicInformation, - props: MatterDeviceProps, - ) { + constructor(homeAssistant: HomeAssistantBehavior.State) { const entity = - props.entity as HomeAssistantEntityRegistryWithInitialState; + homeAssistant.entity as HomeAssistantEntityState; const supportedColorModes: LightDeviceColorMode[] = - entity.initialState.attributes.supported_color_modes ?? []; + entity.attributes.supported_color_modes ?? []; const supportsBrightness = supportedColorModes.some((mode) => brightnessModes.includes(mode), ); @@ -57,21 +40,14 @@ export class LightDevice extends MatterDevice { LightDeviceColorMode.COLOR_TEMP, ); - let type: EndpointType; - let options: Endpoint.Options; - if (supportsColorControl) { - type = ExtendedColorLightType; - options = extendedColorLightOptions(basicInformation, props); - } else if (supportsColorTemperature) { - type = ColorTemperatureLightType; - options = colorTemperatureLightOptions(basicInformation, props); - } else if (supportsBrightness) { - type = DimmableLightType; - options = dimmableLightOptions(basicInformation, props); - } else { - type = OnOffLightType; - options = onOffLightOptions(basicInformation, props); - } - super(type, options, props); + const type = supportsColorControl + ? ExtendedColorLightType + : supportsColorTemperature + ? ColorTemperatureLightType + : supportsBrightness + ? DimmableLightType + : OnOffLightType; + + super(type, homeAssistant); } } diff --git a/packages/backend/src/matter/devices/light/color-temperature-light.ts b/packages/backend/src/matter/devices/light/color-temperature-light.ts index 40f2a76..0b78b2b 100644 --- a/packages/backend/src/matter/devices/light/color-temperature-light.ts +++ b/packages/backend/src/matter/devices/light/color-temperature-light.ts @@ -1,36 +1,16 @@ import { ColorTemperatureLightDevice as Device } from "@project-chip/matter.js/devices/ColorTemperatureLightDevice"; -import { MatterDeviceProps } from "../../matter-device.js"; import { IdentifyServer } from "../../behaviors/identify-server.js"; import { BasicInformationServer } from "../../behaviors/basic-information-server.js"; import { OnOffServer } from "../../behaviors/on-off-server.js"; -import { BridgeBasicInformation } from "@home-assistant-matter-hub/common"; import { LightLevelControlServer } from "./light-level-control-server.js"; import { ColorTemperatureControlServer } from "../../behaviors/color-temperature-control-server.js"; -import { Endpoint } from "@project-chip/matter.js/endpoint"; +import { HomeAssistantBehavior } from "../../custom-behaviors/home-assistant-behavior.js"; export const ColorTemperatureLightType = Device.with( IdentifyServer, BasicInformationServer, + HomeAssistantBehavior, OnOffServer, LightLevelControlServer, ColorTemperatureControlServer, ); - -export function colorTemperatureLightOptions( - basicInformation: BridgeBasicInformation, - props: MatterDeviceProps, -): Endpoint.Options { - return { - onOff: OnOffServer.createState(props.entity.initialState), - levelControl: LightLevelControlServer.createState( - props.entity.initialState, - ), - colorControl: ColorTemperatureControlServer.createState( - props.entity.initialState, - ), - bridgedDeviceBasicInformation: BasicInformationServer.createState( - basicInformation, - props.entity.initialState, - ), - }; -} diff --git a/packages/backend/src/matter/devices/light/dimmable-light.ts b/packages/backend/src/matter/devices/light/dimmable-light.ts index 4a5aa77..283e376 100644 --- a/packages/backend/src/matter/devices/light/dimmable-light.ts +++ b/packages/backend/src/matter/devices/light/dimmable-light.ts @@ -1,31 +1,14 @@ import { DimmableLightDevice as Device } from "@project-chip/matter.js/devices/DimmableLightDevice"; -import { MatterDeviceProps } from "../../matter-device.js"; import { IdentifyServer } from "../../behaviors/identify-server.js"; import { BasicInformationServer } from "../../behaviors/basic-information-server.js"; import { OnOffServer } from "../../behaviors/on-off-server.js"; -import { BridgeBasicInformation } from "@home-assistant-matter-hub/common"; import { LightLevelControlServer } from "./light-level-control-server.js"; -import { Endpoint } from "@project-chip/matter.js/endpoint"; +import { HomeAssistantBehavior } from "../../custom-behaviors/home-assistant-behavior.js"; export const DimmableLightType = Device.with( IdentifyServer, BasicInformationServer, + HomeAssistantBehavior, OnOffServer, LightLevelControlServer, ); - -export function dimmableLightOptions( - basicInformation: BridgeBasicInformation, - props: MatterDeviceProps, -): Endpoint.Options { - return { - onOff: OnOffServer.createState(props.entity.initialState), - levelControl: LightLevelControlServer.createState( - props.entity.initialState, - ), - bridgedDeviceBasicInformation: BasicInformationServer.createState( - basicInformation, - props.entity.initialState, - ), - }; -} diff --git a/packages/backend/src/matter/devices/light/extended-color-light.ts b/packages/backend/src/matter/devices/light/extended-color-light.ts index c7ba6bb..471e229 100644 --- a/packages/backend/src/matter/devices/light/extended-color-light.ts +++ b/packages/backend/src/matter/devices/light/extended-color-light.ts @@ -1,36 +1,16 @@ import { ExtendedColorLightDevice as Device } from "@project-chip/matter.js/devices/ExtendedColorLightDevice"; -import { MatterDeviceProps } from "../../matter-device.js"; import { IdentifyServer } from "../../behaviors/identify-server.js"; import { BasicInformationServer } from "../../behaviors/basic-information-server.js"; import { OnOffServer } from "../../behaviors/on-off-server.js"; -import { BridgeBasicInformation } from "@home-assistant-matter-hub/common"; import { LightLevelControlServer } from "./light-level-control-server.js"; import { ExtendedColorControlServer } from "../../behaviors/extended-color-control-server.js"; -import { Endpoint } from "@project-chip/matter.js/endpoint"; +import { HomeAssistantBehavior } from "../../custom-behaviors/home-assistant-behavior.js"; export const ExtendedColorLightType = Device.with( IdentifyServer, BasicInformationServer, + HomeAssistantBehavior, OnOffServer, LightLevelControlServer, ExtendedColorControlServer, ); - -export function extendedColorLightOptions( - basicInformation: BridgeBasicInformation, - props: MatterDeviceProps, -): Endpoint.Options { - return { - onOff: OnOffServer.createState(props.entity.initialState), - levelControl: LightLevelControlServer.createState( - props.entity.initialState, - ), - colorControl: ExtendedColorControlServer.createState( - props.entity.initialState, - ), - bridgedDeviceBasicInformation: BasicInformationServer.createState( - basicInformation, - props.entity.initialState, - ), - }; -} diff --git a/packages/backend/src/matter/devices/light/light-level-control-server.ts b/packages/backend/src/matter/devices/light/light-level-control-server.ts index 8229a19..d1a90e7 100644 --- a/packages/backend/src/matter/devices/light/light-level-control-server.ts +++ b/packages/backend/src/matter/devices/light/light-level-control-server.ts @@ -20,11 +20,6 @@ const lightLevelControlConfig: LevelControlConfig = { data: (brightness) => ({ brightness: (brightness / 254) * 255 }), }, }; -export const LightLevelControlServer = Object.assign( - LevelControlServer(lightLevelControlConfig), - { - createState(state: HomeAssistantEntityState) { - return LevelControlServer.createState(lightLevelControlConfig, state); - }, - }, +export const LightLevelControlServer = LevelControlServer( + lightLevelControlConfig, ); diff --git a/packages/backend/src/matter/devices/light/on-off-light-device.ts b/packages/backend/src/matter/devices/light/on-off-light-device.ts index a93ee1c..1044e48 100644 --- a/packages/backend/src/matter/devices/light/on-off-light-device.ts +++ b/packages/backend/src/matter/devices/light/on-off-light-device.ts @@ -1,26 +1,12 @@ import { OnOffLightDevice as Device } from "@project-chip/matter.js/devices/OnOffLightDevice"; -import { MatterDeviceProps } from "../../matter-device.js"; import { IdentifyServer } from "../../behaviors/identify-server.js"; import { BasicInformationServer } from "../../behaviors/basic-information-server.js"; import { OnOffServer } from "../../behaviors/on-off-server.js"; -import { BridgeBasicInformation } from "@home-assistant-matter-hub/common"; -import { Endpoint } from "@project-chip/matter.js/endpoint"; +import { HomeAssistantBehavior } from "../../custom-behaviors/home-assistant-behavior.js"; export const OnOffLightType = Device.with( IdentifyServer, BasicInformationServer, + HomeAssistantBehavior, OnOffServer, ); - -export function onOffLightOptions( - basicInformation: BridgeBasicInformation, - props: MatterDeviceProps, -): Endpoint.Options { - return { - onOff: OnOffServer.createState(props.entity.initialState), - bridgedDeviceBasicInformation: BasicInformationServer.createState( - basicInformation, - props.entity.initialState, - ), - }; -} diff --git a/packages/backend/src/matter/devices/lock-device.ts b/packages/backend/src/matter/devices/lock-device.ts index f7b600b..1ecf86c 100644 --- a/packages/backend/src/matter/devices/lock-device.ts +++ b/packages/backend/src/matter/devices/lock-device.ts @@ -1,31 +1,19 @@ -import { MatterDevice, MatterDeviceProps } from "../matter-device.js"; +import { MatterDevice } from "../matter-device.js"; import { DoorLockDevice } from "@project-chip/matter.js/devices/DoorLockDevice"; import { BasicInformationServer } from "../behaviors/basic-information-server.js"; import { IdentifyServer } from "../behaviors/identify-server.js"; import { LockServer } from "../behaviors/lock-server.js"; -import { BridgeBasicInformation } from "@home-assistant-matter-hub/common"; +import { HomeAssistantBehavior } from "../custom-behaviors/home-assistant-behavior.js"; const LockDeviceType = DoorLockDevice.with( BasicInformationServer, IdentifyServer, + HomeAssistantBehavior, LockServer, ); export class LockDevice extends MatterDevice { - constructor( - basicInformation: BridgeBasicInformation, - props: MatterDeviceProps, - ) { - super( - LockDeviceType, - { - doorLock: LockServer.createState(props.entity.initialState), - bridgedDeviceBasicInformation: BasicInformationServer.createState( - basicInformation, - props.entity.initialState, - ), - }, - props, - ); + constructor(homeAssistant: HomeAssistantBehavior.State) { + super(LockDeviceType, homeAssistant); } } diff --git a/packages/backend/src/matter/devices/sensor-device.ts b/packages/backend/src/matter/devices/sensor-device.ts index 12cc79f..ae2e73c 100644 --- a/packages/backend/src/matter/devices/sensor-device.ts +++ b/packages/backend/src/matter/devices/sensor-device.ts @@ -1,41 +1,28 @@ -import { MatterDevice, MatterDeviceProps } from "../matter-device.js"; +import { MatterDevice } from "../matter-device.js"; import { - BridgeBasicInformation, - HomeAssistantEntityRegistryWithInitialState, + HomeAssistantEntityState, SensorDeviceAttributes, SensorDeviceClass, } from "@home-assistant-matter-hub/common"; -import { EndpointType } from "@project-chip/matter.js/endpoint/type"; -import { Endpoint } from "@project-chip/matter.js/endpoint"; -import { - temperatureSensorOptions, - TemperatureSensorType, -} from "./sensor/temperature-sensor.js"; -import { - humiditySensorOptions, - HumiditySensorType, -} from "./sensor/humidity-sensor.js"; +import { TemperatureSensorType } from "./sensor/temperature-sensor.js"; +import { HumiditySensorType } from "./sensor/humidity-sensor.js"; +import { HomeAssistantBehavior } from "../custom-behaviors/home-assistant-behavior.js"; export function SensorDevice( - basicInformation: BridgeBasicInformation, - props: MatterDeviceProps, + homeAssistant: HomeAssistantBehavior.State, ): MatterDevice | undefined { const entity = - props.entity as HomeAssistantEntityRegistryWithInitialState; - const deviceClass = entity.initialState.attributes.device_class; + homeAssistant.entity as HomeAssistantEntityState; + const deviceClass = entity.attributes.device_class; + + const type = + deviceClass === SensorDeviceClass.temperature + ? TemperatureSensorType + : deviceClass === SensorDeviceClass.humidity + ? HumiditySensorType + : undefined; + + if (!type) return undefined; - let type: EndpointType | undefined; - let options: Endpoint.Options | undefined; - if (deviceClass === SensorDeviceClass.temperature) { - type = TemperatureSensorType; - options = temperatureSensorOptions(basicInformation, props); - } else if (deviceClass === SensorDeviceClass.humidity) { - type = HumiditySensorType; - options = humiditySensorOptions(basicInformation, props); - } - if (type && options) { - return new MatterDevice(type, options, props); - } else { - return undefined; - } + return new MatterDevice(type, homeAssistant); } diff --git a/packages/backend/src/matter/devices/sensor/humidity-sensor.ts b/packages/backend/src/matter/devices/sensor/humidity-sensor.ts index de0f836..a4a7e97 100644 --- a/packages/backend/src/matter/devices/sensor/humidity-sensor.ts +++ b/packages/backend/src/matter/devices/sensor/humidity-sensor.ts @@ -1,28 +1,12 @@ import { BasicInformationServer } from "../../behaviors/basic-information-server.js"; import { IdentifyServer } from "../../behaviors/identify-server.js"; -import { BridgeBasicInformation } from "@home-assistant-matter-hub/common"; -import { MatterDeviceProps } from "../../matter-device.js"; -import { Endpoint } from "@project-chip/matter.js/endpoint"; import { HumiditySensorDevice } from "@project-chip/matter.js/devices/HumiditySensorDevice"; import { HumidityMeasurementServer } from "../../behaviors/humidity-measurement-server.js"; +import { HomeAssistantBehavior } from "../../custom-behaviors/home-assistant-behavior.js"; export const HumiditySensorType = HumiditySensorDevice.with( BasicInformationServer, IdentifyServer, + HomeAssistantBehavior, HumidityMeasurementServer, ); - -export function humiditySensorOptions( - basicInformation: BridgeBasicInformation, - props: MatterDeviceProps, -): Endpoint.Options { - return { - bridgedDeviceBasicInformation: BasicInformationServer.createState( - basicInformation, - props.entity.initialState, - ), - relativeHumidityMeasurement: HumidityMeasurementServer.createState( - props.entity.initialState, - ), - }; -} diff --git a/packages/backend/src/matter/devices/sensor/temperature-sensor.ts b/packages/backend/src/matter/devices/sensor/temperature-sensor.ts index 349f682..91d1eca 100644 --- a/packages/backend/src/matter/devices/sensor/temperature-sensor.ts +++ b/packages/backend/src/matter/devices/sensor/temperature-sensor.ts @@ -2,27 +2,11 @@ import { TemperatureSensorDevice } from "@project-chip/matter.js/devices/Tempera import { BasicInformationServer } from "../../behaviors/basic-information-server.js"; import { IdentifyServer } from "../../behaviors/identify-server.js"; import { TemperatureMeasurementServer } from "../../behaviors/temperature-measurement-server.js"; -import { BridgeBasicInformation } from "@home-assistant-matter-hub/common"; -import { MatterDeviceProps } from "../../matter-device.js"; -import { Endpoint } from "@project-chip/matter.js/endpoint"; +import { HomeAssistantBehavior } from "../../custom-behaviors/home-assistant-behavior.js"; export const TemperatureSensorType = TemperatureSensorDevice.with( BasicInformationServer, IdentifyServer, + HomeAssistantBehavior, TemperatureMeasurementServer, ); - -export function temperatureSensorOptions( - basicInformation: BridgeBasicInformation, - props: MatterDeviceProps, -): Endpoint.Options { - return { - bridgedDeviceBasicInformation: BasicInformationServer.createState( - basicInformation, - props.entity.initialState, - ), - temperatureMeasurement: TemperatureMeasurementServer.createState( - props.entity.initialState, - ), - }; -} diff --git a/packages/backend/src/matter/devices/switch-device.ts b/packages/backend/src/matter/devices/switch-device.ts index 4d1ec2c..032b105 100644 --- a/packages/backend/src/matter/devices/switch-device.ts +++ b/packages/backend/src/matter/devices/switch-device.ts @@ -1,31 +1,19 @@ -import { MatterDevice, MatterDeviceProps } from "../matter-device.js"; -import { BridgeBasicInformation } from "@home-assistant-matter-hub/common"; +import { MatterDevice } from "../matter-device.js"; import { OnOffPlugInUnitDevice } from "@project-chip/matter.js/devices/OnOffPlugInUnitDevice"; import { OnOffServer } from "../behaviors/on-off-server.js"; import { BasicInformationServer } from "../behaviors/basic-information-server.js"; import { IdentifyServer } from "../behaviors/identify-server.js"; +import { HomeAssistantBehavior } from "../custom-behaviors/home-assistant-behavior.js"; const SwitchEndpointType = OnOffPlugInUnitDevice.with( - IdentifyServer, BasicInformationServer, + IdentifyServer, + HomeAssistantBehavior, OnOffServer, ); export class SwitchDevice extends MatterDevice { - constructor( - basicInformation: BridgeBasicInformation, - props: MatterDeviceProps, - ) { - super( - SwitchEndpointType, - { - onOff: OnOffServer.createState(props.entity.initialState), - bridgedDeviceBasicInformation: BasicInformationServer.createState( - basicInformation, - props.entity.initialState, - ), - }, - props, - ); + constructor(homeAssistant: HomeAssistantBehavior.State) { + super(SwitchEndpointType, homeAssistant); } } diff --git a/packages/backend/src/matter/matter-device.ts b/packages/backend/src/matter/matter-device.ts index 8273129..75fc5e9 100644 --- a/packages/backend/src/matter/matter-device.ts +++ b/packages/backend/src/matter/matter-device.ts @@ -1,47 +1,33 @@ -import { - HomeAssistantEntityRegistryWithInitialState, - HomeAssistantEntityState, -} from "@home-assistant-matter-hub/common"; -import { Endpoint } from "@project-chip/matter.js/endpoint"; -import { EndpointType } from "@project-chip/matter.js/endpoint/type"; -import { HomeAssistantActions } from "../home-assistant/home-assistant-actions.js"; -import { Observable, Subject } from "rxjs"; -import { Logger } from "winston"; - -export interface MatterDeviceProps { - logger: Logger; - actions: HomeAssistantActions; - entity: HomeAssistantEntityRegistryWithInitialState; - deviceConfig?: TDeviceConfig; -} +import { HomeAssistantEntityState } from "@home-assistant-matter-hub/common"; +import { Endpoint, EndpointType } from "@project-chip/matter.js/endpoint"; +import { HomeAssistantBehavior } from "./custom-behaviors/home-assistant-behavior.js"; +import { Behavior } from "@project-chip/matter.js/behavior"; export class MatterDevice< T extends EndpointType = EndpointType.Empty, -> extends Endpoint { - private readonly state$ = new Subject(); - get entityState(): Observable { - return this.state$.asObservable(); - } +> extends Endpoint { + public readonly entityId: string; - readonly logger: Logger; - readonly actions: HomeAssistantActions; - readonly entity: HomeAssistantEntityRegistryWithInitialState; + constructor(type: T, homeAssistant: HomeAssistantBehavior.State) { + const entityId = homeAssistant.registry.entity_id; + if (!(HomeAssistantBehavior.id in type.behaviors)) { + throw new Error( + `${type.name} does not utilize HomeAssistantBehavior (${entityId})`, + ); + } - constructor(type: T, options: Endpoint.Options, props: MatterDeviceProps) { - super(type, { - id: props.entity.entity_id.replace(/\./g, "_"), - ...options, - }); - this.logger = props.logger; - this.actions = props.actions; - this.entity = props.entity; + const newOptions: { + id: string; + homeAssistant: Behavior.StateOf; + } = { + id: entityId.replace(/\./g, "_"), + homeAssistant, + }; + super(type, newOptions); + this.entityId = entityId; } async update(state: HomeAssistantEntityState) { - this.logger.silly( - "Update from HomeAssistant:\n%s", - JSON.stringify(state, null, 2), - ); - this.state$.next(state); + await this.setStateOf(HomeAssistantBehavior, { entity: state }); } } diff --git a/packages/backend/src/matter/mixins/ha-mixin.ts b/packages/backend/src/matter/mixins/ha-mixin.ts deleted file mode 100644 index ea4764b..0000000 --- a/packages/backend/src/matter/mixins/ha-mixin.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { Behavior } from "@project-chip/matter.js/behavior"; -import type { MatterDevice } from "../matter-device.js"; -import { createChildLogger } from "../../logging/create-child-logger.js"; -import { Type } from "@home-assistant-matter-hub/common"; - -export function haMixin>(name: string, type: T) { - return class HaMixin extends type { - override get endpoint(): MatterDevice { - return super.endpoint as MatterDevice; - } - - readonly logger = createChildLogger( - this.endpoint.logger, - `${this.entity.entity_id} / ${name}`, - ); - - get entity() { - return this.endpoint.entity; - } - - callAction = this.endpoint.actions.callAction.bind(this.endpoint.actions); - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - constructor(...args: any[]) { - super(...args); - } - }; -} diff --git a/packages/backend/src/utils/async-observable.ts b/packages/backend/src/utils/async-observable.ts new file mode 100644 index 0000000..3ead017 --- /dev/null +++ b/packages/backend/src/utils/async-observable.ts @@ -0,0 +1,5 @@ +import { ActionContext } from "@project-chip/matter.js/behavior"; +import { AsyncObservable as Base } from "@project-chip/matter.js/util"; + +export const AsyncObservable = () => + Base<[value: T, oldValue: T, context: ActionContext]>(); diff --git a/packages/backend/src/utils/json/device-to-json.ts b/packages/backend/src/utils/json/device-to-json.ts index 914d801..bbe5fb0 100644 --- a/packages/backend/src/utils/json/device-to-json.ts +++ b/packages/backend/src/utils/json/device-to-json.ts @@ -1,11 +1,18 @@ import { MatterDevice } from "../../matter/matter-device.js"; import { DeviceData } from "@home-assistant-matter-hub/common"; +import _ from "lodash"; +import { HomeAssistantBehavior } from "../../matter/custom-behaviors/home-assistant-behavior.js"; export function deviceToJson(device: MatterDevice): DeviceData { + const behaviors = _.pickBy( + device.behaviors.supported, + (b) => b.id !== HomeAssistantBehavior.id, + ); + const state = _.mapValues(behaviors, (b) => device.stateOf(b)); return { - entityId: device.entity.entity_id, + entityId: device.entityId, endpointCode: device.type.deviceType.toString(16), endpointType: device.type.name, - state: device.state, + state, }; } diff --git a/packages/backend/tsconfig.json b/packages/backend/tsconfig.json index 8fce8c1..72ee51f 100644 --- a/packages/backend/tsconfig.json +++ b/packages/backend/tsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { // Compiler - "target": "ES2020", + "target": "ES2022", "module": "NodeNext", "moduleResolution": "NodeNext", "noEmit": true, diff --git a/packages/common/package.json b/packages/common/package.json index c450084..bdc3046 100644 --- a/packages/common/package.json +++ b/packages/common/package.json @@ -6,6 +6,7 @@ "main": "dist/index.js", "types": "dist/index.d.ts", "scripts": { + "cleanup": "rimraf dist", "lint": "eslint .", "lint:fix": "eslint . --fix", "build": "tsc" diff --git a/packages/common/src/home-assistant-entity-registry.ts b/packages/common/src/home-assistant-entity-registry.ts index 0c7f2c4..62d0632 100644 --- a/packages/common/src/home-assistant-entity-registry.ts +++ b/packages/common/src/home-assistant-entity-registry.ts @@ -1,5 +1,3 @@ -import { HomeAssistantEntityState } from "./home-assistant-entity-state.js"; - export interface HomeAssistantEntityRegistry { area_id?: string; categories: Record; @@ -24,9 +22,3 @@ export interface HomeAssistantEntityRegistry { translation_key?: unknown; unique_id: string; } - -export interface HomeAssistantEntityRegistryWithInitialState< - T extends object = {}, -> extends HomeAssistantEntityRegistry { - initialState: HomeAssistantEntityState; -} diff --git a/packages/documentation/package.json b/packages/documentation/package.json index d48a585..698e598 100644 --- a/packages/documentation/package.json +++ b/packages/documentation/package.json @@ -4,6 +4,7 @@ "private": true, "type": "module", "scripts": { + "cleanup": "rimraf generated/pages", "build": "node generate.js" }, "dependencies": {}, diff --git a/packages/frontend/package.json b/packages/frontend/package.json index 7fc66a0..e70d703 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -5,6 +5,7 @@ "type": "module", "destination": "./dist", "scripts": { + "cleanup": "rimraf dist", "dev": "vite", "serve": "vite", "build": "tsc -b && vite build", diff --git a/packages/frontend/src/components/cluster/states/ThermostatState.tsx b/packages/frontend/src/components/cluster/states/ThermostatState.tsx index 453d98c..ca18a70 100644 --- a/packages/frontend/src/components/cluster/states/ThermostatState.tsx +++ b/packages/frontend/src/components/cluster/states/ThermostatState.tsx @@ -46,7 +46,7 @@ export const ThermostatState = ({ state }: ThermostatStateProps) => { {Labels[state.systemMode ?? ThermostatSystemMode.Off]} {state.localTemperature != null && ( - {state.localTemperature / 100} °C + , {state.localTemperature / 100} °C )} );