Skip to content

Commit

Permalink
Fix: network crashes (#9)
Browse files Browse the repository at this point in the history
* Clean up unused properties docs, moved to a GH issue

* Try/catch around all device calls that use the network

* Add debug functionality, improve IP change handling

* Bump version to v0.0.4

Changelog: Fix crashes caused by network errors, add debug function
  • Loading branch information
ChrisTerBeke authored May 22, 2024
1 parent 54836f2 commit 0329e75
Show file tree
Hide file tree
Showing 7 changed files with 168 additions and 97 deletions.
3 changes: 3 additions & 0 deletions .homeychangelog.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,8 @@
},
"0.0.3": {
"en": "Fixed crashes caused by network issues"
},
"0.0.4": {
"en": "Fix crashes caused by network errors, add debug function"
}
}
2 changes: 1 addition & 1 deletion .homeycompose/app.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"id": "com.christerbeke.uponor-smatrix",
"version": "0.0.3",
"version": "0.0.4",
"compatibility": ">=5.0.0",
"sdk": 3,
"platforms": [
Expand Down
35 changes: 32 additions & 3 deletions app.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"_comment": "This file is generated. Please edit .homeycompose/app.json instead.",
"id": "com.christerbeke.uponor-smatrix",
"version": "0.0.3",
"version": "0.0.4",
"compatibility": ">=5.0.0",
"sdk": 3,
"platforms": [
Expand Down Expand Up @@ -82,13 +82,42 @@
{
"id": "address",
"label": {
"en": "Override IP Address"
"en": "Override IP Address",
"nl": "IP-adres overschrijven"
},
"type": "text",
"required": false,
"hint": {
"en": "Overrides the auto-discovered IP address of the device."
"en": "Overrides the auto-discovered IP address of the device.",
"nl": "Overschrijft het automatisch ontdekte IP-adres van het apparaat."
}
},
{
"type": "group",
"label": {
"en": "Troubleshooting",
"nl": "Probleemoplossing"
},
"children": [
{
"id": "debugEnabled",
"type": "checkbox",
"label": {
"en": "Enable debug data",
"nl": "Schakel probleemverhelping in"
},
"value": false
},
{
"id": "apiData",
"type": "textarea",
"label": {
"en": "Last API response",
"nl": "Laatste API-reactie"
},
"value": "{}"
}
]
}
]
}
Expand Down
93 changes: 62 additions & 31 deletions drivers/uponor/device.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { isIPv4 } from 'net'
import { Device, DiscoveryResult } from 'homey'
import { UponorHTTPClient, Mode } from '../../lib/UponorHTTPClient'
import { Device, DiscoveryResultMAC } from 'homey'
import { UponorHTTPClient } from '../../lib/UponorHTTPClient'

const POLL_INTERVAL_MS = 1000 * 60 * 1

Expand All @@ -22,20 +22,20 @@ class UponorThermostatDevice extends Device {
this._uninit()
}

