diff --git a/.gitignore b/.gitignore index 6a08ed4..4358842 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,2 @@ -/.idea/ -__pycache__/ +/.idea/ +__pycache__/ diff --git a/README.md b/README.md index 5c3afb9..85dea1c 100644 --- a/README.md +++ b/README.md @@ -34,8 +34,11 @@ Work is in progress to support Goldair WiFi dehumidifiers. Installation ------------ -The preferred installation method is via [Custom Updater](https://github.com/custom-components/custom_updater). Once -you have Custom Updater set up, simply go to the dev-service page +The preferred installation method is via [HACS](https://hacs.xyz/). Once you have HACS set up, simply follow the +[instructions for adding a custom repository](https://hacs.xyz/docs/navigation/settings#custom-repositories). + +You can also use [Custom Updater](https://github.com/custom-components/custom_updater). Once you have Custom Updater set +up, simply go to the dev-service page The dev-service icon and call the `custom_updater.install` service with this service data: ```json @@ -77,8 +80,9 @@ goldair_climate: #### type     *(string) (Required)* The type of Goldair device. Currently `heater` is the only option; a - future update will add support for dehumidifiers, so setting the type now - will prevent the component breaking when this functionality is released. + future update will add support for dehumidifiers and other devices, so + setting the type now will prevent the component breaking when this + functionality is released. #### climate     *(boolean) (Optional)* Whether to surface this heater as a climate device. diff --git a/custom_components/goldair_climate/__init__.py b/custom_components/goldair_climate/__init__.py index d796633..ea53445 100644 --- a/custom_components/goldair_climate/__init__.py +++ b/custom_components/goldair_climate/__init__.py @@ -1,468 +1,224 @@ -""" -Platform for Goldair WiFi-connected heaters and panels. - -Based on sean6541/tuya-homeassistant for service call logic, and TarxBoy's -investigation into Goldair's tuyapi statuses -https://github.com/codetheweb/tuyapi/issues/31. -""" -from time import time -from threading import Timer, Lock -import logging -import json -import voluptuous as vol - -import homeassistant.helpers.config_validation as cv -from homeassistant.const import (CONF_NAME, CONF_HOST, ATTR_TEMPERATURE, TEMP_CELSIUS) -from homeassistant.components.climate import ATTR_OPERATION_MODE -from homeassistant.helpers.discovery import load_platform - -VERSION = '0.0.3' -REQUIREMENTS = ['pytuya==7.0'] - -_LOGGER = logging.getLogger(__name__) - -DOMAIN = 'goldair_climate' -DATA_GOLDAIR_CLIMATE = 'data_goldair_climate' - - -CONF_DEVICE_ID = 'device_id' -CONF_LOCAL_KEY = 'local_key' -CONF_TYPE = 'type' -CONF_TYPE_HEATER = 'heater' -CONF_CLIMATE = 'climate' -CONF_SENSOR = 'sensor' -CONF_CHILD_LOCK = 'child_lock' -CONF_DISPLAY_LIGHT = 'display_light' - -ATTR_ON = 'on' -ATTR_TARGET_TEMPERATURE = 'target_temperature' -ATTR_CHILD_LOCK = 'child_lock' -ATTR_FAULT = 'fault' -ATTR_POWER_LEVEL = 'power_level' -ATTR_TIMER_MINUTES = 'timer_minutes' -ATTR_TIMER_ON = 'timer_on' -ATTR_DISPLAY_ON = 'display_on' -ATTR_POWER_MODE = 'power_mode' -ATTR_ECO_TARGET_TEMPERATURE = 'eco_' + ATTR_TARGET_TEMPERATURE - -STATE_COMFORT = 'Comfort' -STATE_ECO = 'Eco' -STATE_ANTI_FREEZE = 'Anti-freeze' - -GOLDAIR_PROPERTY_TO_DPS_ID = { - ATTR_ON: '1', - ATTR_TARGET_TEMPERATURE: '2', - ATTR_TEMPERATURE: '3', - ATTR_OPERATION_MODE: '4', - ATTR_CHILD_LOCK: '6', - ATTR_FAULT: '12', - ATTR_POWER_LEVEL: '101', - ATTR_TIMER_MINUTES: '102', - ATTR_TIMER_ON: '103', - ATTR_DISPLAY_ON: '104', - ATTR_POWER_MODE: '105', - ATTR_ECO_TARGET_TEMPERATURE: '106' -} - -GOLDAIR_MODE_TO_DPS_MODE = { - STATE_COMFORT: 'C', - STATE_ECO: 'ECO', - STATE_ANTI_FREEZE: 'AF' -} -GOLDAIR_POWER_LEVEL_TO_DPS_LEVEL = { - 'Stop': 'stop', - '1': '1', - '2': '2', - '3': '3', - '4': '4', - '5': '5', - 'Auto': 'auto' -} -GOLDAIR_POWER_MODES = ['auto', 'user'] - -PLATFORM_SCHEMA = vol.Schema({ - vol.Required(CONF_NAME): cv.string, - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_DEVICE_ID): cv.string, - vol.Required(CONF_LOCAL_KEY): cv.string, - vol.Required(CONF_TYPE): vol.In([CONF_TYPE_HEATER]), - vol.Optional(CONF_CLIMATE, default=True): cv.boolean, - vol.Optional(CONF_SENSOR, default=False): cv.boolean, - vol.Optional(CONF_DISPLAY_LIGHT, default=False): cv.boolean, - vol.Optional(CONF_CHILD_LOCK, default=False): cv.boolean -}) - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.All(cv.ensure_list, [PLATFORM_SCHEMA]) -}, extra=vol.ALLOW_EXTRA) - - -def setup(hass, config): - hass.data[DOMAIN] = {} - for device_config in config.get(DOMAIN, []): - host = device_config.get(CONF_HOST) - - device = GoldairHeaterDevice( - device_config.get(CONF_NAME), - device_config.get(CONF_DEVICE_ID), - device_config.get(CONF_HOST), - device_config.get(CONF_LOCAL_KEY) - ) - hass.data[DOMAIN][host] = device - - if device_config.get(CONF_TYPE) == CONF_TYPE_HEATER: - discovery_info = {'host': host, 'type': 'heater'} - if device_config.get(CONF_CLIMATE): - load_platform(hass, 'climate', DOMAIN, discovery_info, config) - if device_config.get(CONF_SENSOR): - load_platform(hass, 'sensor', DOMAIN, discovery_info, config) - if device_config.get(CONF_DISPLAY_LIGHT): - load_platform(hass, 'light', DOMAIN, discovery_info, config) - if device_config.get(CONF_CHILD_LOCK): - load_platform(hass, 'lock', DOMAIN, discovery_info, config) - - return True - - -class GoldairHeaterDevice(object): - def __init__(self, name, dev_id, address, local_key): - """ - Represents a Goldair Heater device. - - Args: - dev_id (str): The device id. - address (str): The network address. - local_key (str): The encryption key. - """ - import pytuya - self._name = name - self._api = pytuya.Device(dev_id, address, local_key, 'device') - - self._fixed_properties = {} - self._reset_cached_state() - - self._TEMPERATURE_UNIT = TEMP_CELSIUS - self._TEMPERATURE_STEP = 1 - self._TEMPERATURE_LIMITS = { - STATE_COMFORT: { - 'min': 5, - 'max': 35 - }, - STATE_ECO: { - 'min': 5, - 'max': 21 - } - } - - # API calls to update Goldair heaters are asynchronous and non-blocking. This means - # you can send a change and immediately request an updated state (like HA does), - # but because it has not yet finished processing you will be returned the old state. - # The solution is to keep a temporary list of changed properties that we can overlay - # onto the state while we wait for the board to update its switches. - self._FAKE_IT_TIL_YOU_MAKE_IT_TIMEOUT = 10 - self._CACHE_TIMEOUT = 20 - self._CONNECTION_ATTEMPTS = 2 - self._lock = Lock() - - @property - def name(self): - return self._name - - @property - def is_on(self): - return self._get_cached_state()[ATTR_ON] - - def turn_on(self): - self._set_properties({ATTR_ON: True}) - - def turn_off(self): - self._set_properties({ATTR_ON: False}) - - @property - def temperature_unit(self): - return self._TEMPERATURE_UNIT - - @property - def target_temperature(self): - state = self._get_cached_state() - if self.operation_mode == STATE_COMFORT: - return state[ATTR_TARGET_TEMPERATURE] - elif self.operation_mode == STATE_ECO: - return state[ATTR_ECO_TARGET_TEMPERATURE] - else: - return None - - @property - def target_temperature_step(self): - return self._TEMPERATURE_STEP - - @property - def min_target_teperature(self): - if self.operation_mode and self.operation_mode != STATE_ANTI_FREEZE: - return self._TEMPERATURE_LIMITS[self.operation_mode]['min'] - else: - return None - - @property - def max_target_temperature(self): - if self.operation_mode and self.operation_mode != STATE_ANTI_FREEZE: - return self._TEMPERATURE_LIMITS[self.operation_mode]['max'] - else: - return None - - def set_target_temperature(self, target_temperature): - target_temperature = int(round(target_temperature)) - operation_mode = self.operation_mode - - if operation_mode == STATE_ANTI_FREEZE: - raise ValueError('You cannot set the temperature in Anti-freeze mode.') - - limits = self._TEMPERATURE_LIMITS[operation_mode] - if not limits['min'] <= target_temperature <= limits['max']: - raise ValueError( - f'Target temperature ({target_temperature}) must be between ' - f'{limits["min"]} and {limits["max"]}' - ) - - if operation_mode == STATE_COMFORT: - self._set_properties({ATTR_TARGET_TEMPERATURE: target_temperature}) - elif operation_mode == STATE_ECO: - self._set_properties({ATTR_ECO_TARGET_TEMPERATURE: target_temperature}) - - @property - def current_temperature(self): - return self._get_cached_state()[ATTR_TEMPERATURE] - - @property - def operation_mode(self): - return self._get_cached_state()[ATTR_OPERATION_MODE] - - @property - def operation_mode_list(self): - return list(GOLDAIR_MODE_TO_DPS_MODE.keys()) - - def set_operation_mode(self, 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 - def is_child_locked(self): - return self._get_cached_state()[ATTR_CHILD_LOCK] - - def enable_child_lock(self): - self._set_properties({ATTR_CHILD_LOCK: True}) - - def disable_child_lock(self): - self._set_properties({ATTR_CHILD_LOCK: False}) - - @property - def is_faulted(self): - return self._get_cached_state()[ATTR_FAULT] - - @property - def power_level(self): - power_mode = self._get_cached_state()[ATTR_POWER_MODE] - if power_mode == 'user': - return self._get_cached_state()[ATTR_POWER_LEVEL] - elif power_mode == 'auto': - return 'Auto' - else: - return None - - @property - def power_level_list(self): - return list(GOLDAIR_POWER_LEVEL_TO_DPS_LEVEL.keys()) - - def set_power_level(self, new_level): - if new_level not in GOLDAIR_POWER_LEVEL_TO_DPS_LEVEL.keys(): - raise ValueError(f'Invalid power level: {new_level}') - self._set_properties({ATTR_POWER_LEVEL: new_level}) - - @property - def timer_timeout_in_minutes(self): - return self._get_cached_state()[ATTR_TIMER_MINUTES] - - @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, - ATTR_TIMER_MINUTES: minutes - }) - - def stop_timer(self): - self._set_properties({ATTR_TIMER_ON: False}) - - @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}) - - @property - def power_mode(self): - return self._get_cached_state()[ATTR_POWER_MODE] - - def set_power_mode(self, new_mode): - if new_mode not in GOLDAIR_POWER_MODES: - raise ValueError(f'Invalid user mode: {new_mode}') - self._set_properties({ATTR_POWER_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: eco_target_temperature}) - - def set_fixed_properties(self, fixed_properties): - self._fixed_properties = fixed_properties - set_fixed_properties = Timer(10, lambda: self._set_properties(self._fixed_properties)) - set_fixed_properties.start() - - def refresh(self): - now = time() - 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, - ATTR_TARGET_TEMPERATURE: None, - ATTR_TEMPERATURE: None, - ATTR_OPERATION_MODE: None, - ATTR_CHILD_LOCK: None, - ATTR_FAULT: None, - ATTR_POWER_LEVEL: None, - ATTR_TIMER_MINUTES: None, - ATTR_TIMER_ON: None, - ATTR_DISPLAY_ON: None, - ATTR_POWER_MODE: None, - ATTR_ECO_TARGET_TEMPERATURE: None, - 'updated_at': 0 - } - self._pending_updates = {} - - def _refresh_cached_state(self): - new_state = self._api.status() - self._update_cached_state_from_dps(new_state['dps']) - _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 - - self._add_properties_to_pending_updates(properties) - self._debounce_sending_updates() - - 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): - try: - self._debounce.cancel() - except AttributeError: - 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 = GoldairHeaterDevice._generate_dps_payload_for_properties(pending_properties) - payload = self._api.generate_payload('set', new_state) - - _LOGGER.debug(f'sending updated properties: {json.dumps(pending_properties)}') - _LOGGER.info(f'sending dps update: {json.dumps(new_state)}') - - self._retry_on_failed_connection(lambda: self._send_payload(payload), 'Failed to update device state.') - - def _send_payload(self, payload): - try: - self._lock.acquire() - self._api._send_receive(payload) - self._cached_state['updated_at'] = 0 - now = time() - pending_updates = self._get_pending_updates() - for key, value in pending_updates.items(): - pending_updates[key]['updated_at'] = now - finally: - self._lock.release() - - def _retry_on_failed_connection(self, func, error_message): - for i in range(self._CONNECTION_ATTEMPTS): - try: - func() - except: - if i + 1 == self._CONNECTION_ATTEMPTS: - self._reset_cached_state() - _LOGGER.error(error_message) - - def _get_cached_state(self): - cached_state = self._cached_state.copy() - _LOGGER.debug(f'pending updates: {json.dumps(self._get_pending_updates())}') - return {**cached_state, **self._get_pending_properties()} - - def _get_pending_properties(self): - return {key: info['value'] for key, info in self._get_pending_updates().items()} - - 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} - return self._pending_updates - - def _update_cached_state_from_dps(self, dps): - now = time() - - for key, dps_id in GOLDAIR_PROPERTY_TO_DPS_ID.items(): - if dps_id in dps: - value = dps[dps_id] - if dps_id == GOLDAIR_PROPERTY_TO_DPS_ID[ATTR_OPERATION_MODE]: - self._cached_state[key] = GoldairHeaterDevice._get_key_for_value(GOLDAIR_MODE_TO_DPS_MODE, value) - elif dps_id == GOLDAIR_PROPERTY_TO_DPS_ID[ATTR_POWER_LEVEL]: - self._cached_state[key] = GoldairHeaterDevice._get_key_for_value(GOLDAIR_POWER_LEVEL_TO_DPS_LEVEL, value) - else: - self._cached_state[key] = value - self._cached_state['updated_at'] = now - - @staticmethod - def _generate_dps_payload_for_properties(properties): - dps = {} - - for key, dps_id in GOLDAIR_PROPERTY_TO_DPS_ID.items(): - if key in properties: - value = properties[key] - if dps_id == GOLDAIR_PROPERTY_TO_DPS_ID[ATTR_OPERATION_MODE]: - dps[dps_id] = GOLDAIR_MODE_TO_DPS_MODE[value] - elif dps_id == GOLDAIR_PROPERTY_TO_DPS_ID[ATTR_POWER_LEVEL]: - dps[dps_id] = GOLDAIR_POWER_LEVEL_TO_DPS_LEVEL[value] - else: - dps[dps_id] = value - - return dps - - @staticmethod - def _get_key_for_value(obj, value): - keys = list(obj.keys()) - values = list(obj.values()) - return keys[values.index(value)] +""" +Platform for Goldair WiFi-connected heaters and panels. + +Based on sean6541/tuya-homeassistant for service call logic, and TarxBoy's +investigation into Goldair's tuyapi statuses +https://github.com/codetheweb/tuyapi/issues/31. +""" +from time import time +from threading import Timer, Lock +import logging +import json +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.const import (CONF_NAME, CONF_HOST, TEMP_CELSIUS) +from homeassistant.helpers.discovery import load_platform + +VERSION = '0.0.3' + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'goldair_climate' +DATA_GOLDAIR_CLIMATE = 'data_goldair_climate' + +CONF_DEVICE_ID = 'device_id' +CONF_LOCAL_KEY = 'local_key' +CONF_TYPE = 'type' +CONF_TYPE_HEATER = 'heater' +CONF_CLIMATE = 'climate' +CONF_SENSOR = 'sensor' +CONF_DISPLAY_LIGHT = 'display_light' +CONF_CHILD_LOCK = 'child_lock' + +PLATFORM_SCHEMA = vol.Schema({ + vol.Required(CONF_NAME): cv.string, + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_DEVICE_ID): cv.string, + vol.Required(CONF_LOCAL_KEY): cv.string, + vol.Required(CONF_TYPE): vol.In([CONF_TYPE_HEATER]), + vol.Optional(CONF_CLIMATE, default=True): cv.boolean, + vol.Optional(CONF_SENSOR, default=False): cv.boolean, + vol.Optional(CONF_DISPLAY_LIGHT, default=False): cv.boolean, + vol.Optional(CONF_CHILD_LOCK, default=False): cv.boolean, +}) + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.All(cv.ensure_list, [PLATFORM_SCHEMA]) +}, extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + hass.data[DOMAIN] = {} + for device_config in config.get(DOMAIN, []): + host = device_config.get(CONF_HOST) + + device = GoldairTuyaDevice( + device_config.get(CONF_NAME), + device_config.get(CONF_DEVICE_ID), + device_config.get(CONF_HOST), + device_config.get(CONF_LOCAL_KEY) + ) + hass.data[DOMAIN][host] = device + discovery_info = {CONF_HOST: host, CONF_TYPE: device_config.get(CONF_TYPE)} + + if device_config.get(CONF_CLIMATE) == True: + load_platform(hass, 'climate', DOMAIN, discovery_info, config) + if device_config.get(CONF_SENSOR) == True: + load_platform(hass, 'sensor', DOMAIN, discovery_info, config) + if device_config.get(CONF_DISPLAY_LIGHT) == True: + load_platform(hass, 'light', DOMAIN, discovery_info, config) + if device_config.get(CONF_CHILD_LOCK) == True: + load_platform(hass, 'lock', DOMAIN, discovery_info, config) + + return True + + +class GoldairTuyaDevice(object): + def __init__(self, name, dev_id, address, local_key): + """ + Represents a Goldair Tuya-based device. + + Args: + dev_id (str): The device id. + address (str): The network address. + local_key (str): The encryption key. + """ + import pytuya + self._name = name + self._api = pytuya.Device(dev_id, address, local_key, 'device') + + self._fixed_properties = {} + self._reset_cached_state() + + self._TEMPERATURE_UNIT = TEMP_CELSIUS + + # API calls to update Goldair heaters are asynchronous and non-blocking. This means + # you can send a change and immediately request an updated state (like HA does), + # but because it has not yet finished processing you will be returned the old state. + # The solution is to keep a temporary list of changed properties that we can overlay + # onto the state while we wait for the board to update its switches. + self._FAKE_IT_TIL_YOU_MAKE_IT_TIMEOUT = 10 + self._CACHE_TIMEOUT = 20 + self._CONNECTION_ATTEMPTS = 2 + self._lock = Lock() + + @property + def name(self): + return self._name + + @property + def temperature_unit(self): + return self._TEMPERATURE_UNIT + + def set_fixed_properties(self, fixed_properties): + self._fixed_properties = fixed_properties + set_fixed_properties = Timer(10, lambda: self._set_properties(self._fixed_properties)) + set_fixed_properties.start() + + def refresh(self): + now = time() + cached_state = self._get_cached_state() + if now - cached_state['updated_at'] >= self._CACHE_TIMEOUT: + self._cached_state['updated_at'] = time() + self._retry_on_failed_connection(lambda: self._refresh_cached_state(), 'Failed to refresh device state.') + + def get_property(self, dps_id): + cached_state = self._get_cached_state() + if dps_id in cached_state: + return cached_state[dps_id] + else: + return None + + def set_property(self, dps_id, value): + self._set_properties({dps_id: value}) + + def _reset_cached_state(self): + self._cached_state = { + 'updated_at': 0 + } + self._pending_updates = {} + + def _refresh_cached_state(self): + new_state = self._api.status() + self._cached_state = new_state['dps'] + self._cached_state['updated_at'] = time() + _LOGGER.info(f'refreshed device state: {json.dumps(new_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 + + self._add_properties_to_pending_updates(properties) + self._debounce_sending_updates() + + 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): + try: + self._debounce.cancel() + except AttributeError: + pass + self._debounce = Timer(1, self._send_pending_updates) + self._debounce.start() + + def _send_pending_updates(self): + pending_properties = self._get_pending_properties() + payload = self._api.generate_payload('set', pending_properties) + + _LOGGER.info(f'sending dps update: {json.dumps(pending_properties)}') + + self._retry_on_failed_connection(lambda: self._send_payload(payload), 'Failed to update device state.') + + def _send_payload(self, payload): + try: + self._lock.acquire() + self._api._send_receive(payload) + self._cached_state['updated_at'] = 0 + now = time() + pending_updates = self._get_pending_updates() + for key, value in pending_updates.items(): + pending_updates[key]['updated_at'] = now + finally: + self._lock.release() + + def _retry_on_failed_connection(self, func, error_message): + for i in range(self._CONNECTION_ATTEMPTS): + try: + func() + except: + if i + 1 == self._CONNECTION_ATTEMPTS: + self._reset_cached_state() + _LOGGER.error(error_message) + + def _get_cached_state(self): + cached_state = self._cached_state.copy() + _LOGGER.debug(f'pending updates: {json.dumps(self._get_pending_updates())}') + return {**cached_state, **self._get_pending_properties()} + + def _get_pending_properties(self): + return {key: info['value'] for key, info in self._get_pending_updates().items()} + + 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} + return self._pending_updates + + @staticmethod + def get_key_for_value(obj, value): + keys = list(obj.keys()) + values = list(obj.values()) + return keys[values.index(value)] diff --git a/custom_components/goldair_climate/climate.py b/custom_components/goldair_climate/climate.py index 7fe6c43..f9f32bf 100644 --- a/custom_components/goldair_climate/climate.py +++ b/custom_components/goldair_climate/climate.py @@ -1,136 +1,14 @@ -""" -Platform to control Goldair WiFi-connected heaters and panels. -""" -from homeassistant.components.climate import ( - ClimateDevice, ATTR_OPERATION_MODE, ATTR_TEMPERATURE -) -from homeassistant.components.climate.const import ( - SUPPORT_ON_OFF, SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE, SUPPORT_SWING_MODE -) -from homeassistant.const import STATE_UNAVAILABLE -import custom_components.goldair_climate as goldair_climate - -SUPPORT_FLAGS = SUPPORT_ON_OFF | SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE | SUPPORT_SWING_MODE - -def setup_platform(hass, config, add_devices, discovery_info=None): - """Set up the Goldair WiFi heater.""" - device = hass.data[goldair_climate.DOMAIN][discovery_info['host']] - if discovery_info[goldair_climate.CONF_TYPE] == goldair_climate.CONF_TYPE_HEATER: - add_devices([GoldairHeater(device)]) - - -class GoldairHeater(ClimateDevice): - """Representation of a Goldair WiFi heater.""" - - def __init__(self, device): - """Initialize the heater. - Args: - name (str): The device's name. - device (GoldairHeaterDevice): The device API instance.""" - self._device = device - - self._support_flags = SUPPORT_FLAGS - - @property - def supported_features(self): - """Return the list of supported features.""" - return self._support_flags - - @property - def should_poll(self): - """Return the polling state.""" - return True - - @property - def name(self): - """Return the name of the climate device.""" - return self._device.name - - @property - def state(self): - """Return the state of the climate device.""" - if self._device.is_on is None: - return STATE_UNAVAILABLE - else: - return super().state - - @property - def is_on(self): - """Return true if the device is on.""" - return self._device.is_on - - def turn_on(self): - """Turn on.""" - self._device.turn_on() - - def turn_off(self): - """Turn off.""" - self._device.turn_off() - - @property - def temperature_unit(self): - """Return the unit of measurement.""" - return self._device.temperature_unit - - @property - def target_temperature(self): - """Return the temperature we try to reach.""" - return self._device.target_temperature - - @property - def target_temperature_step(self): - """Return the supported step of target temperature.""" - return self._device.target_temperature_step - - @property - def min_temp(self): - """Return the minimum temperature.""" - return self._device.min_target_teperature - - @property - def max_temp(self): - """Return the maximum temperature.""" - return self._device.max_target_temperature - - def set_temperature(self, **kwargs): - """Set new target temperatures.""" - if kwargs.get(ATTR_TEMPERATURE) is not None: - self._device.set_target_temperature(kwargs.get(ATTR_TEMPERATURE)) - if kwargs.get(ATTR_OPERATION_MODE) is not None: - self._device.set_operation_mode(kwargs.get(ATTR_OPERATION_MODE)) - - @property - def current_temperature(self): - """Return the current temperature.""" - return self._device.current_temperature - - @property - def current_operation(self): - """Return current operation, ie Comfort, Eco, Anti-freeze.""" - return self._device.operation_mode - - @property - def operation_list(self): - """Return the list of available operation modes.""" - return self._device.operation_mode_list - - def set_operation_mode(self, operation_mode): - """Set new operation mode.""" - self._device.set_operation_mode(operation_mode) - - @property - def current_swing_mode(self): - """Return the fan setting.""" - return self._device.power_level - - @property - def swing_list(self): - """List of available swing modes.""" - return self._device.power_level_list - - def set_swing_mode(self, swing_mode): - """Set new target temperature.""" - self._device.set_power_level(swing_mode) - - def update(self): - self._device.refresh() +""" +Setup for different kinds of Goldair climate devices +""" +from homeassistant.const import CONF_HOST +from custom_components.goldair_climate import ( + DOMAIN, CONF_TYPE, CONF_TYPE_HEATER +) +from custom_components.goldair_climate.heater.climate import GoldairHeater + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the Goldair climate device according to its type.""" + device = hass.data[DOMAIN][discovery_info[CONF_HOST]] + if discovery_info[CONF_TYPE] == CONF_TYPE_HEATER: + add_devices([GoldairHeater(device)]) diff --git a/custom_components/goldair_climate/heater/__init__.py b/custom_components/goldair_climate/heater/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/custom_components/goldair_climate/heater/climate.py b/custom_components/goldair_climate/heater/climate.py new file mode 100644 index 0000000..64fe263 --- /dev/null +++ b/custom_components/goldair_climate/heater/climate.py @@ -0,0 +1,244 @@ +""" +Goldair WiFi Heater device. +""" +from homeassistant.const import ( + ATTR_TEMPERATURE, TEMP_CELSIUS, STATE_UNAVAILABLE +) +from homeassistant.components.climate import ( + ClimateDevice, ATTR_HVAC_MODE, ATTR_PRESET_MODE, ATTR_TEMPERATURE, HVAC_MODE_OFF, HVAC_MODE_HEAT +) +from homeassistant.components.climate.const import ( + SUPPORT_TARGET_TEMPERATURE, SUPPORT_PRESET_MODE, SUPPORT_SWING_MODE +) +from custom_components.goldair_climate import GoldairTuyaDevice + +ATTR_ON = 'on' +ATTR_TARGET_TEMPERATURE = 'target_temperature' +ATTR_CHILD_LOCK = 'child_lock' +ATTR_FAULT = 'fault' +ATTR_POWER_MODE_AUTO = 'auto' +ATTR_POWER_MODE_USER = 'user' +ATTR_POWER_LEVEL = 'power_level' +ATTR_TIMER_MINUTES = 'timer_minutes' +ATTR_TIMER_ON = 'timer_on' +ATTR_DISPLAY_ON = 'display_on' +ATTR_POWER_MODE = 'power_mode' +ATTR_ECO_TARGET_TEMPERATURE = 'eco_' + ATTR_TARGET_TEMPERATURE + +STATE_COMFORT = 'Comfort' +STATE_ECO = 'Eco' +STATE_ANTI_FREEZE = 'Anti-freeze' + +GOLDAIR_PROPERTY_TO_DPS_ID = { + ATTR_HVAC_MODE: '1', + ATTR_TARGET_TEMPERATURE: '2', + ATTR_TEMPERATURE: '3', + ATTR_PRESET_MODE: '4', + ATTR_CHILD_LOCK: '6', + ATTR_FAULT: '12', + ATTR_POWER_LEVEL: '101', + ATTR_TIMER_MINUTES: '102', + ATTR_TIMER_ON: '103', + ATTR_DISPLAY_ON: '104', + ATTR_POWER_MODE: '105', + ATTR_ECO_TARGET_TEMPERATURE: '106' +} + +GOLDAIR_MODE_TO_HVAC_MODE = { + HVAC_MODE_OFF: False, + HVAC_MODE_HEAT: True +} +GOLDAIR_MODE_TO_PRESET_MODE = { + STATE_COMFORT: 'C', + STATE_ECO: 'ECO', + STATE_ANTI_FREEZE: 'AF' +} +GOLDAIR_POWER_LEVEL_TO_DPS_LEVEL = { + 'Stop': 'stop', + '1': '1', + '2': '2', + '3': '3', + '4': '4', + '5': '5', + 'Auto': 'auto' +} +GOLDAIR_POWER_MODES = [ATTR_POWER_MODE_USER, ATTR_POWER_MODE_USER] + +SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE | SUPPORT_SWING_MODE + +class GoldairHeater(ClimateDevice): + """Representation of a Goldair WiFi heater.""" + + def __init__(self, device): + """Initialize the heater. + Args: + name (str): The device's name. + device (GoldairTuyaDevice): The device API instance.""" + self._device = device + + self._support_flags = SUPPORT_FLAGS + + self._TEMPERATURE_STEP = 1 + self._TEMPERATURE_LIMITS = { + STATE_COMFORT: { + 'min': 5, + 'max': 35 + }, + STATE_ECO: { + 'min': 5, + 'max': 21 + } + } + + @property + def supported_features(self): + """Return the list of supported features.""" + return self._support_flags + + @property + def should_poll(self): + """Return the polling state.""" + return True + + @property + def name(self): + """Return the name of the climate device.""" + return self._device.name + + @property + def state(self): + """Return the state of the climate device.""" + return self.hvac_mode + + @property + def temperature_unit(self): + """Return the unit of measurement.""" + return self._device.temperature_unit + + @property + def target_temperature(self): + """Return the temperature we try to reach.""" + if self.preset_mode == STATE_COMFORT: + return self._device.get_property(GOLDAIR_PROPERTY_TO_DPS_ID[ATTR_TARGET_TEMPERATURE]) + elif self.preset_mode == STATE_ECO: + return self._device.get_property(GOLDAIR_PROPERTY_TO_DPS_ID[ATTR_ECO_TARGET_TEMPERATURE]) + else: + return None + + @property + def target_temperature_step(self): + """Return the supported step of target temperature.""" + return self._TEMPERATURE_STEP + + @property + def min_temp(self): + """Return the minimum temperature.""" + if self.preset_mode and self.preset_mode != STATE_ANTI_FREEZE: + return self._TEMPERATURE_LIMITS[self.preset_mode]['min'] + else: + return None + + @property + def max_temp(self): + """Return the maximum temperature.""" + if self.preset_mode and self.preset_mode != STATE_ANTI_FREEZE: + return self._TEMPERATURE_LIMITS[self.preset_mode]['max'] + else: + return None + + def set_temperature(self, **kwargs): + """Set new target temperatures.""" + if kwargs.get(ATTR_PRESET_MODE) is not None: + self.set_preset_mode(kwargs.get(ATTR_PRESET_MODE)) + if kwargs.get(ATTR_TEMPERATURE) is not None: + self.set_target_temperature(kwargs.get(ATTR_TEMPERATURE)) + + def set_target_temperature(self, target_temperature): + target_temperature = int(round(target_temperature)) + preset_mode = self.preset_mode + + if preset_mode == STATE_ANTI_FREEZE: + raise ValueError('You cannot set the temperature in Anti-freeze mode.') + + limits = self._TEMPERATURE_LIMITS[preset_mode] + if not limits['min'] <= target_temperature <= limits['max']: + raise ValueError( + f'Target temperature ({target_temperature}) must be between ' + f'{limits["min"]} and {limits["max"]}' + ) + + if preset_mode == STATE_COMFORT: + self._device.set_property(GOLDAIR_PROPERTY_TO_DPS_ID[ATTR_TARGET_TEMPERATURE], target_temperature) + elif preset_mode == STATE_ECO: + self._device.set_property(GOLDAIR_PROPERTY_TO_DPS_ID[ATTR_ECO_TARGET_TEMPERATURE], target_temperature) + + @property + def current_temperature(self): + """Return the current temperature.""" + return self._device.get_property(GOLDAIR_PROPERTY_TO_DPS_ID[ATTR_TEMPERATURE]) + + @property + def hvac_mode(self): + """Return current HVAC mode, ie Heat or Off.""" + dps_mode = self._device.get_property(GOLDAIR_PROPERTY_TO_DPS_ID[ATTR_HVAC_MODE]) + + if dps_mode is not None: + return GoldairTuyaDevice.get_key_for_value(GOLDAIR_MODE_TO_HVAC_MODE, dps_mode) + else: + return STATE_UNAVAILABLE + + @property + def hvac_modes(self): + """Return the list of available HVAC modes.""" + return list(GOLDAIR_MODE_TO_HVAC_MODE.keys()) + + def set_hvac_mode(self, hvac_mode): + """Set new HVAC mode.""" + dps_mode = GOLDAIR_MODE_TO_HVAC_MODE[hvac_mode] + self._device.set_property(GOLDAIR_PROPERTY_TO_DPS_ID[ATTR_HVAC_MODE], dps_mode) + + @property + def preset_mode(self): + """Return current preset mode, ie Comfort, Eco, Anti-freeze.""" + dps_mode = self._device.get_property(GOLDAIR_PROPERTY_TO_DPS_ID[ATTR_PRESET_MODE]) + if dps_mode is not None: + return GoldairTuyaDevice.get_key_for_value(GOLDAIR_MODE_TO_PRESET_MODE, dps_mode) + else: + return None + + @property + def preset_modes(self): + """Return the list of available preset modes.""" + return list(GOLDAIR_MODE_TO_PRESET_MODE.keys()) + + def set_preset_mode(self, preset_mode): + """Set new preset mode.""" + dps_mode = GOLDAIR_MODE_TO_PRESET_MODE[preset_mode] + self._device.set_property(GOLDAIR_PROPERTY_TO_DPS_ID[ATTR_PRESET_MODE], dps_mode) + + @property + def swing_mode(self): + """Return the power level.""" + dps_mode = self._device.get_property(GOLDAIR_PROPERTY_TO_DPS_ID[ATTR_POWER_MODE]) + if dps_mode == ATTR_POWER_MODE_USER: + return self._device.get_property(GOLDAIR_PROPERTY_TO_DPS_ID[ATTR_POWER_LEVEL]) + elif dps_mode == ATTR_POWER_MODE_AUTO: + return GoldairTuyaDevice.get_key_for_value(GOLDAIR_POWER_LEVEL_TO_DPS_LEVEL, dps_mode) + else: + return None + + @property + def swing_modes(self): + """List of power levels.""" + return list(GOLDAIR_POWER_LEVEL_TO_DPS_LEVEL.keys()) + + def set_swing_mode(self, swing_mode): + """Set new power level.""" + new_level = swing_mode + if new_level not in GOLDAIR_POWER_LEVEL_TO_DPS_LEVEL.keys(): + raise ValueError(f'Invalid power level: {new_level}') + dps_level = GOLDAIR_POWER_LEVEL_TO_DPS_LEVEL[new_level] + self._device.set_property(GOLDAIR_PROPERTY_TO_DPS_ID[ATTR_POWER_LEVEL], dps_level) + + def update(self): + self._device.refresh() diff --git a/custom_components/goldair_climate/heater/light.py b/custom_components/goldair_climate/heater/light.py new file mode 100644 index 0000000..3e49271 --- /dev/null +++ b/custom_components/goldair_climate/heater/light.py @@ -0,0 +1,58 @@ +""" +Platform to control the LED display light on Goldair WiFi-connected heaters and panels. +""" +from homeassistant.components.light import Light +from homeassistant.const import STATE_UNAVAILABLE +from custom_components.goldair_climate import GoldairTuyaDevice +from custom_components.goldair_climate.heater.climate import ( + ATTR_DISPLAY_ON, GOLDAIR_PROPERTY_TO_DPS_ID, GOLDAIR_MODE_TO_HVAC_MODE +) +from homeassistant.components.climate import ( + ATTR_HVAC_MODE, HVAC_MODE_OFF +) + +import logging +_LOGGER = logging.getLogger(__name__) + +class GoldairHeaterLedDisplayLight(Light): + """Representation of a Goldair WiFi-connected heater LED display.""" + + def __init__(self, device): + """Initialize the light. + Args: + device (GoldairTuyaDevice): The device API instance.""" + self._device = device + + @property + def should_poll(self): + """Return the polling state.""" + return True + + @property + def name(self): + """Return the name of the light.""" + return self._device.name + + @property + def is_on(self): + """Return the current state.""" + dps_hvac_mode = self._device.get_property(GOLDAIR_PROPERTY_TO_DPS_ID[ATTR_HVAC_MODE]) + dps_display_on = self._device.get_property(GOLDAIR_PROPERTY_TO_DPS_ID[ATTR_DISPLAY_ON]) + + if dps_hvac_mode is None or dps_hvac_mode == GOLDAIR_MODE_TO_HVAC_MODE[HVAC_MODE_OFF]: + return STATE_UNAVAILABLE + else: + return dps_display_on + + def turn_on(self): + self._device.set_property(GOLDAIR_PROPERTY_TO_DPS_ID[ATTR_DISPLAY_ON], True) + + def turn_off(self): + self._device.set_property(GOLDAIR_PROPERTY_TO_DPS_ID[ATTR_DISPLAY_ON], False) + + def toggle(self): + dps_hvac_mode = self._device.get_property(GOLDAIR_PROPERTY_TO_DPS_ID[ATTR_HVAC_MODE]) + dps_display_on = self._device.get_property(GOLDAIR_PROPERTY_TO_DPS_ID[ATTR_DISPLAY_ON]) + + if dps_hvac_mode != GOLDAIR_MODE_TO_HVAC_MODE[HVAC_MODE_OFF]: + self.turn_on() if not dps_display_on else self.turn_off() diff --git a/custom_components/goldair_climate/heater/lock.py b/custom_components/goldair_climate/heater/lock.py new file mode 100755 index 0000000..49141b0 --- /dev/null +++ b/custom_components/goldair_climate/heater/lock.py @@ -0,0 +1,49 @@ +""" +Platform to control the child lock on Goldair WiFi-connected heaters and panels. +""" +from homeassistant.components.lock import (STATE_LOCKED, STATE_UNLOCKED, LockDevice) +from homeassistant.const import STATE_UNAVAILABLE +from custom_components.goldair_climate import GoldairTuyaDevice +from custom_components.goldair_climate.heater.climate import ( + ATTR_CHILD_LOCK, GOLDAIR_PROPERTY_TO_DPS_ID, GOLDAIR_MODE_TO_HVAC_MODE +) + +class GoldairHeaterChildLock(LockDevice): + """Representation of a Goldair WiFi-connected heater child lock.""" + + def __init__(self, device): + """Initialize the lock. + Args: + device (GoldairTuyaDevice): The device API instance.""" + self._device = device + + @property + def should_poll(self): + """Return the polling state.""" + return True + + @property + def name(self): + """Return the name of the lock.""" + return self._device.name + + @property + def state(self): + """Return the current state.""" + if self.is_locked is None: + return STATE_UNAVAILABLE + else: + return STATE_LOCKED if self.is_locked else STATE_UNLOCKED + + @property + def is_locked(self): + """Return the a boolean representing whether the child lock is on or not.""" + return self._device.get_property(GOLDAIR_PROPERTY_TO_DPS_ID[ATTR_CHILD_LOCK]) + + def lock(self, **kwargs): + """Turn on the child lock.""" + self._device.set_property(GOLDAIR_PROPERTY_TO_DPS_ID[ATTR_CHILD_LOCK], True) + + def unlock(self, **kwargs): + """Turn off the child lock.""" + self._device.set_property(GOLDAIR_PROPERTY_TO_DPS_ID[ATTR_CHILD_LOCK], False) diff --git a/custom_components/goldair_climate/heater/sensor.py b/custom_components/goldair_climate/heater/sensor.py new file mode 100755 index 0000000..8ee3a6c --- /dev/null +++ b/custom_components/goldair_climate/heater/sensor.py @@ -0,0 +1,41 @@ +""" +Platform to sense the current temperature at a Goldair WiFi-connected heaters and panels. +""" +from homeassistant.helpers.entity import Entity +from homeassistant.const import ( + STATE_UNAVAILABLE, ATTR_TEMPERATURE +) +from custom_components.goldair_climate import GoldairTuyaDevice +from custom_components.goldair_climate.heater.climate import GOLDAIR_PROPERTY_TO_DPS_ID + +class GoldairHeaterTemperatureSensor(Entity): + """Representation of a Goldair WiFi-connected heater thermometer.""" + + def __init__(self, device): + """Initialize the lock. + Args: + device (GoldairTuyaDevice): The device API instance.""" + self._device = device + + @property + def should_poll(self): + """Return the polling state.""" + return True + + @property + def name(self): + """Return the name of the sensor.""" + return self._device.name + + @property + def state(self): + """Return the current temperature.""" + current_temperature = self._device.get_property(GOLDAIR_PROPERTY_TO_DPS_ID[ATTR_TEMPERATURE]) + if current_temperature is None: + return STATE_UNAVAILABLE + else: + return current_temperature + + @property + def unit_of_measurement(self): + return self._device.temperature_unit diff --git a/custom_components/goldair_climate/light.py b/custom_components/goldair_climate/light.py index 8d9992c..985aaeb 100644 --- a/custom_components/goldair_climate/light.py +++ b/custom_components/goldair_climate/light.py @@ -1,52 +1,14 @@ -""" -Platform to control the LED display light on Goldair WiFi-connected heaters and panels. -""" -from homeassistant.components.light import Light -from homeassistant.const import STATE_UNAVAILABLE -import custom_components.goldair_climate as goldair_climate - - -def setup_platform(hass, config, add_devices, discovery_info=None): - device = hass.data[goldair_climate.DOMAIN][discovery_info['host']] - add_devices([ - GoldairLedDisplayLight(device) - ]) - - -class GoldairLedDisplayLight(Light): - """Representation of a Goldair WiFi-connected heater LED display.""" - - def __init__(self, device): - """Initialize the light. - Args: - device (GoldairHeaterDevice): The device API instance.""" - self._device = device - - @property - def should_poll(self): - """Return the polling state.""" - return True - - @property - def name(self): - """Return the name of the light.""" - return self._device.name - - @property - def is_on(self): - """Return the current state.""" - if self._device.is_on is None: - return STATE_UNAVAILABLE - else: - return self._device.is_on and self._device.is_display_on - - def turn_on(self): - """Turn on the LED display.""" - self._device.turn_display_on() - - def turn_off(self): - """Turn off the LED display.""" - self._device.turn_display_off() - - def toggle(self): - self._device.turn_display_on() if not self._device.is_display_on else self._device.turn_display_off() +""" +Setup for different kinds of Goldair climate devices +""" +from homeassistant.const import CONF_HOST +from custom_components.goldair_climate import ( + DOMAIN, CONF_TYPE, CONF_TYPE_HEATER +) +from custom_components.goldair_climate.heater.light import GoldairHeaterLedDisplayLight + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the Goldair climate device according to its type.""" + device = hass.data[DOMAIN][discovery_info[CONF_HOST]] + if discovery_info[CONF_TYPE] == CONF_TYPE_HEATER: + add_devices([GoldairHeaterLedDisplayLight(device)]) diff --git a/custom_components/goldair_climate/lock.py b/custom_components/goldair_climate/lock.py old mode 100644 new mode 100755 index 9f80ce5..9f2dfb7 --- a/custom_components/goldair_climate/lock.py +++ b/custom_components/goldair_climate/lock.py @@ -1,54 +1,14 @@ -""" -Platform to control the child lock on Goldair WiFi-connected heaters and panels. -""" -from homeassistant.components.lock import (STATE_LOCKED, STATE_UNLOCKED, LockDevice) -from homeassistant.const import STATE_UNAVAILABLE -import custom_components.goldair_climate as goldair_climate - - -def setup_platform(hass, config, add_devices, discovery_info=None): - device = hass.data[goldair_climate.DOMAIN][discovery_info['host']] - add_devices([ - GoldairChildLock(device) - ]) - - -class GoldairChildLock(LockDevice): - """Representation of a Goldair WiFi-connected heater child lock.""" - - def __init__(self, device): - """Initialize the lock. - Args: - device (GoldairHeaterDevice): The device API instance.""" - self._device = device - - @property - def should_poll(self): - """Return the polling state.""" - return True - - @property - def name(self): - """Return the name of the lock.""" - return self._device.name - - @property - def state(self): - """Return the current state.""" - if self.is_locked is None: - return STATE_UNAVAILABLE - else: - return STATE_LOCKED if self.is_locked else STATE_UNLOCKED - - @property - def is_locked(self): - """Return the current state.""" - return self._device.is_child_locked - - def lock(self, code): - """Turn on the LED display.""" - self._device.enable_child_lock() - - def unlock(self, code): - """Turn off the LED display.""" - self._device.disable_child_lock() +""" +Setup for different kinds of Goldair climate devices +""" +from homeassistant.const import CONF_HOST +from custom_components.goldair_climate import ( + DOMAIN, CONF_TYPE, CONF_TYPE_HEATER +) +from custom_components.goldair_climate.heater.lock import GoldairHeaterChildLock + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the Goldair climate device according to its type.""" + device = hass.data[DOMAIN][discovery_info[CONF_HOST]] + if discovery_info[CONF_TYPE] == CONF_TYPE_HEATER: + add_devices([GoldairHeaterChildLock(device)]) diff --git a/custom_components/goldair_climate/manifest.json b/custom_components/goldair_climate/manifest.json new file mode 100644 index 0000000..9195967 --- /dev/null +++ b/custom_components/goldair_climate/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "goldair_climate", + "name": "goldair_climate", + "documentation": "https://github.com/nikrolls/homeassistant-goldair-climate", + "dependencies": [], + "codeowners": ["@nikrolls"], + "requirements": ["pytuya>=7.0.5"], + "homeassistant": "0.96.0" +} diff --git a/custom_components/goldair_climate/sensor.py b/custom_components/goldair_climate/sensor.py old mode 100644 new mode 100755 index a954a3f..7968bb8 --- a/custom_components/goldair_climate/sensor.py +++ b/custom_components/goldair_climate/sensor.py @@ -1,45 +1,14 @@ -""" -Platform to sense the current temperature at a Goldair WiFi-connected heaters and panels. -""" -from homeassistant.helpers.entity import Entity -from homeassistant.const import STATE_UNAVAILABLE -import custom_components.goldair_climate as goldair_climate - - -def setup_platform(hass, config, add_devices, discovery_info=None): - device = hass.data[goldair_climate.DOMAIN][discovery_info['host']] - add_devices([ - GoldairTemperatureSensor(device) - ]) - - -class GoldairTemperatureSensor(Entity): - """Representation of a Goldair WiFi-connected heater thermometer.""" - - def __init__(self, device): - """Initialize the lock. - Args: - device (GoldairHeaterDevice): The device API instance.""" - self._device = device - - @property - def should_poll(self): - """Return the polling state.""" - return True - - @property - def name(self): - """Return the name of the sensor.""" - return self._device.name - - @property - def state(self): - """Return the current state.""" - if self._device.current_temperature is None: - return STATE_UNAVAILABLE - else: - return self._device.current_temperature - - @property - def unit_of_measurement(self): - return self._device.temperature_unit +""" +Setup for different kinds of Goldair climate devices +""" +from homeassistant.const import CONF_HOST +from custom_components.goldair_climate import ( + DOMAIN, CONF_TYPE, CONF_TYPE_HEATER +) +from custom_components.goldair_climate.heater.sensor import GoldairHeaterTemperatureSensor + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the Goldair climate device according to its type.""" + device = hass.data[DOMAIN][discovery_info[CONF_HOST]] + if discovery_info[CONF_TYPE] == CONF_TYPE_HEATER: + add_devices([GoldairHeaterTemperatureSensor(device)]) diff --git a/custom_updater.json b/custom_updater.json index abe74f6..3c04083 100644 --- a/custom_updater.json +++ b/custom_updater.json @@ -1,15 +1,22 @@ -{ - "goldair_climate": { - "version": "0.0.3", - "local_location": "/custom_components/goldair_climate/__init__.py", - "remote_location": "https://raw.githubusercontent.com/nikrolls/homeassistant-goldair-climate/master/custom_components/goldair_climate/__init__.py", - "visit_repo": "https://github.com/nikrolls/homeassistant-goldair-climate", - "changelog": "https://github.com/nikrolls/homeassistant-goldair-climate/releases/latest", - "resources": [ - "https://raw.githubusercontent.com/nikrolls/homeassistant-goldair-climate/master/custom_components/goldair_climate/climate.py", - "https://raw.githubusercontent.com/nikrolls/homeassistant-goldair-climate/master/custom_components/goldair_climate/light.py", - "https://raw.githubusercontent.com/nikrolls/homeassistant-goldair-climate/master/custom_components/goldair_climate/lock.py", - "https://raw.githubusercontent.com/nikrolls/homeassistant-goldair-climate/master/custom_components/goldair_climate/sensor.py" - ] - } -} +{ + "goldair_climate": { + "version": "0.0.4", + "local_location": "/custom_components/goldair_climate/__init__.py", + "remote_location": "https://raw.githubusercontent.com/nikrolls/homeassistant-goldair-climate/master/custom_components/goldair_climate/__init__.py", + "visit_repo": "https://github.com/nikrolls/homeassistant-goldair-climate", + "changelog": "https://github.com/nikrolls/homeassistant-goldair-climate/releases/latest", + "resources": [ + "https://raw.githubusercontent.com/nikrolls/homeassistant-goldair-climate/master/custom_components/goldair_climate/manifest.json", + "https://raw.githubusercontent.com/nikrolls/homeassistant-goldair-climate/master/custom_components/goldair_climate/__init__.py", + "https://raw.githubusercontent.com/nikrolls/homeassistant-goldair-climate/master/custom_components/goldair_climate/climate.py", + "https://raw.githubusercontent.com/nikrolls/homeassistant-goldair-climate/master/custom_components/goldair_climate/light.py", + "https://raw.githubusercontent.com/nikrolls/homeassistant-goldair-climate/master/custom_components/goldair_climate/lock.py", + "https://raw.githubusercontent.com/nikrolls/homeassistant-goldair-climate/master/custom_components/goldair_climate/sensor.py", + "https://raw.githubusercontent.com/nikrolls/homeassistant-goldair-climate/master/custom_components/goldair_climate/heater/__init__.py", + "https://raw.githubusercontent.com/nikrolls/homeassistant-goldair-climate/master/custom_components/goldair_climate/heater/climate.py", + "https://raw.githubusercontent.com/nikrolls/homeassistant-goldair-climate/master/custom_components/goldair_climate/heater/light.py", + "https://raw.githubusercontent.com/nikrolls/homeassistant-goldair-climate/master/custom_components/goldair_climate/heater/lock.py", + "https://raw.githubusercontent.com/nikrolls/homeassistant-goldair-climate/master/custom_components/goldair_climate/heater/sensor.py" + ] + } +}