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

Fix: network crashes #9

Merged
merged 4 commits into from
May 22, 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
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
Loading