onDiscoveryResult(discoveryResult: DiscoveryResult): boolean {
onDiscoveryResult(discoveryResult: DiscoveryResultMAC): boolean {
return this.getData().id.includes(discoveryResult.id)
}

async onDiscoveryAvailable(discoveryResult: DiscoveryResult): Promise<void> {
this._updateAddress(discoveryResult.id)
async onDiscoveryAvailable(discoveryResult: DiscoveryResultMAC): Promise<void> {
this._updateAddress(discoveryResult.address, true)
}

async onDiscoveryAddressChanged(discoveryResult: DiscoveryResult): Promise<void> {
this._updateAddress(discoveryResult.id)
async onDiscoveryAddressChanged(discoveryResult: DiscoveryResultMAC): Promise<void> {
this._updateAddress(discoveryResult.address, true)
}

async onDiscoveryLastSeenChanged(discoveryResult: DiscoveryResult): Promise<void> {
this._updateAddress(discoveryResult.id)
async onDiscoveryLastSeenChanged(discoveryResult: DiscoveryResultMAC): Promise<void> {
this._updateAddress(discoveryResult.address, true)
}

async onSettings({ newSettings }: { newSettings: { [key: string]: any } }): Promise<void> {
Expand All @@ -51,21 +51,29 @@ class UponorThermostatDevice extends Device {

private _getAddress(): string | undefined {
const settingAddress = this.getSetting('address')
if (settingAddress) return settingAddress
if (settingAddress && settingAddress.length > 0) return settingAddress
const storeAddress = this.getStoreValue('address')
if (storeAddress) return storeAddress
if (storeAddress && storeAddress.length > 0) return storeAddress
return undefined
}

private async _updateAddress(newAddress: string): Promise<boolean> {
if (newAddress.length > 0) {
private async _updateAddress(newAddress: string, persist = false): Promise<boolean> {
if (newAddress && newAddress.length > 0) {
const isValidIP = isIPv4(newAddress)
if (!isValidIP) return false
const client = new UponorHTTPClient(newAddress)
const canConnect = await client.testConnection()
if (!canConnect) return false
try {
const canConnect = await client.testConnection()
if (!canConnect) return false
} catch (error) {
return false
}
}

if (persist) {
this.setStoreValue('address', newAddress)
}
this.setStoreValue('address', newAddress)

this._init()
return true
}
Expand All @@ -74,12 +82,17 @@ class UponorThermostatDevice extends Device {
await this._uninit()
const address = this._getAddress()
if (!address) return this.setUnavailable('No IP address configured')
const client = new UponorHTTPClient(address)
const canConnect = await client.testConnection()
if (!canConnect) return this.setUnavailable(`Could not connect to Uponor controller on IP address ${address}`)
this._client = client
this._syncInterval = setInterval(this._syncAttributes.bind(this), POLL_INTERVAL_MS)
this._syncAttributes()

try {
const client = new UponorHTTPClient(address)
const canConnect = await client.testConnection()
if (!canConnect) return this.setUnavailable(`Could not connect to Uponor controller on IP address ${address}`)
this._client = client
this._syncInterval = setInterval(this._syncAttributes.bind(this), POLL_INTERVAL_MS)
this._syncAttributes()
} catch (error) {
this.setUnavailable(`Could not connect to Uponor controller on IP address ${address}`)
}
}

async _uninit(): Promise<void> {
Expand All @@ -91,19 +104,37 @@ class UponorThermostatDevice extends Device {

private async _syncAttributes(): Promise<void> {
if (!this._client) return this.setUnavailable('No Uponor client')
await this._client.syncAttributes()
const { controllerID, thermostatID } = this.getData()
const data = this._client.getThermostat(controllerID, thermostatID)
if (!data) return this.setUnavailable('Could not find thermostat data')
this.setAvailable()
this.setCapabilityValue('measure_temperature', data.temperature)
this.setCapabilityValue('target_temperature', data.setPoint)

try {
await this._client.syncAttributes()
const { controllerID, thermostatID } = this.getData()
const data = this._client.getThermostat(controllerID, thermostatID)
if (!data) return this.setUnavailable('Could not find thermostat data')
this.setAvailable()
this.setCapabilityValue('measure_temperature', data.temperature)
this.setCapabilityValue('target_temperature', data.setPoint)
} catch (error) {
this.setUnavailable('Could not fetch data from Uponor controller')
}

try {
const { debugEnabled } = this.getSettings()
if (!debugEnabled) return
const debug = await this._client.debug()
this.setSettings({ apiData: JSON.stringify(debug) })
} catch (error) { }
}

private async _setTargetTemperature(value: number): Promise<void> {
if (!this._client) return
const { controllerID, thermostatID } = this.getData()
await this._client.setTargetTemperature(controllerID, thermostatID, value)

try {
const { controllerID, thermostatID } = this.getData()
await this._client.setTargetTemperature(controllerID, thermostatID, value)
} catch (error) {
this.setUnavailable('Could not send data to Uponor controller')
}

await this._syncAttributes()
}
}
Expand Down
35 changes: 32 additions & 3 deletions drivers/uponor/driver.compose.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,42 @@
{
"id": "address",
"label": {
"en": "Override IP Address"
"en": "Override IP Address",
"nl": "IP-adres overschrijven"
},
"type": "text",
"required": false,
"hint": {
"en": "Overrides the auto-discovered IP address of the device."
"en": "Overrides the auto-discovered IP address of the device.",
"nl": "Overschrijft het automatisch ontdekte IP-adres van het apparaat."
}
},
{
"type": "group",
"label": {
"en": "Troubleshooting",
"nl": "Probleemoplossing"
},
"children": [
{
"id": "debugEnabled",
"type": "checkbox",
"label": {
"en": "Enable debug data",
"nl": "Schakel probleemverhelping in"
},
"value": false
},
{
"id": "apiData",
"type": "textarea",
"label": {
"en": "Last API response",
"nl": "Laatste API-reactie"
},
"value": "{}"
}
]
}
]
}
}
44 changes: 24 additions & 20 deletions drivers/uponor/driver.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Driver } from 'homey'
import { UponorHTTPClient } from '../../lib/UponorHTTPClient'
import { Thermostat, UponorHTTPClient } from '../../lib/UponorHTTPClient'
import { PairSession } from 'homey/lib/Driver';

class UponorDriver extends Driver {
Expand Down Expand Up @@ -32,27 +32,31 @@ class UponorDriver extends Driver {
}

private async _findDevices(ipAddress: string, systemID: string): Promise<any[]> {
const devices: any[] = []
const client = new UponorHTTPClient(ipAddress)
const connected = await client.testConnection()
if (!connected) return devices

await client.syncAttributes()
client.getThermostats().forEach((thermostat) => {
devices.push({
name: thermostat.name,
data: {
id: `${systemID}_${thermostat.id}`,
controllerID: thermostat.controllerID,
thermostatID: thermostat.thermostatID,
},
store: {
address: ipAddress,
}
})
})

return devices
try {
const connected = await client.testConnection()
if (!connected) return []
await client.syncAttributes()
const thermostats = Array.from(client.getThermostats().values())
return thermostats.map(this._mapDevice.bind(this, ipAddress, systemID))
} catch (error) {
return []
}
}

private _mapDevice(ipAddress: string, systemID: string, thermostat: Thermostat): any {
return {
name: thermostat.name,
data: {
id: `${systemID}_${thermostat.id}`,
controllerID: thermostat.controllerID,
thermostatID: thermostat.thermostatID,
},
store: {
address: ipAddress,
}
}
}
}

Expand Down
53 changes: 14 additions & 39 deletions lib/UponorHTTPClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,19 @@ export class UponorHTTPClient {
this._thermostats = this._syncThermostats()
}

public async debug(): Promise<any> {
try {
const request = await fetch(this._url, {
method: 'POST',
headers: { 'x-jnap-action': 'http://phyn.com/jnap/uponorsky/GetAttributes' },
body: '{}',
})
return await request.json()
} catch (error) {
return false
}
}

public async testConnection(): Promise<boolean> {
try {
const request = await fetch(this._url, {
Expand Down Expand Up @@ -97,45 +110,7 @@ export class UponorHTTPClient {
const controllerID = matches[1] // first capture group
const thermostatID = matches[2] // second capture group
const ctKey = UponorHTTPClient._createKey(controllerID, thermostatID)

// TODO: calculate actual mode using heat/cool/eco/holiday/comfort mode attributes
// C2_T6_cool_allowed = "0"
// C2_T6_manual_cool_allowed = "0"
// C2_T6_heat_cool_mode = "0"
// C1_T1_heat_cool_slave = "0"
// C2_T6_stat_cb_comfort_eco_mode = "0"
// C1_T1_mode_comfort_eco = "0"
// C2_T6_stat_cb_eco_forced = "0"
// C1_T1_eco_profile_number = "0"
// C2_T6_stat_cb_holiday_mode = "0"
// C2_T6_stat_cb_heat_cool_mode = "0"
// C2_T6_stat_comfort_eco_mode = "0"
// C2_T6_stat_eco_program = "0"
// C2_T6_eco_setting = "0"
// C1_T1_eco_offset = "72"

// TODO: implement alarms
// C2_T6_stat_cb_fallbk_heatalarm = "0"
// C2_T6_stat_air_sensor_error = "0"
// C2_T6_stat_external_sensor_err = "0"
// C2_T6_stat_rh_sensor_error = "0"
// C2_T6_stat_tamper_alarm = "0"
// C2_T6_stat_rf_error = "0"
// C2_T6_stat_battery_error = "0"
// C2_T6_stat_rf_low_sig_warning = "0"
// C2_T6_stat_valve_position_err = "0"

// TODO: other
// C1_T1_head1_supply_temp = "54"
// C1_T1_head1_valve_pos_percent = "0"
// C1_T1_head1_valve_pos = "0"
// C1_T1_head2_valve_pos_percent = "0"
// C1_T1_head2_valve_pos = "0"
// C1_T1_head2_supply_temp = "0"
// C1_T1_channel_position = "7"
// C1_T1_head_number = "0"
// C1_T1_bypass_enable = "0"


thermostats.set(ctKey, {
id: ctKey,
name: value,
Expand Down

0 comments on commit 0329e75

Please sign in to comment.