179 lines
8.6 KiB
TypeScript
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);
|
|
}
|
|
}
|