|
|
@@ -7,6 +7,8 @@ https://github.com/codetheweb/tuyapi/issues/31.
|
|
|
"""
|
|
|
import logging
|
|
|
import json
|
|
|
+from time import time
|
|
|
+from threading import Timer, Lock
|
|
|
|
|
|
import voluptuous as vol
|
|
|
from homeassistant.components.climate import (
|
|
|
@@ -17,7 +19,6 @@ from homeassistant.const import (
|
|
|
CONF_NAME, CONF_HOST, STATE_ON, STATE_OFF, STATE_UNAVAILABLE, TEMP_CELSIUS
|
|
|
)
|
|
|
import homeassistant.helpers.config_validation as cv
|
|
|
-from threading import Lock
|
|
|
|
|
|
REQUIREMENTS = ['pytuya==7.0']
|
|
|
|
|
|
@@ -43,7 +44,7 @@ ATTR_POWER_LEVEL = 'power_level'
|
|
|
ATTR_TIMER_MINUTES = 'timer_minutes'
|
|
|
ATTR_TIMER_ON = 'timer_on'
|
|
|
ATTR_DISPLAY_ON = 'display_on'
|
|
|
-ATTR_USER_MODE = 'user_mode' # not sure what this does
|
|
|
+ATTR_USER_MODE = 'user_mode' # not sure what this does
|
|
|
ATTR_ECO_TARGET_TEMPERATURE = 'eco_' + ATTR_TARGET_TEMPERATURE
|
|
|
|
|
|
GOLDAIR_PROPERTY_TO_DPS_ID = {
|
|
|
@@ -57,7 +58,7 @@ GOLDAIR_PROPERTY_TO_DPS_ID = {
|
|
|
ATTR_TIMER_MINUTES: '102',
|
|
|
ATTR_TIMER_ON: '103',
|
|
|
ATTR_DISPLAY_ON: '104',
|
|
|
- ATTR_USER_MODE: '105', # not sure what this does
|
|
|
+ ATTR_USER_MODE: '105', # not sure what this does
|
|
|
ATTR_ECO_TARGET_TEMPERATURE: '106'
|
|
|
}
|
|
|
|
|
|
@@ -67,7 +68,7 @@ GOLDAIR_MODE_TO_DPS_MODE = {
|
|
|
STATE_ANTI_FREEZE: 'AF'
|
|
|
}
|
|
|
GOLDAIR_POWER_LEVELS = ['stop', '1', '2', '3', '4', '5', 'auto']
|
|
|
-GOLDAIR_USER_MODES = ['auto', 'user'] # not sure what this does
|
|
|
+GOLDAIR_USER_MODES = ['auto', 'user'] # not sure what this does
|
|
|
|
|
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
|
|
vol.Required(CONF_NAME): cv.string,
|
|
|
@@ -79,7 +80,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
|
|
vol.Optional(CONF_DISPLAY_ON): cv.boolean
|
|
|
})
|
|
|
|
|
|
-SUPPORT_FLAGS = SUPPORT_ON_OFF | SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE
|
|
|
+SUPPORT_FLAGS = SUPPORT_ON_OFF | SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE
|
|
|
|
|
|
|
|
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
|
|
@@ -103,6 +104,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|
|
GoldairHeater(config.get(CONF_NAME), device)
|
|
|
])
|
|
|
|
|
|
+
|
|
|
class GoldairHeater(ClimateDevice):
|
|
|
"""Representation of a Goldair WiFi heater."""
|
|
|
|
|
|
@@ -207,9 +209,9 @@ class GoldairHeater(ClimateDevice):
|
|
|
self._device.refresh()
|
|
|
|
|
|
|
|
|
+# HA prefers this to be lazy-imported, however we can't do that because it needs to be subclassed
|
|
|
import pytuya
|
|
|
-from time import time
|
|
|
-from threading import Timer
|
|
|
+
|
|
|
|
|
|
class GoldairHeaterDevice(pytuya.Device):
|
|
|
def __init__(self, dev_id, address, local_key):
|
|
|
@@ -222,7 +224,7 @@ class GoldairHeaterDevice(pytuya.Device):
|
|
|
local_key (str): The encryption key.
|
|
|
"""
|
|
|
super().__init__(dev_id, address, local_key, 'device')
|
|
|
-
|
|
|
+
|
|
|
self._fixed_properties = {}
|
|
|
self._reset_cached_state()
|
|
|
|
|
|
@@ -270,16 +272,16 @@ class GoldairHeaterDevice(pytuya.Device):
|
|
|
@property
|
|
|
def max_target_temperature(self):
|
|
|
return self._MAX_TARGET_TEMPERATURE
|
|
|
-
|
|
|
+
|
|
|
def set_target_temperature(self, target_temperature):
|
|
|
target_temperature = int(round(target_temperature))
|
|
|
if not self._MIN_TARGET_TEMPERATURE <= target_temperature <= self._MAX_TARGET_TEMPERATURE:
|
|
|
raise ValueError(
|
|
|
f'Target temperature ({target_temperature}) must be between '
|
|
|
f'{self._MIN_TARGET_TEMPERATURE} and {self._MAX_TARGET_TEMPERATURE}'
|
|
|
- )
|
|
|
+ )
|
|
|
self._set_properties({ATTR_TARGET_TEMPERATURE: target_temperature})
|
|
|
-
|
|
|
+
|
|
|
@property
|
|
|
def current_temperature(self):
|
|
|
return self._get_cached_state()[ATTR_TEMPERATURE]
|
|
|
@@ -293,8 +295,8 @@ class GoldairHeaterDevice(pytuya.Device):
|
|
|
return list(GOLDAIR_MODE_TO_DPS_MODE.keys())
|
|
|
|
|
|
def set_operation_mode(self, new_mode):
|
|
|
- if not new_mode in GOLDAIR_MODE_TO_DPS_MODE:
|
|
|
- raise ValueError(f'Invalid mode: {new_mode}')
|
|
|
+ if new_mode not in GOLDAIR_MODE_TO_DPS_MODE:
|
|
|
+ raise ValueError(f'Invalid mode: {new_mode}')
|
|
|
self._set_properties({ATTR_OPERATION_MODE: new_mode})
|
|
|
|
|
|
@property
|
|
|
@@ -316,7 +318,7 @@ class GoldairHeaterDevice(pytuya.Device):
|
|
|
return self._get_cached_state()[ATTR_POWER_LEVEL]
|
|
|
|
|
|
def set_power_level(self, new_level):
|
|
|
- if not new_level in GOLDAIR_POWER_LEVELS:
|
|
|
+ if new_level not in GOLDAIR_POWER_LEVELS:
|
|
|
raise ValueError(f'Invalid power level: {new_level}')
|
|
|
self._set_properties({ATTR_POWER_LEVEL: new_level})
|
|
|
|
|
|
@@ -327,7 +329,7 @@ class GoldairHeaterDevice(pytuya.Device):
|
|
|
@property
|
|
|
def is_timer_on(self):
|
|
|
return self._get_cached_state()[ATTR_TIMER_ON]
|
|
|
-
|
|
|
+
|
|
|
def start_timer(self, minutes):
|
|
|
self._set_properties({
|
|
|
ATTR_TIMER_ON: True,
|
|
|
@@ -340,10 +342,10 @@ class GoldairHeaterDevice(pytuya.Device):
|
|
|
@property
|
|
|
def is_display_on(self):
|
|
|
return self._get_cached_state()[ATTR_DISPLAY_ON]
|
|
|
-
|
|
|
+
|
|
|
def turn_display_on(self):
|
|
|
self._set_properties({ATTR_DISPLAY_ON: True})
|
|
|
-
|
|
|
+
|
|
|
def turn_display_off(self):
|
|
|
self._set_properties({ATTR_DISPLAY_ON: False})
|
|
|
|
|
|
@@ -352,16 +354,16 @@ class GoldairHeaterDevice(pytuya.Device):
|
|
|
return self._get_cached_state()[ATTR_USER_MODE]
|
|
|
|
|
|
def set_user_mode(self, new_mode):
|
|
|
- if not new_mode in GOLDAIR_USER_MODES:
|
|
|
+ if new_mode not in GOLDAIR_USER_MODES:
|
|
|
raise ValueError(f'Invalid user mode: {new_mode}')
|
|
|
self._set_properties({ATTR_USER_MODE: new_mode})
|
|
|
|
|
|
@property
|
|
|
def eco_target_temperature(self):
|
|
|
return self._get_cached_state()[ATTR_ECO_TARGET_TEMPERATURE]
|
|
|
-
|
|
|
+
|
|
|
def set_eco_target_temperature(self, eco_target_temperature):
|
|
|
- self._set_properties({ATTR_ECO_TARGET_TEMPERATURE})
|
|
|
+ self._set_properties({ATTR_ECO_TARGET_TEMPERATURE: eco_target_temperature})
|
|
|
|
|
|
def set_fixed_properties(self, fixed_properties):
|
|
|
self._fixed_properties = fixed_properties
|
|
|
@@ -373,7 +375,7 @@ class GoldairHeaterDevice(pytuya.Device):
|
|
|
cached_state = self._get_cached_state()
|
|
|
if now - cached_state['updated_at'] >= self._CACHE_TIMEOUT:
|
|
|
self._retry_on_failed_connection(lambda: self._refresh_cached_state(), 'Failed to refresh device state.')
|
|
|
-
|
|
|
+
|
|
|
def _reset_cached_state(self):
|
|
|
self._cached_state = {
|
|
|
ATTR_ON: None,
|
|
|
@@ -398,7 +400,7 @@ class GoldairHeaterDevice(pytuya.Device):
|
|
|
_LOGGER.info(f'refreshed device state: {json.dumps(new_state)}')
|
|
|
_LOGGER.debug(f'new cache state: {json.dumps(self._cached_state)}')
|
|
|
_LOGGER.debug(f'new cache state (including pending properties): {json.dumps(self._get_cached_state())}')
|
|
|
-
|
|
|
+
|
|
|
def _set_properties(self, properties):
|
|
|
if len(properties) == 0:
|
|
|
return
|
|
|
@@ -409,14 +411,14 @@ class GoldairHeaterDevice(pytuya.Device):
|
|
|
def _add_properties_to_pending_updates(self, properties):
|
|
|
now = time()
|
|
|
properties = {**properties, **self._fixed_properties}
|
|
|
-
|
|
|
+
|
|
|
pending_updates = self._get_pending_updates()
|
|
|
for key, value in properties.items():
|
|
|
pending_updates[key] = {
|
|
|
'value': value,
|
|
|
'updated_at': now
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
_LOGGER.debug(f'new pending updates: {json.dumps(self._pending_updates)}')
|
|
|
|
|
|
def _debounce_sending_updates(self):
|
|
|
@@ -426,10 +428,10 @@ class GoldairHeaterDevice(pytuya.Device):
|
|
|
pass
|
|
|
self._debounce = Timer(1, self._send_pending_updates)
|
|
|
self._debounce.start()
|
|
|
-
|
|
|
+
|
|
|
def _send_pending_updates(self):
|
|
|
pending_properties = self._get_pending_properties()
|
|
|
- new_state = self._generate_dps_payload_for_properties(pending_properties)
|
|
|
+ new_state = GoldairHeaterDevice._generate_dps_payload_for_properties(pending_properties)
|
|
|
payload = self.generate_payload('set', new_state)
|
|
|
|
|
|
_LOGGER.debug(f'sending updated properties: {json.dumps(pending_properties)}')
|
|
|
@@ -443,9 +445,8 @@ class GoldairHeaterDevice(pytuya.Device):
|
|
|
self._send_receive(payload)
|
|
|
now = time()
|
|
|
pending_updates = self._get_pending_updates()
|
|
|
- for key, value in properties.items():
|
|
|
- if key in pending_updates:
|
|
|
- pending_updates[key]['updated_at'] = now
|
|
|
+ for key, value in pending_updates.items():
|
|
|
+ pending_updates[key]['updated_at'] = now
|
|
|
finally:
|
|
|
self._lock.release()
|
|
|
|
|
|
@@ -469,7 +470,7 @@ class GoldairHeaterDevice(pytuya.Device):
|
|
|
def _get_pending_updates(self):
|
|
|
now = time()
|
|
|
self._pending_updates = {key: value for key, value in self._pending_updates.items()
|
|
|
- if now - value['updated_at'] < self._FAKE_IT_TIL_YOU_MAKE_IT_TIMEOUT}
|
|
|
+ if now - value['updated_at'] < self._FAKE_IT_TIL_YOU_MAKE_IT_TIMEOUT}
|
|
|
return self._pending_updates
|
|
|
|
|
|
def _update_cached_state_from_dps(self, dps):
|
|
|
@@ -479,12 +480,13 @@ class GoldairHeaterDevice(pytuya.Device):
|
|
|
if dps_id in dps:
|
|
|
value = dps[dps_id]
|
|
|
if dps_id == GOLDAIR_PROPERTY_TO_DPS_ID[ATTR_OPERATION_MODE]:
|
|
|
- self._cached_state[key] = self._get_key_for_value(GOLDAIR_MODE_TO_DPS_MODE, value)
|
|
|
+ self._cached_state[key] = GoldairHeaterDevice._get_key_for_value(GOLDAIR_MODE_TO_DPS_MODE, value)
|
|
|
else:
|
|
|
self._cached_state[key] = value
|
|
|
self._cached_state['updated_at'] = now
|
|
|
|
|
|
- def _generate_dps_payload_for_properties(self, properties):
|
|
|
+ @staticmethod
|
|
|
+ def _generate_dps_payload_for_properties(properties):
|
|
|
dps = {}
|
|
|
|
|
|
for key, dps_id in GOLDAIR_PROPERTY_TO_DPS_ID.items():
|
|
|
@@ -497,7 +499,8 @@ class GoldairHeaterDevice(pytuya.Device):
|
|
|
|
|
|
return dps
|
|
|
|
|
|
- def _get_key_for_value(self, obj, value):
|
|
|
+ @staticmethod
|
|
|
+ def _get_key_for_value(obj, value):
|
|
|
keys = list(obj.keys())
|
|
|
values = list(obj.values())
|
|
|
return keys[values.index(value)]
|