Files
integrations/ts/integrations/opentherm_gw/opentherm_gw.classes.integration.ts
T

179 lines
8.6 KiB
TypeScript

import type * as shxInterfaces from '@smarthome.exchange/interfaces';
import { BaseIntegration } from '../../core/classes.baseintegration.js';
import type { IIntegrationEntity, IIntegrationEvent, IIntegrationRuntime, IIntegrationSetupContext, IServiceCallRequest, IServiceCallResult } from '../../core/types.js';
import { OpenthermGwClient } from './opentherm_gw.classes.client.js';
import { OpenthermGwConfigFlow } from './opentherm_gw.classes.configflow.js';
import { createOpenthermGwDiscoveryDescriptor } from './opentherm_gw.discovery.js';
import { OpenthermGwMapper } from './opentherm_gw.mapper.js';
import type { IOpenthermGwConfig, TOpenthermGwCommandValue } from './opentherm_gw.types.js';
import { openthermGwDomain } from './opentherm_gw.types.js';
export class OpenthermGwIntegration extends BaseIntegration<IOpenthermGwConfig> {
public readonly domain = openthermGwDomain;
public readonly displayName = 'OpenTherm Gateway';
public readonly status = 'control-runtime' as const;
public readonly discoveryDescriptor = createOpenthermGwDiscoveryDescriptor();
public readonly configFlow = new OpenthermGwConfigFlow();
public readonly metadata = {
source: 'home-assistant/core',
upstreamPath: 'homeassistant/components/opentherm_gw',
upstreamDomain: openthermGwDomain,
integrationType: 'device',
iotClass: 'local_push',
requirements: ['pyotgw==2.2.3'],
dependencies: [],
afterDependencies: [],
codeowners: ['@mvn23'],
configFlow: true,
documentation: 'https://www.home-assistant.io/integrations/opentherm_gw',
};
public async setup(configArg: IOpenthermGwConfig, contextArg: IIntegrationSetupContext): Promise<IIntegrationRuntime> {
void contextArg;
return new OpenthermGwRuntime(new OpenthermGwClient(configArg));
}
public async destroy(): Promise<void> {}
}
export class HomeAssistantOpenthermGwIntegration extends OpenthermGwIntegration {}
class OpenthermGwRuntime implements IIntegrationRuntime {
public domain = openthermGwDomain;
constructor(private readonly client: OpenthermGwClient) {}
public async devices(): Promise<shxInterfaces.data.IDeviceDefinition[]> {
return OpenthermGwMapper.toDevices(await this.client.getSnapshot());
}
public async entities(): Promise<IIntegrationEntity[]> {
return OpenthermGwMapper.toEntities(await this.client.getSnapshot());
}
public async subscribe(handlerArg: (eventArg: IIntegrationEvent) => void): Promise<() => Promise<void>> {
const unsubscribe = this.client.subscribe((eventArg) => handlerArg(OpenthermGwMapper.toIntegrationEvent(eventArg)));
return async () => unsubscribe();
}
public async callService(requestArg: IServiceCallRequest): Promise<IServiceCallResult> {
try {
if (requestArg.domain === openthermGwDomain) {
return await this.callGatewayService(requestArg);
}
if (requestArg.domain === 'climate') {
return await this.callClimateService(requestArg);
}
if (requestArg.domain === 'switch') {
return await this.callSwitchService(requestArg);
}
return { success: false, error: `Unsupported OpenTherm Gateway service domain: ${requestArg.domain}` };
} catch (errorArg) {
return { success: false, error: errorArg instanceof Error ? errorArg.message : String(errorArg) };
}
}
public async destroy(): Promise<void> {
await this.client.destroy();
}
private async callGatewayService(requestArg: IServiceCallRequest): Promise<IServiceCallResult> {
if (requestArg.service === 'set_control_setpoint') {
return { success: true, data: await this.client.setControlSetpoint(this.numberValue(requestArg.data?.temperature ?? requestArg.data?.setpoint, 'set_control_setpoint requires data.temperature.')) };
}
if (requestArg.service === 'set_hot_water') {
if (requestArg.data?.temperature !== undefined) {
return { success: true, data: await this.client.setHotWaterSetpoint(this.numberValue(requestArg.data.temperature, 'set_hot_water temperature must be a number.')) };
}
return { success: true, data: await this.client.setHotWater(this.hotWaterValue(requestArg.data)) };
}
if (requestArg.service === 'set_hot_water_ovrd') {
return { success: true, data: await this.client.setHotWater(this.hotWaterValue(requestArg.data)) };
}
if (requestArg.service === 'set_hot_water_setpoint') {
return { success: true, data: await this.client.setHotWaterSetpoint(this.numberValue(requestArg.data?.temperature, 'set_hot_water_setpoint requires data.temperature.')) };
}
if (requestArg.service === 'set_outside_temperature') {
return { success: true, data: await this.client.setOutsideTemperature(this.numberValue(requestArg.data?.temperature, 'set_outside_temperature requires data.temperature.')) };
}
if (requestArg.service === 'set_central_heating_ovrd') {
return { success: true, data: await this.client.setCentralHeatingOverride(1, this.booleanValue(requestArg.data?.ch_override ?? requestArg.data?.enabled, 'set_central_heating_ovrd requires data.ch_override or data.enabled.')) };
}
if (requestArg.service === 'reset' || requestArg.service === 'reset_gateway') {
return { success: true, data: await this.client.reset() };
}
if (requestArg.service === 'command' || requestArg.service === 'send_transparent_command') {
const code = this.stringValue(requestArg.data?.command ?? requestArg.data?.transp_cmd, 'OpenTherm Gateway command service requires data.command.');
const value = this.commandValue(requestArg.data?.value ?? requestArg.data?.transp_arg);
return { success: true, data: await this.client.command(code, value) };
}
return { success: false, error: `Unsupported OpenTherm Gateway service: ${requestArg.service}` };
}
private async callClimateService(requestArg: IServiceCallRequest): Promise<IServiceCallResult> {
if (requestArg.service !== 'set_temperature') {
return { success: false, error: `Unsupported OpenTherm Gateway climate service: ${requestArg.service}` };
}
const temperature = this.numberValue(requestArg.data?.temperature, 'climate.set_temperature requires data.temperature.');
const temporary = typeof requestArg.data?.temporary === 'boolean' ? requestArg.data.temporary : undefined;
return { success: true, data: await this.client.setRoomSetpoint(temperature, temporary) };
}
private async callSwitchService(requestArg: IServiceCallRequest): Promise<IServiceCallResult> {
if (requestArg.service !== 'turn_on' && requestArg.service !== 'turn_off') {
return { success: false, error: `Unsupported OpenTherm Gateway switch service: ${requestArg.service}` };
}
const target = `${requestArg.target.entityId || requestArg.target.deviceId || ''} ${requestArg.data?.key || ''}`;
const circuit = target.includes('2') ? 2 : 1;
return { success: true, data: await this.client.setCentralHeatingOverride(circuit, requestArg.service === 'turn_on') };
}
private hotWaterValue(dataArg: Record<string, unknown> | undefined): boolean | string | number {
const value = dataArg?.enabled ?? dataArg?.hot_water ?? dataArg?.hotWater ?? dataArg?.dhw_override ?? dataArg?.dhwOverride ?? dataArg?.value;
if (typeof value === 'boolean' || typeof value === 'string' || typeof value === 'number') {
return value;
}
throw new Error('set_hot_water requires data.enabled, data.hot_water, or data.dhw_override unless data.temperature is used.');
}
private commandValue(valueArg: unknown): TOpenthermGwCommandValue {
if (typeof valueArg === 'string' || typeof valueArg === 'number' || typeof valueArg === 'boolean') {
return valueArg;
}
throw new Error('OpenTherm Gateway command service requires data.value.');
}
private numberValue(valueArg: unknown, errorArg: string): number {
if (typeof valueArg === 'number' && Number.isFinite(valueArg)) {
return valueArg;
}
if (typeof valueArg === 'string' && valueArg.trim()) {
const parsed = Number(valueArg);
if (Number.isFinite(parsed)) {
return parsed;
}
}
throw new Error(errorArg);
}
private booleanValue(valueArg: unknown, errorArg: string): boolean {
if (typeof valueArg === 'boolean') {
return valueArg;
}
if (valueArg === '1' || valueArg === 1 || valueArg === 'true') {
return true;
}
if (valueArg === '0' || valueArg === 0 || valueArg === 'false') {
return false;
}
throw new Error(errorArg);
}
private stringValue(valueArg: unknown, errorArg: string): string {
if (typeof valueArg === 'string' && valueArg.trim()) {
return valueArg.trim().toUpperCase();
}
throw new Error(errorArg);
}
}