From 10708f3f0da7ba61fee4c4b789136bc5c92bf146 Mon Sep 17 00:00:00 2001 From: Chris ter Beke <1134120+ChrisTerBeke@users.noreply.github.com> Date: Tue, 9 Jul 2024 19:49:06 +0200 Subject: [PATCH] Refactor system capability detection, implement fireplace mode toggle --- .../capabilities/fireplace_mode.json | 10 ++ .homeycompose/capabilities/mode.json | 1 - app.json | 11 ++- drivers/remeha/device.ts | 72 +++++++++++--- lib/RemehaMobileApi.ts | 96 +++++++++++++------ 5 files changed, 147 insertions(+), 43 deletions(-) create mode 100644 .homeycompose/capabilities/fireplace_mode.json diff --git a/.homeycompose/capabilities/fireplace_mode.json b/.homeycompose/capabilities/fireplace_mode.json new file mode 100644 index 0000000..d0ad395 --- /dev/null +++ b/.homeycompose/capabilities/fireplace_mode.json @@ -0,0 +1,10 @@ +{ + "type": "boolean", + "title": { + "en": "Fireplace mode", + "nl": "Openhaardmodus" + }, + "uiComponent": "button", + "getable": true, + "setable": true +} diff --git a/.homeycompose/capabilities/mode.json b/.homeycompose/capabilities/mode.json index 2f4a436..52fa651 100644 --- a/.homeycompose/capabilities/mode.json +++ b/.homeycompose/capabilities/mode.json @@ -5,7 +5,6 @@ "nl": "Modus" }, "uiComponent": "sensor", - "icon": "/assets/capabilities/temperature.svg", "getable": true, "setable": false } diff --git a/app.json b/app.json index 9ba27ab..a74269c 100644 --- a/app.json +++ b/app.json @@ -108,6 +108,16 @@ } ], "capabilities": { + "fireplace_mode": { + "type": "boolean", + "title": { + "en": "Fireplace mode", + "nl": "Openhaardmodus" + }, + "uiComponent": "button", + "getable": true, + "setable": true + }, "measure_temperature_outside": { "type": "number", "title": { @@ -145,7 +155,6 @@ "nl": "Modus" }, "uiComponent": "sensor", - "icon": "/assets/capabilities/temperature.svg", "getable": true, "setable": false }, diff --git a/drivers/remeha/device.ts b/drivers/remeha/device.ts index 09e614c..3b5bd0d 100644 --- a/drivers/remeha/device.ts +++ b/drivers/remeha/device.ts @@ -10,11 +10,6 @@ class RemehaThermostatDevice extends Device { private _client?: RemehaMobileApi async onInit(): Promise { - this.addCapability('measure_temperature_outside') - this.addCapability('measure_pressure') - this.addCapability('alarm_water') - this.addCapability('mode') - this.registerCapabilityListener('target_temperature', this._setTargetTemperature.bind(this)) this._init() } @@ -30,6 +25,7 @@ class RemehaThermostatDevice extends Device { const { accessToken } = this.getStore() this._client = new RemehaMobileApi(accessToken) this._syncInterval = setInterval(this._syncAttributes.bind(this), POLL_INTERVAL_MS) + await this._syncCapabilities() this._syncAttributes() } @@ -39,6 +35,33 @@ class RemehaThermostatDevice extends Device { this._client = undefined } + private async _syncCapabilities(): Promise { + await this._refreshAccessToken() + if (!this._client) return this.setUnavailable('No Remeha Home client') + const { id } = this.getData() + + try { + const capabilities = await this._client.capabilities(id) + if (!capabilities) return this.setUnavailable('Could not find capabilities') + + // required capabilities + await this.addCapability('measure_temperature') + await this.addCapability('target_temperature') + this.registerCapabilityListener('target_temperature', this._setTargetTemperature.bind(this)) + await this.addCapability('measure_pressure') + await this.addCapability('alarm_water') + await this.addCapability('mode') + + // optional capabilities + await this._addOrRemoveCapability('measure_temperature_water', capabilities.hotWaterZone) + await this._addOrRemoveCapability('target_temperature_water', capabilities.hotWaterZone) + await this._addOrRemoveCapability('measure_temperature_outside', capabilities.outdoorTemperature) + await this._addOrRemoveCapability('fireplace_mode', capabilities.fireplaceMode, this._setFireplaceMode.bind(this)) + } catch (error) { + this.setUnavailable('Could not find capabilities') + } + } + private async _syncAttributes(): Promise { await this._refreshAccessToken() if (!this._client) return this.setUnavailable('No Remeha Home client') @@ -48,14 +71,19 @@ class RemehaThermostatDevice extends Device { const data = await this._client.device(id) if (!data) return this.setUnavailable('Could not find thermostat data') this.setAvailable() + + // required capabilities this.setCapabilityValue('measure_temperature', data.temperature) this.setCapabilityValue('target_temperature', data.targetTemperature) - this.setCapabilityValue('measure_temperature_outside', data.outdoorTemperature) this.setCapabilityValue('measure_pressure', (data.waterPressure * 1000)) this.setCapabilityValue('alarm_water', !data.waterPressureOK) this.setCapabilityValue('mode', data.mode) - this._setOptionalCapability('measure_temperature_water', data.waterTemperature) - this._setOptionalCapability('target_temperature_water', data.targetWaterTemperature) + + // optional capabilities + this._setOptionalCapabilityValue('measure_temperature_outside', data.outdoorTemperature) + this._setOptionalCapabilityValue('measure_temperature_water', data.waterTemperature) + this._setOptionalCapabilityValue('target_temperature_water', data.targetWaterTemperature) + this._setOptionalCapabilityValue('fireplace_mode', data.fireplaceMode) } catch (error) { this.setUnavailable('Could not find thermostat data') } @@ -68,12 +96,18 @@ class RemehaThermostatDevice extends Device { } catch (error) { } } - private async _setOptionalCapability(capability: string, value: number | boolean | string | undefined | null): Promise { - if (value) { + private async _addOrRemoveCapability(capability: string, enabled: boolean, listener?: Device.CapabilityCallback): Promise { + if (enabled) { await this.addCapability(capability) - this.setCapabilityValue(capability, value) + if (listener) this.registerCapabilityListener(capability, listener) } else { - this.removeCapability(capability) + await this.removeCapability(capability) + } + } + + private async _setOptionalCapabilityValue(capability: string, value: any): Promise { + if (this.hasCapability(capability)) { + await this.setCapabilityValue(capability, value) } } @@ -85,7 +119,19 @@ class RemehaThermostatDevice extends Device { try { await this._client.setTargetTemperature(id, value) } catch (error) { - this.setUnavailable('Could set target temperature') + this.setUnavailable('Could not set target temperature') + } + } + + private async _setFireplaceMode(value: boolean): Promise { + await this._refreshAccessToken() + if (!this._client) return this.setUnavailable('No Remeha Home client') + const { id } = this.getData() + + try { + await this._client.setFireplaceMode(id, value) + } catch (error) { + this.setUnavailable('Could not set fireplace mode') } } diff --git a/lib/RemehaMobileApi.ts b/lib/RemehaMobileApi.ts index beaa8f7..422b466 100644 --- a/lib/RemehaMobileApi.ts +++ b/lib/RemehaMobileApi.ts @@ -1,5 +1,11 @@ import fetch from 'node-fetch' +export type DeviceCapabilities = { + fireplaceMode: boolean + outdoorTemperature: boolean + hotWaterZone: boolean +} + export type DeviceData = { id: string name: string @@ -8,9 +14,10 @@ export type DeviceData = { targetTemperature: number waterPressure: number waterPressureOK: boolean - outdoorTemperature: number + outdoorTemperature?: number waterTemperature?: number targetWaterTemperature?: number + fireplaceMode?: boolean } type ResponseClimateZone = { @@ -19,6 +26,8 @@ type ResponseClimateZone = { roomTemperature: number setPoint: number zoneMode: string + capabilityFirePlaceMode: boolean + firePlaceModeActive?: boolean } type ResponseHotWaterZone = { @@ -28,11 +37,13 @@ type ResponseHotWaterZone = { } type ResponseAppliance = { + applianceId: string climateZones: ResponseClimateZone[] hotWaterZones: ResponseHotWaterZone[] - outdoorTemperature: number waterPressure: number waterPressureOK: boolean + capabilityOutdoorTemperature: boolean + outdoorTemperature?: number } type DashboardResponse = { @@ -54,39 +65,31 @@ export class RemehaMobileApi { } public async devices(): Promise { - const dashboard = await this._call('/homes/dashboard') as DashboardResponse + const dashboard = await this._getDashboard() if (!dashboard?.appliances) return [] return dashboard.appliances.map(this._createDeviceData) } - private _createDeviceData(appliance: ResponseAppliance): DeviceData { - const deviceData: DeviceData = { - id: appliance.climateZones[0].climateZoneId, - name: appliance.climateZones[0].name, - mode: appliance.climateZones[0].zoneMode, - temperature: appliance.climateZones[0].roomTemperature, - targetTemperature: appliance.climateZones[0].setPoint, - waterPressure: appliance.waterPressure, - waterPressureOK: appliance.waterPressureOK, - outdoorTemperature: appliance.outdoorTemperature, - } - - // not every installation has a hot water zone, for example in Hybrid heat pumps - if (appliance.hotWaterZones.length > 0) { - deviceData.waterTemperature = appliance.hotWaterZones[0].dhwTemperature - deviceData.targetWaterTemperature = appliance.hotWaterZones[0].targetSetpoint - } - - return deviceData + public async device(climateZoneId: string): Promise { + const devices = await this.devices() + return devices.find(device => device.id === climateZoneId) } public async debug(): Promise { return await this._call('/homes/dashboard') } - public async device(climateZoneId: string): Promise { - const devices = await this.devices() - return devices.find(device => device.id === climateZoneId) + public async capabilities(climateZoneId: string): Promise { + const dashboard = await this._getDashboard() + if (!dashboard?.appliances) return undefined + const appliance = dashboard.appliances.find(appliance => appliance.climateZones[0].climateZoneId === climateZoneId) + if (!appliance) return undefined + + return { + fireplaceMode: appliance.climateZones[0].capabilityFirePlaceMode, + outdoorTemperature: appliance.capabilityOutdoorTemperature, + hotWaterZone: appliance.hotWaterZones.length > 0, + } } public async setTargetTemperature(climateZoneID: string, roomTemperatureSetPoint: number): Promise { @@ -99,8 +102,16 @@ export class RemehaMobileApi { } } - private async _call(path: string, method: string = 'GET', data: { [key: string]: string | number } | undefined = undefined): Promise { - console.log('call', path, method, data) + public async setFireplaceMode(climateZoneID: string, fireplaceModeActive: boolean): Promise { + await this._call(`/climate-zones/${climateZoneID}/modes/fireplacemode`, 'POST', { fireplaceModeActive }) + } + + public async _getDashboard(): Promise { + const dashboard = await this._call('/homes/dashboard') as DashboardResponse + return dashboard + } + + private async _call(path: string, method: string = 'GET', data: { [key: string]: string | number | boolean } | undefined = undefined): Promise { try { const response = await fetch(`${this._rootURL}${path}`, { method: method, @@ -115,7 +126,6 @@ export class RemehaMobileApi { return } const responseBody = await response.text() - console.log('response', responseBody) if (responseBody.length > 0) { return JSON.parse(responseBody) } @@ -124,4 +134,34 @@ export class RemehaMobileApi { return Promise.reject(error) } } + + private _createDeviceData(appliance: ResponseAppliance): DeviceData { + const deviceData: DeviceData = { + id: appliance.climateZones[0].climateZoneId, + name: appliance.climateZones[0].name, + mode: appliance.climateZones[0].zoneMode, + temperature: appliance.climateZones[0].roomTemperature, + targetTemperature: appliance.climateZones[0].setPoint, + waterPressure: appliance.waterPressure, + waterPressureOK: appliance.waterPressureOK, + } + + // not every installation supports outdoor temperature + if (appliance.capabilityOutdoorTemperature) { + deviceData.outdoorTemperature = appliance.outdoorTemperature + } + + // not every installation supports fireplace mode + if (appliance.climateZones[0].capabilityFirePlaceMode) { + deviceData.fireplaceMode = appliance.climateZones[0].firePlaceModeActive + } + + // not every installation has a hot water zone, for example in Hybrid heat pumps + if (appliance.hotWaterZones.length > 0) { + deviceData.waterTemperature = appliance.hotWaterZones[0].dhwTemperature + deviceData.targetWaterTemperature = appliance.hotWaterZones[0].targetSetpoint + } + + return deviceData + } }