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

Feature: fireplace mode #11

Merged
merged 1 commit into from
Jul 9, 2024
Merged
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
10 changes: 10 additions & 0 deletions .homeycompose/capabilities/fireplace_mode.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"type": "boolean",
"title": {
"en": "Fireplace mode",
"nl": "Openhaardmodus"
},
"uiComponent": "button",
"getable": true,
"setable": true
}
1 change: 0 additions & 1 deletion .homeycompose/capabilities/mode.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
"nl": "Modus"
},
"uiComponent": "sensor",
"icon": "/assets/capabilities/temperature.svg",
"getable": true,
"setable": false
}
11 changes: 10 additions & 1 deletion app.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down Expand Up @@ -145,7 +155,6 @@
"nl": "Modus"
},
"uiComponent": "sensor",
"icon": "/assets/capabilities/temperature.svg",
"getable": true,
"setable": false
},
Expand Down
72 changes: 59 additions & 13 deletions drivers/remeha/device.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,6 @@ class RemehaThermostatDevice extends Device {
private _client?: RemehaMobileApi

async onInit(): Promise<void> {
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()
}

Expand All @@ -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()
}

Expand All @@ -39,6 +35,33 @@ class RemehaThermostatDevice extends Device {
this._client = undefined
}

private async _syncCapabilities(): Promise<void> {
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<void> {
await this._refreshAccessToken()
if (!this._client) return this.setUnavailable('No Remeha Home client')
Expand All @@ -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')
}
Expand All @@ -68,12 +96,18 @@ class RemehaThermostatDevice extends Device {
} catch (error) { }
}

private async _setOptionalCapability(capability: string, value: number | boolean | string | undefined | null): Promise<void> {
if (value) {
private async _addOrRemoveCapability(capability: string, enabled: boolean, listener?: Device.CapabilityCallback): Promise<void> {
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<void> {
if (this.hasCapability(capability)) {
await this.setCapabilityValue(capability, value)
}
}

Expand All @@ -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<void> {
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')
}
}

Expand Down
96 changes: 68 additions & 28 deletions lib/RemehaMobileApi.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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 = {
Expand All @@ -19,6 +26,8 @@ type ResponseClimateZone = {
roomTemperature: number
setPoint: number
zoneMode: string
capabilityFirePlaceMode: boolean
firePlaceModeActive?: boolean
}

type ResponseHotWaterZone = {
Expand All @@ -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 = {
Expand All @@ -54,39 +65,31 @@ export class RemehaMobileApi {
}

public async devices(): Promise<DeviceData[]> {
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<DeviceData | undefined> {
const devices = await this.devices()
return devices.find(device => device.id === climateZoneId)
}

public async debug(): Promise<any> {
return await this._call('/homes/dashboard')
}

public async device(climateZoneId: string): Promise<DeviceData | undefined> {
const devices = await this.devices()
return devices.find(device => device.id === climateZoneId)
public async capabilities(climateZoneId: string): Promise<DeviceCapabilities | undefined> {
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<void> {
Expand All @@ -99,8 +102,16 @@ export class RemehaMobileApi {
}
}

private async _call(path: string, method: string = 'GET', data: { [key: string]: string | number } | undefined = undefined): Promise<any> {
console.log('call', path, method, data)
public async setFireplaceMode(climateZoneID: string, fireplaceModeActive: boolean): Promise<void> {
await this._call(`/climate-zones/${climateZoneID}/modes/fireplacemode`, 'POST', { fireplaceModeActive })
}

public async _getDashboard(): Promise<DashboardResponse> {
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<any> {
try {
const response = await fetch(`${this._rootURL}${path}`, {
method: method,
Expand All @@ -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)
}
Expand All @@ -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
}
}
Loading