diff --git a/packages/backend/src/home-assistant/home-assistant-client.ts b/packages/backend/src/home-assistant/home-assistant-client.ts index bf609275..e8378afb 100644 --- a/packages/backend/src/home-assistant/home-assistant-client.ts +++ b/packages/backend/src/home-assistant/home-assistant-client.ts @@ -7,6 +7,8 @@ import { } from "home-assistant-js-websocket"; import { Environment, Environmental } from "@matter/main"; import { register } from "../environment/register.js"; +import { Logger } from "winston"; +import { createLogger } from "../logging/create-logger.js"; export interface HomeAssistantClientProps { readonly url: string; @@ -14,6 +16,7 @@ export interface HomeAssistantClientProps { } export class HomeAssistantClient implements Environmental.Service { + private readonly log: Logger; readonly construction: Promise; public connection!: Connection; @@ -21,36 +24,42 @@ export class HomeAssistantClient implements Environmental.Service { environment: Environment, private readonly props: HomeAssistantClientProps, ) { + this.log = createLogger("HomeAssistantClient"); register(environment, HomeAssistantClient, this); this.construction = this.initialize(); } - private async initialize() { - this.connection = await createConnection({ - auth: createLongLivedTokenAuth( - this.props.url.replace(/\/$/, ""), - this.props.accessToken, - ), - }).catch((reason) => { - throw this.parseError(reason); - }); - } - - async [Symbol.asyncDispose]() { - this.connection?.close(); + private async initialize(): Promise { + try { + this.connection?.close(); + this.connection = await createConnection({ + auth: createLongLivedTokenAuth( + this.props.url.replace(/\/$/, ""), + this.props.accessToken, + ), + }); + } catch (reason: unknown) { + return this.handleInitializationError(reason); + } } - private parseError(reason: unknown): Error { + private async handleInitializationError(reason: unknown): Promise { if (reason === ERR_CANNOT_CONNECT) { - return new Error( - `Unable to connect to home assistant with url: ${this.props.url}`, + this.log.error( + `Unable to connect to home assistant with url: ${this.props.url}. Retrying in 5 seconds...`, ); + await new Promise((resolve) => setTimeout(resolve, 5000)); + return this.initialize(); } else if (reason === ERR_INVALID_AUTH) { - return new Error( + throw new Error( "Authentication failed while connecting to home assistant", ); } else { - return new Error(`Unable to connect to home assistant: ${reason}`); + throw new Error(`Unable to connect to home assistant: ${reason}`); } } + + async [Symbol.asyncDispose]() { + this.connection?.close(); + } } diff --git a/packages/backend/src/home-assistant/home-assistant-config.ts b/packages/backend/src/home-assistant/home-assistant-config.ts new file mode 100644 index 00000000..7428e42e --- /dev/null +++ b/packages/backend/src/home-assistant/home-assistant-config.ts @@ -0,0 +1,29 @@ +import { Environment, Environmental } from "@matter/main"; +import { register } from "../environment/register.js"; +import { HomeAssistantClient } from "./home-assistant-client.js"; +import { getConfig, HassConfig } from "home-assistant-js-websocket"; + +export class HomeAssistantConfig implements Environmental.Service { + static [Environmental.create](environment: Environment) { + return new this(environment); + } + + private readonly environment: Environment; + readonly construction: Promise; + private config!: HassConfig; + + get unitSystem() { + return this.config.unit_system; + } + + constructor(environment: Environment) { + register(environment, HomeAssistantConfig, this); + this.environment = environment; + this.construction = this.initialize(); + } + + private async initialize(): Promise { + const { connection } = await this.environment.load(HomeAssistantClient); + this.config = await getConfig(connection); + } +} diff --git a/packages/backend/src/matter/behaviors/fan-control-server.ts b/packages/backend/src/matter/behaviors/fan-control-server.ts index 9815a9a6..0001dade 100644 --- a/packages/backend/src/matter/behaviors/fan-control-server.ts +++ b/packages/backend/src/matter/behaviors/fan-control-server.ts @@ -7,7 +7,7 @@ import { import { HomeAssistantEntityBehavior } from "../custom-behaviors/home-assistant-entity-behavior.js"; import { applyPatchState } from "../../utils/apply-patch-state.js"; import { ClusterType } from "@matter/main/types"; -import * as utils from "./fan-control-server-utils.js"; +import * as utils from "./utils/fan-control-server-utils.js"; import { BridgeDataProvider } from "../bridge/bridge-data-provider.js"; const FeaturedBase = Base.with( diff --git a/packages/backend/src/matter/behaviors/temperature-measurement-server.ts b/packages/backend/src/matter/behaviors/temperature-measurement-server.ts index 15a9c112..20807513 100644 --- a/packages/backend/src/matter/behaviors/temperature-measurement-server.ts +++ b/packages/backend/src/matter/behaviors/temperature-measurement-server.ts @@ -5,6 +5,7 @@ import { } from "@home-assistant-matter-hub/common"; import { HomeAssistantEntityBehavior } from "../custom-behaviors/home-assistant-entity-behavior.js"; import { applyPatchState } from "../../utils/apply-patch-state.js"; +import { convertTemperatureToCelsius } from "./utils/temperature-utils.js"; export interface TemperatureMeasurementConfig { getValue: (state: HomeAssistantEntityState) => number | null; @@ -32,21 +33,11 @@ export class TemperatureMeasurementServer extends Base { private getTemperature(entity: HomeAssistantEntityState): number | null { const value = this.state.config.getValue(entity); const unitOfMeasurement = this.state.config.getUnitOfMeasurement?.(entity); - if (value == null) { + const celsius = convertTemperatureToCelsius(value, unitOfMeasurement); + if (celsius == null) { return null; - } - switch (unitOfMeasurement) { - case "°F": - return (value - 32) * (5 / 9) * 100; - case "K": - return (value - 273.15) * 100; - case "°C": - case "": - case null: - case undefined: - return value * 100; - default: - return null; + } else { + return celsius * 100; } } } diff --git a/packages/backend/src/matter/behaviors/thermostat-server.ts b/packages/backend/src/matter/behaviors/thermostat-server.ts index 54ced35c..f7eb262f 100644 --- a/packages/backend/src/matter/behaviors/thermostat-server.ts +++ b/packages/backend/src/matter/behaviors/thermostat-server.ts @@ -7,16 +7,21 @@ import { } from "@home-assistant-matter-hub/common"; import { ClusterType } from "@matter/main/types"; import { applyPatchState } from "../../utils/apply-patch-state.js"; -import * as utils from "./thermostat-server-utils.js"; +import * as utils from "./utils/thermostat-server-utils.js"; +import { HomeAssistantConfig } from "../../home-assistant/home-assistant-config.js"; const FeaturedBase = Base.with("Heating", "Cooling", "AutoMode"); export class ThermostatServerBase extends FeaturedBase { declare state: ThermostatServerBase.State; + declare internal: ThermostatServerBase.Internal; override async initialize() { await super.initialize(); const homeAssistant = await this.agent.load(HomeAssistantEntityBehavior); + const config = await this.env.load(HomeAssistantConfig); + this.internal.homeAssistantUnit = config.unitSystem.temperature; + this.update(homeAssistant.entity); this.reactTo(this.events.systemMode$Changed, this.systemModeChanged); if (this.features.cooling) { @@ -36,12 +41,19 @@ export class ThermostatServerBase extends FeaturedBase { private update(entity: HomeAssistantEntityInformation) { const attributes = entity.state.attributes as ClimateDeviceAttributes; - const minSetpointLimit = utils.toMatterTemperature(attributes.min_temp); - const maxSetpointLimit = utils.toMatterTemperature(attributes.max_temp); + const unit = this.internal.homeAssistantUnit; + const minSetpointLimit = utils.toMatterTemperature( + attributes.min_temp, + unit, + ); + const maxSetpointLimit = utils.toMatterTemperature( + attributes.max_temp, + unit, + ); applyPatchState(this.state, { localTemperature: - utils.toMatterTemperature(attributes.current_temperature) ?? null, + utils.toMatterTemperature(attributes.current_temperature, unit) ?? null, systemMode: utils.getMatterSystemMode( attributes.hvac_mode ?? entity.state.state, this.features, @@ -184,6 +196,7 @@ export class ThermostatServerBase extends FeaturedBase { attributes.target_temp_low ?? attributes.target_temperature ?? attributes.temperature, + this.internal.homeAssistantUnit, ); } @@ -192,12 +205,17 @@ export class ThermostatServerBase extends FeaturedBase { attributes.target_temp_high ?? attributes.target_temperature ?? attributes.temperature, + this.internal.homeAssistantUnit, ); } } export namespace ThermostatServerBase { export class State extends FeaturedBase.State {} + + export class Internal extends FeaturedBase.Internal { + homeAssistantUnit!: string; + } } export class ThermostatServer extends ThermostatServerBase.for( diff --git a/packages/backend/src/matter/behaviors/fan-control-server-utils.test.ts b/packages/backend/src/matter/behaviors/utils/fan-control-server-utils.test.ts similarity index 100% rename from packages/backend/src/matter/behaviors/fan-control-server-utils.test.ts rename to packages/backend/src/matter/behaviors/utils/fan-control-server-utils.test.ts diff --git a/packages/backend/src/matter/behaviors/fan-control-server-utils.ts b/packages/backend/src/matter/behaviors/utils/fan-control-server-utils.ts similarity index 100% rename from packages/backend/src/matter/behaviors/fan-control-server-utils.ts rename to packages/backend/src/matter/behaviors/utils/fan-control-server-utils.ts diff --git a/packages/backend/src/matter/behaviors/utils/temperature-utils.ts b/packages/backend/src/matter/behaviors/utils/temperature-utils.ts new file mode 100644 index 00000000..bdd7d5fd --- /dev/null +++ b/packages/backend/src/matter/behaviors/utils/temperature-utils.ts @@ -0,0 +1,27 @@ +/** + * Convert any temperature (C, F, K) to Celsius. + * If unit is null or undefined, it is considered to be "°C" + * @param value the temperature + * @param unit the unit of measurement (°C, °F, K). + */ +export function convertTemperatureToCelsius( + value: number | null | undefined, + unit: string | null | undefined, +): number | null { + if (value == null || isNaN(value)) { + return null; + } + switch (unit) { + case "°F": + return (value - 32) * (5 / 9); + case "K": + return value - 273.15; + case "°C": + case "": + case null: + case undefined: + return value; + default: + return null; + } +} diff --git a/packages/backend/src/matter/behaviors/thermostat-server-utils.test.ts b/packages/backend/src/matter/behaviors/utils/thermostat-server-utils.test.ts similarity index 91% rename from packages/backend/src/matter/behaviors/thermostat-server-utils.test.ts rename to packages/backend/src/matter/behaviors/utils/thermostat-server-utils.test.ts index 20e42099..b36c70ea 100644 --- a/packages/backend/src/matter/behaviors/thermostat-server-utils.test.ts +++ b/packages/backend/src/matter/behaviors/utils/thermostat-server-utils.test.ts @@ -111,15 +111,17 @@ describe("ThermostatServerUtils", () => { describe("toMatterTemperature", () => { it.each([ - [100, 100_00], - [85, 85_00], - [23.4581, 23_46], - ["22.212", 22_21], - ["not a number", undefined], - [undefined, undefined], - [null, undefined], - ])("should convert '%s' to '%s'", (temperature, expected) => { - expect(toMatterTemperature(temperature)).toEqual(expected); + [100, "°C", 100_00], + [85, "°C", 85_00], + [294.15, "K", 21_00], + [70, "°F", 21_11], + [23.4581, "°C", 23_46], + ["22.212", "°C", 22_21], + ["not a number", "°C", undefined], + [undefined, "°C", undefined], + [null, "°C", undefined], + ])("should convert '%s %s' to '%s'", (temperature, unit, expected) => { + expect(toMatterTemperature(temperature, unit)).toEqual(expected); }); }); }); diff --git a/packages/backend/src/matter/behaviors/thermostat-server-utils.ts b/packages/backend/src/matter/behaviors/utils/thermostat-server-utils.ts similarity index 92% rename from packages/backend/src/matter/behaviors/thermostat-server-utils.ts rename to packages/backend/src/matter/behaviors/utils/thermostat-server-utils.ts index 64fbb84d..798d7cff 100644 --- a/packages/backend/src/matter/behaviors/thermostat-server-utils.ts +++ b/packages/backend/src/matter/behaviors/utils/thermostat-server-utils.ts @@ -3,6 +3,7 @@ import { ClimateHvacMode, } from "@home-assistant-matter-hub/common"; import { Thermostat } from "@matter/main/clusters"; +import { convertTemperatureToCelsius } from "./temperature-utils.js"; export interface ThermostatFeatures { autoMode: boolean; @@ -151,15 +152,18 @@ export function getMatterSystemMode( /** * Convert the temperature from home assistant to matter compatible values - * @param value + * @param value the temperature from home assistant + * @param unitOfMeasurement unit of measurement of the given temperature */ export function toMatterTemperature( value: number | string | null | undefined, + unitOfMeasurement: string, ): number | undefined { const current = value != null ? +value : null; - if (current == null || isNaN(current)) { + const celsius = convertTemperatureToCelsius(current, unitOfMeasurement); + if (celsius == null) { return undefined; } else { - return Math.round(current * 100); + return Math.round(celsius * 100); } } diff --git a/packages/backend/src/matter/behaviors/window-covering-server.ts b/packages/backend/src/matter/behaviors/window-covering-server.ts index a9aad29d..ed38460d 100644 --- a/packages/backend/src/matter/behaviors/window-covering-server.ts +++ b/packages/backend/src/matter/behaviors/window-covering-server.ts @@ -14,7 +14,13 @@ import { HomeAssistantEntityBehavior } from "../custom-behaviors/home-assistant- import { applyPatchState } from "../../utils/apply-patch-state.js"; import { ClusterType } from "@matter/main/types"; -const FeaturedBase = Base.with("Lift", "PositionAwareLift", "AbsolutePosition"); +const FeaturedBase = Base.with( + "Lift", + "PositionAwareLift", + "AbsolutePosition", + "Tilt", + "PositionAwareTilt", +); export class WindowCoveringServerBase extends FeaturedBase { declare state: WindowCoveringServerBase.State; @@ -37,30 +43,37 @@ export class WindowCoveringServerBase extends FeaturedBase { ? WindowCovering.MovementStatus.Closing : WindowCovering.MovementStatus.Stopped; - let currentLift = this.convertLiftValue(state.attributes.current_position); - if (currentLift != null) { - currentLift *= 100; - } else { - if (coverState === CoverDeviceState.open) { - currentLift = 0; - } else if (state.state === "closed") { - currentLift = 100_00; - } - } + const currentLift = this.getCurrentPosition( + state.attributes.current_position, + coverState, + ); + const currentTilt = this.getCurrentPosition( + state.attributes.current_tilt_position, + coverState, + ); + applyPatchState(this.state, { type: WindowCovering.WindowCoveringType.Rollershade, endProductType: WindowCovering.EndProductType.RollerShade, operationalStatus: { global: movementStatus, - lift: movementStatus, + ...(this.features.lift ? { lift: movementStatus } : {}), + ...(this.features.tilt ? { tilt: movementStatus } : {}), }, - ...(this.features.absolutePosition + ...(this.features.absolutePosition && this.features.lift ? { installedOpenLimitLift: 0, - installedCloseLimit: 100_00, + installedClosedLimitLift: 100_00, currentPositionLift: currentLift, } : {}), + ...(this.features.absolutePosition && this.features.tilt + ? { + installedOpenLimitTilt: 0, + installedClosedLimitTilt: 100_00, + currentPositionTilt: currentLift, + } + : {}), ...(this.features.positionAwareLift ? { currentPositionLiftPercent100ths: currentLift, @@ -68,9 +81,33 @@ export class WindowCoveringServerBase extends FeaturedBase { this.state.targetPositionLiftPercent100ths ?? currentLift, } : {}), + ...(this.features.positionAwareTilt + ? { + currentPositionTiltPercent100ths: currentTilt, + targetPositionTiltPercent100ths: + this.state.targetPositionTiltPercent100ths ?? currentTilt, + } + : {}), }); } + private getCurrentPosition( + percentage: number | undefined, + coverState: CoverDeviceState, + ) { + let currentValue = this.invertValue(percentage); + if (currentValue != null) { + currentValue *= 100; + } else { + if (coverState === CoverDeviceState.open) { + currentValue = 0; + } else if (coverState === CoverDeviceState.closed) { + currentValue = 100_00; + } + } + return currentValue; + } + override async handleMovement( type: MovementType, _: boolean, @@ -79,14 +116,25 @@ export class WindowCoveringServerBase extends FeaturedBase { ) { if (type === MovementType.Lift) { if (targetPercent100ths != null && this.features.absolutePosition) { - await this.handleGoToPosition(targetPercent100ths); + await this.handleGoToLiftPosition(targetPercent100ths); + } else if ( + direction === MovementDirection.Close || + (targetPercent100ths != null && targetPercent100ths > 0) + ) { + await this.handleLiftClose(); + } else if (direction === MovementDirection.Open) { + await this.handleLiftOpen(); + } + } else if (type === MovementType.Tilt) { + if (targetPercent100ths != null && this.features.absolutePosition) { + await this.handleGoToTiltPosition(targetPercent100ths); } else if ( direction === MovementDirection.Close || (targetPercent100ths != null && targetPercent100ths > 0) ) { - await this.handleClose(); + await this.handleTiltClose(); } else if (direction === MovementDirection.Open) { - await this.handleOpen(); + await this.handleTiltOpen(); } } } @@ -94,21 +142,21 @@ export class WindowCoveringServerBase extends FeaturedBase { const homeAssistant = this.agent.get(HomeAssistantEntityBehavior); await homeAssistant.callAction("cover.stop_cover"); } - private async handleOpen() { + + private async handleLiftOpen() { const homeAssistant = this.agent.get(HomeAssistantEntityBehavior); await homeAssistant.callAction("cover.open_cover"); } - private async handleClose() { + private async handleLiftClose() { const homeAssistant = this.agent.get(HomeAssistantEntityBehavior); await homeAssistant.callAction("cover.close_cover"); } - - private async handleGoToPosition(targetPercent100ths: number) { + private async handleGoToLiftPosition(targetPercent100ths: number) { const homeAssistant = this.agent.get(HomeAssistantEntityBehavior); const attributes = homeAssistant.entity.state .attributes as CoverDeviceAttributes; const currentPosition = attributes.current_position; - const targetPosition = this.convertLiftValue(targetPercent100ths / 100); + const targetPosition = this.invertValue(targetPercent100ths / 100); if (targetPosition == null || targetPosition === currentPosition) { return; } @@ -117,9 +165,29 @@ export class WindowCoveringServerBase extends FeaturedBase { }); } - private convertLiftValue( - percentage: number | undefined | null, - ): number | null { + private async handleTiltOpen() { + const homeAssistant = this.agent.get(HomeAssistantEntityBehavior); + await homeAssistant.callAction("cover.open_cover_tilt"); + } + private async handleTiltClose() { + const homeAssistant = this.agent.get(HomeAssistantEntityBehavior); + await homeAssistant.callAction("cover.close_cover_tilt"); + } + private async handleGoToTiltPosition(targetPercent100ths: number) { + const homeAssistant = this.agent.get(HomeAssistantEntityBehavior); + const attributes = homeAssistant.entity.state + .attributes as CoverDeviceAttributes; + const currentPosition = attributes.current_tilt_position; + const targetPosition = this.invertValue(targetPercent100ths / 100); + if (targetPosition == null || targetPosition === currentPosition) { + return; + } + await homeAssistant.callAction("cover.set_cover_tilt_position", { + tilt_position: targetPosition, + }); + } + + private invertValue(percentage: number | undefined | null): number | null { if (percentage == null) { return null; } diff --git a/packages/backend/src/matter/bridge/create-device.test.ts b/packages/backend/src/matter/bridge/create-device.test.ts index 389a0ae2..4d4df4b5 100644 --- a/packages/backend/src/matter/bridge/create-device.test.ts +++ b/packages/backend/src/matter/bridge/create-device.test.ts @@ -61,7 +61,9 @@ const testEntities: Record< }), ], [HomeAssistantDomain.cover]: [ - createEntity("cover.co1", "on", {}), + createEntity("cover.co1", "on", { + supported_features: 15, + }), ], [HomeAssistantDomain.fan]: [ createEntity("fan.f1", "on"), diff --git a/packages/backend/src/matter/devices/climate-device.ts b/packages/backend/src/matter/devices/climate-device.ts index 6495a181..15704b54 100644 --- a/packages/backend/src/matter/devices/climate-device.ts +++ b/packages/backend/src/matter/devices/climate-device.ts @@ -32,15 +32,16 @@ function thermostatFeatures( supportsCooling: boolean, supportsHeating: boolean, ) { - const features: FeatureSelection> = []; + const features: FeatureSelection> = + new Set(); if (supportsCooling) { - features.push("Cooling"); + features.add("Cooling"); } if (supportsHeating) { - features.push("Heating"); + features.add("Heating"); } if (supportsHeating && supportsCooling) { - features.push("AutoMode"); + features.add("AutoMode"); } return features; } diff --git a/packages/backend/src/matter/devices/cover-device.ts b/packages/backend/src/matter/devices/cover-device.ts index 39041ac2..1339883b 100644 --- a/packages/backend/src/matter/devices/cover-device.ts +++ b/packages/backend/src/matter/devices/cover-device.ts @@ -9,16 +9,39 @@ import { } from "@home-assistant-matter-hub/common"; import { testBit } from "../../utils/test-bit.js"; import { EndpointType } from "@matter/main"; +import { FeatureSelection } from "../../utils/feature-selection.js"; +import { WindowCovering } from "@matter/main/clusters"; + +const CoverDeviceType = (supportedFeatures: number) => { + const features: FeatureSelection = new Set(); + if (testBit(supportedFeatures, CoverSupportedFeatures.support_open)) { + features.add("Lift"); + features.add("PositionAwareLift"); + if ( + testBit(supportedFeatures, CoverSupportedFeatures.support_set_position) + ) { + features.add("AbsolutePosition"); + } + } + + if (testBit(supportedFeatures, CoverSupportedFeatures.support_open_tilt)) { + features.add("Tilt"); + features.add("PositionAwareTilt"); + if ( + testBit( + supportedFeatures, + CoverSupportedFeatures.support_set_tilt_position, + ) + ) { + features.add("AbsolutePosition"); + } + } -const CoverDeviceType = (positionAwareLift: boolean) => { - const windowCoveringServer = positionAwareLift - ? WindowCoveringServer.with("Lift", "PositionAwareLift", "AbsolutePosition") - : WindowCoveringServer.with("Lift", "PositionAwareLift"); return WindowCoveringDevice.with( BasicInformationServer, IdentifyServer, HomeAssistantEntityBehavior, - windowCoveringServer, + WindowCoveringServer.with(...features), ); }; @@ -27,10 +50,7 @@ export function CoverDevice( ): EndpointType { const attributes = homeAssistantEntity.entity.state .attributes as CoverDeviceAttributes; - const supportedFeatures = attributes.supported_features ?? 0; - const positionAwareLift = testBit( - supportedFeatures, - CoverSupportedFeatures.support_set_position, - ); - return CoverDeviceType(positionAwareLift).set({ homeAssistantEntity }); + return CoverDeviceType(attributes.supported_features ?? 0).set({ + homeAssistantEntity, + }); } diff --git a/packages/backend/src/matter/devices/fan-device.ts b/packages/backend/src/matter/devices/fan-device.ts index 43118e61..bad81077 100644 --- a/packages/backend/src/matter/devices/fan-device.ts +++ b/packages/backend/src/matter/devices/fan-device.ts @@ -19,15 +19,16 @@ const fanOnOffConfig: OnOffConfig = { }; const FanControlFeatures = (supportedFeatures: number) => { - const features: FeatureSelection = []; + const features: FeatureSelection = new Set(); if (testBit(supportedFeatures, FanDeviceFeature.SET_SPEED)) { - features.push("MultiSpeed", "Step"); + features.add("MultiSpeed"); + features.add("Step"); } if (testBit(supportedFeatures, FanDeviceFeature.PRESET_MODE)) { - features.push("Auto"); + features.add("Auto"); } if (testBit(supportedFeatures, FanDeviceFeature.DIRECTION)) { - features.push("AirflowDirection"); + features.add("AirflowDirection"); } return features; }; diff --git a/packages/backend/src/utils/feature-selection.ts b/packages/backend/src/utils/feature-selection.ts index 9a3202db..df8d723f 100644 --- a/packages/backend/src/utils/feature-selection.ts +++ b/packages/backend/src/utils/feature-selection.ts @@ -1,5 +1,5 @@ import { ClusterType } from "@matter/main/types"; -export type FeatureSelection = Capitalize< - string & keyof T["features"] ->[]; +export type FeatureSelection = Set< + Capitalize +>; diff --git a/packages/common/src/domains/cover.ts b/packages/common/src/domains/cover.ts index d3b6d99d..e3215b3b 100644 --- a/packages/common/src/domains/cover.ts +++ b/packages/common/src/domains/cover.ts @@ -6,6 +6,7 @@ export enum CoverDeviceState { } export interface CoverDeviceAttributes { current_position?: number; + current_tilt_position?: number; supported_features?: number; }