forked from asev/homeassistant-uponor
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy path__init__.py
More file actions
335 lines (273 loc) · 13.1 KB
/
Copy path__init__.py
File metadata and controls
335 lines (273 loc) · 13.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
import math
import logging
from homeassistant.core import HomeAssistant
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.const import CONF_HOST
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.event import async_track_time_interval
from homeassistant.helpers.storage import Store
from UponorJnap import UponorJnap
import homeassistant.util.dt as dt_util
from .const import (
DOMAIN,
SIGNAL_UPONOR_STATE_UPDATE,
SCAN_INTERVAL,
STORAGE_KEY,
STORAGE_VERSION,
STATUS_OK,
STATUS_ERROR_BATTERY,
STATUS_ERROR_VALVE,
STATUS_ERROR_GENERAL,
STATUS_ERROR_AIR_SENSOR,
STATUS_ERROR_EXT_SENSOR,
STATUS_ERROR_RH_SENSOR,
STATUS_ERROR_RF_SENSOR,
STATUS_ERROR_TAMPER,
STATUS_ERROR_TOO_HIGH_TEMP,
TOO_HIGH_TEMP_LIMIT,
TOO_LOW_HUMIDITY_LIMIT,
DEFAULT_TEMP
)
_LOGGER = logging.getLogger(__name__)
PLATFORMS = [Platform.CLIMATE, Platform.SWITCH, Platform.SENSOR]
async def async_setup(hass: HomeAssistant, config: dict):
hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN]["config"] = config.get(DOMAIN) or {}
return True
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry):
host = config_entry.data[CONF_HOST]
store = Store(hass, STORAGE_VERSION, STORAGE_KEY)
state_proxy = await hass.async_add_executor_job(lambda: UponorStateProxy(hass, host, store))
await state_proxy.async_update()
thermostats = state_proxy.get_active_thermostats()
hass.data[DOMAIN] = {
"state_proxy": state_proxy,
"thermostats": thermostats
}
def handle_set_variable(call):
var_name = call.data.get('var_name')
var_value = call.data.get('var_value')
_LOGGER.debug(f"Setting variable: {var_name} to: {var_value}")
hass.data[DOMAIN]['state_proxy'].set_variable(var_name, var_value)
hass.services.async_register(DOMAIN, "set_variable", handle_set_variable)
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
async_track_time_interval(hass, state_proxy.async_update, SCAN_INTERVAL)
config_entry.async_on_unload(config_entry.add_update_listener(async_update_options))
return True
async def async_update_options(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Update options."""
_LOGGER.debug("Update setup entry: %s, data: %s, options: %s", entry.entry_id, entry.data, entry.options)
await hass.config_entries.async_reload(entry.entry_id)
async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Unload a config entry."""
_LOGGER.debug("Unloading setup entry: %s, data: %s, options: %s", config_entry.entry_id, config_entry.data, config_entry.options)
unload_ok = await hass.config_entries.async_unload_platforms(config_entry, [Platform.SWITCH, Platform.CLIMATE, Platform.SENSOR])
_LOGGER.debug(f"Unload status for Uponor platforms: {unload_ok}")
return unload_ok
class UponorStateProxy:
def __init__(self, hass, host, store):
self._hass = hass
self._client = UponorJnap(host)
self._store = store
self._data = {}
self._storage_data = {}
self.next_sp_from_dt = None
# Thermostats config
def get_active_thermostats(self):
active = []
for c in range(1, 5):
var = 'sys_controller_' + str(c) + '_presence'
if var in self._data and self._data[var] != "1":
continue
for i in range(1, 13):
var = 'C' + str(c) + '_thermostat_' + str(i) + '_presence'
if var in self._data and self._data[var] == "1":
active.append('C' + str(c) + '_T' + str(i))
return active
def get_room_name(self, thermostat):
var = 'cust_' + thermostat + '_name'
if var in self._data:
return self._data[var]
return thermostat
def get_thermostat_id(self, thermostat):
var = thermostat.replace('T', 'thermostat') + '_id'
if var in self._data:
return self._data[var]
def get_model(self):
var = 'cust_SW_version_update'
if var in self._data:
return self._data[var].split('_')[0]
return '-'
def get_version(self, thermostat):
var = thermostat[0:3] + 'sw_version'
if var in self._data:
return self._data[var].split('_')[0]
# Temperatures & humidity
def get_temperature(self, thermostat):
var = thermostat + '_room_temperature'
if var in self._data and int(self._data[var]) <= TOO_HIGH_TEMP_LIMIT:
return round((int(self._data[var]) - 320) / 18, 1)
def get_min_limit(self, thermostat):
var = thermostat + '_minimum_setpoint'
if var in self._data:
return round((int(self._data[var]) - 320) / 18, 1)
def get_max_limit(self, thermostat):
var = thermostat + '_maximum_setpoint'
if var in self._data:
return round((int(self._data[var]) - 320) / 18, 1)
def has_humidity_sensor(self, thermostat):
var = thermostat + '_rh'
return var in self._data and int(self._data[var]) != 0
def get_humidity(self, thermostat):
var = thermostat + '_rh'
if var in self._data and int(self._data[var]) >= TOO_LOW_HUMIDITY_LIMIT:
return int(self._data[var])
def has_floor_temperature(self, thermostat):
var = thermostat + '_external_temperature'
return var in self._data and int(self._data[var]) != 32767
def get_floor_temperature(self, thermostat):
var = thermostat + '_external_temperature'
if var in self._data:
temp = int(self._data[var])
if temp != 32767 and temp <= TOO_HIGH_TEMP_LIMIT:
return round((temp - 320) / 18, 1)
return None
# Temperature setpoint
def get_setpoint(self, thermostat):
var = thermostat + '_setpoint'
if var in self._data:
temp = math.floor((int(self._data[var]) - 320) / 1.8) / 10
return math.floor((int(self._data[var]) - self.get_active_setback(thermostat, temp) - 320) / 1.8) / 10
def get_active_setback(self, thermostat, temp):
if temp == self.get_min_limit(thermostat) or temp == self.get_max_limit(thermostat):
return 0
cool_setback = 0
var_cool_setback = 'sys_heat_cool_offset'
if var_cool_setback in self._data and self.is_cool_enabled():
cool_setback = int(self._data[var_cool_setback]) * -1
eco_setback = 0
var_eco_setback = thermostat + '_eco_offset'
mode = -1 if self.is_cool_enabled() else 1
if var_eco_setback in self._data and (self.is_eco(thermostat) or self.is_away()):
eco_setback = int(self._data[var_eco_setback]) * mode
return cool_setback + eco_setback
# State
def is_active(self, thermostat):
var = thermostat + '_stat_cb_actuator'
if var in self._data:
return self._data[var] == "1"
def get_pwm(self, thermostat):
var = thermostat + '_ufh_pwm_output'
if var in self._data:
return int(self._data[var])
def get_status(self, thermostat):
var = thermostat + '_stat_battery_error'
if var in self._data and self._data[var] == "1":
return STATUS_ERROR_BATTERY
var = thermostat + '_stat_valve_position_err"'
if var in self._data and self._data[var] == "1":
return STATUS_ERROR_VALVE
var = thermostat[0:3] + 'stat_general_system_alarm'
if var in self._data and self._data[var] == "1":
return STATUS_ERROR_GENERAL
var = thermostat + '_stat_air_sensor_error'
if var in self._data and self._data[var] == "1":
return STATUS_ERROR_AIR_SENSOR
var = thermostat + '_stat_external_sensor_err'
if var in self._data and self._data[var] == "1":
return STATUS_ERROR_EXT_SENSOR
var = thermostat + '_stat_rh_sensor_error'
if var in self._data and self._data[var] == "1":
return STATUS_ERROR_RH_SENSOR
var = thermostat + '_stat_rf_error'
if var in self._data and self._data[var] == "1":
return STATUS_ERROR_RF_SENSOR
var = thermostat + '_stat_tamper_alarm'
if var in self._data and self._data[var] == "1":
return STATUS_ERROR_TAMPER
var = thermostat + '_room_temperature'
if var in self._data and int(self._data[var]) > TOO_HIGH_TEMP_LIMIT:
return STATUS_ERROR_TOO_HIGH_TEMP
return STATUS_OK
# HVAC modes
async def async_switch_to_cooling(self):
for thermostat in self._hass.data[DOMAIN]['thermostats']:
if self.get_setpoint(thermostat) == self.get_min_limit(thermostat):
await self.set_setpoint(thermostat, self.get_max_limit(thermostat))
await self._hass.async_add_executor_job(lambda: self._client.send_data({'sys_heat_cool_mode': '1'}))
self._data['sys_heat_cool_mode'] = '1'
self._hass.async_add_job(async_dispatcher_send, self._hass, SIGNAL_UPONOR_STATE_UPDATE)
async def async_switch_to_heating(self):
for thermostat in self._hass.data[DOMAIN]['thermostats']:
if self.get_setpoint(thermostat) == self.get_max_limit(thermostat):
await self.set_setpoint(thermostat, self.get_min_limit(thermostat))
await self._hass.async_add_executor_job(lambda: self._client.send_data({'sys_heat_cool_mode': '0'}))
self._data['sys_heat_cool_mode'] = '0'
self._hass.async_add_job(async_dispatcher_send, self._hass, SIGNAL_UPONOR_STATE_UPDATE)
async def async_turn_on(self, thermostat):
data = await self._store.async_load()
self._storage_data = {} if data is None else data
last_temp = self._storage_data[thermostat] if thermostat in self._storage_data else DEFAULT_TEMP
await self.set_setpoint(thermostat, last_temp)
async def async_turn_off(self, thermostat):
data = await self._store.async_load()
self._storage_data = {} if data is None else data
self._storage_data[thermostat] = self.get_setpoint(thermostat)
await self._store.async_save(self._storage_data)
off_temp = self.get_max_limit(thermostat) if self.is_cool_enabled() else self.get_min_limit(thermostat)
await self.set_setpoint(thermostat, off_temp)
# Cooling
def is_cool_available(self):
var = 'sys_cooling_available'
if var in self._data:
return self._data[var] == "1"
def is_cool_enabled(self):
var = 'sys_heat_cool_mode'
if var in self._data:
return self._data[var] == "1"
# Away & Eco
def is_away(self):
var = 'sys_forced_eco_mode'
return var in self._data and self._data[var] == "1"
async def async_set_away(self, is_away):
var = 'sys_forced_eco_mode'
data = "1" if is_away else "0"
await self._hass.async_add_executor_job(lambda: self._client.send_data({var: data}))
self._data[var] = data
self._hass.async_add_job(async_dispatcher_send, self._hass, SIGNAL_UPONOR_STATE_UPDATE)
def is_eco(self, thermostat):
if self.get_eco_setback(thermostat) == 0:
return False
var = thermostat + '_stat_cb_comfort_eco_mode'
var_temp = 'cust_Temporary_ECO_Activation'
return (var in self._data and self._data[var] == "1") or (
var_temp in self._data and self._data[var_temp] == "1")
def get_eco_setback(self, thermostat):
var = thermostat + '_eco_offset'
if var in self._data:
return round(int(self._data[var]) / 18, 1)
def get_last_update(self):
return self.next_sp_from_dt
# Rest
async def async_update(self,_=None):
try:
_LOGGER.debug("Running async_update to get data from Uponor thermostat.")
self.next_sp_from_dt = dt_util.now()
self._data = await self._hass.async_add_executor_job(lambda: self._client.get_data())
# For deep debug remove comment and you get the full json message.
# _LOGGER.debug(f"Data recieved from Uponor: {self._data}")
self._hass.async_add_job(async_dispatcher_send, self._hass, SIGNAL_UPONOR_STATE_UPDATE)
except Exception as ex:
_LOGGER.error("Uponor thermostat was unable to update: %s", ex)
def set_variable(self, var_name, var_value):
_LOGGER.debug("Called set variable: name: %s, value: %s, data: %s", var_name, var_value, self._data)
self._client.send_data({var_name: var_value})
self._data[var_name] = var_value
self._hass.async_add_job(async_dispatcher_send, self._hass, SIGNAL_UPONOR_STATE_UPDATE)
async def set_setpoint(self, thermostat, temp):
var = thermostat + '_setpoint'
setpoint = int(temp * 18 + self.get_active_setback(thermostat, temp) + 320)
await self._hass.async_add_executor_job(lambda: self._client.send_data({var: setpoint}))
self._data[var] = setpoint
self._hass.async_add_job(async_dispatcher_send, self._hass, SIGNAL_UPONOR_STATE_UPDATE)