Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dev #357

Merged
merged 3 commits into from
Dec 28, 2024
Merged

Dev #357

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 27 additions & 18 deletions packages/backend/src/home-assistant/home-assistant-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,50 +7,59 @@ 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;
readonly accessToken: string;
}

export class HomeAssistantClient implements Environmental.Service {
private readonly log: Logger;
readonly construction: Promise<void>;
public connection!: Connection;

constructor(
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<void> {
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<void> {
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();
}
}
29 changes: 29 additions & 0 deletions packages/backend/src/home-assistant/home-assistant-config.ts
Original file line number Diff line number Diff line change
@@ -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<void>;
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<void> {
const { connection } = await this.environment.load(HomeAssistantClient);
this.config = await getConfig(connection);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
}
}
Expand Down
26 changes: 22 additions & 4 deletions packages/backend/src/matter/behaviors/thermostat-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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,
Expand Down Expand Up @@ -184,6 +196,7 @@ export class ThermostatServerBase extends FeaturedBase {
attributes.target_temp_low ??
attributes.target_temperature ??
attributes.temperature,
this.internal.homeAssistantUnit,
);
}

Expand All @@ -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(
Expand Down
27 changes: 27 additions & 0 deletions packages/backend/src/matter/behaviors/utils/temperature-utils.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
}
Loading
Loading