diff --git a/homeassistant/components/binary_sensor/wink.py b/homeassistant/components/binary_sensor/wink.py index 9813ca213e6..e4448d96e36 100644 --- a/homeassistant/components/binary_sensor/wink.py +++ b/homeassistant/components/binary_sensor/wink.py @@ -5,6 +5,7 @@ For more details about this platform, please refer to the documentation at at https://home-assistant.io/components/binary_sensor.wink/ """ import json +import logging from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.components.sensor.wink import WinkDevice @@ -53,12 +54,17 @@ class WinkBinarySensorDevice(WinkDevice, BinarySensorDevice, Entity): self.capability = self.wink.capability() def _pubnub_update(self, message, channel): - if 'data' in message: - json_data = json.dumps(message.get('data')) - else: - json_data = message - self.wink.pubnub_update(json.loads(json_data)) - self.update_ha_state() + try: + if 'data' in message: + json_data = json.dumps(message.get('data')) + else: + json_data = message + self.wink.pubnub_update(json.loads(json_data)) + self.update_ha_state() + except (AttributeError, KeyError): + error = "Pubnub returned invalid json for " + self.name + logging.getLogger(__name__).error(error) + self.update_ha_state(True) @property def is_on(self): diff --git a/homeassistant/components/climate/wink.py b/homeassistant/components/climate/wink.py new file mode 100644 index 00000000000..a0094a7c290 --- /dev/null +++ b/homeassistant/components/climate/wink.py @@ -0,0 +1,331 @@ +""" +Support for Wink thermostats. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/climate.wink/ +""" +from homeassistant.components.wink import WinkDevice +from homeassistant.components.climate import ( + STATE_AUTO, STATE_COOL, STATE_HEAT, ClimateDevice, + ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, + ATTR_TEMPERATURE, + ATTR_CURRENT_HUMIDITY) +from homeassistant.const import ( + TEMP_CELSIUS, STATE_ON, + STATE_OFF, STATE_UNKNOWN) +from homeassistant.loader import get_component + +DEPENDENCIES = ['wink'] + +STATE_AUX = 'aux' +STATE_ECO = 'eco' + +ATTR_EXTERNAL_TEMPERATURE = "external_temperature" +ATTR_SMART_TEMPERATURE = "smart_temperature" +ATTR_ECO_TARGET = "eco_target" +ATTR_OCCUPIED = "occupied" + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the Wink thermostat.""" + import pywink + temp_unit = hass.config.units.temperature_unit + add_devices(WinkThermostat(thermostat, temp_unit) + for thermostat in pywink.get_thermostats()) + + +# pylint: disable=abstract-method,too-many-public-methods, too-many-branches +class WinkThermostat(WinkDevice, ClimateDevice): + """Representation of a Wink thermostat.""" + + def __init__(self, wink, temp_unit): + """Initialize the Wink device.""" + super().__init__(wink) + wink = get_component('wink') + self._config_temp_unit = temp_unit + + @property + def temperature_unit(self): + """Return the unit of measurement.""" + # The Wink API always returns temp in Celsius + return TEMP_CELSIUS + + @property + def device_state_attributes(self): + """Return the optional state attributes.""" + data = {} + target_temp_high = self.target_temperature_high + target_temp_low = self.target_temperature_low + if target_temp_high is not None: + data[ATTR_TARGET_TEMP_HIGH] = self._convert_for_display( + self.target_temperature_high) + if target_temp_low is not None: + data[ATTR_TARGET_TEMP_LOW] = self._convert_for_display( + self.target_temperature_low) + + if self.external_temperature: + data[ATTR_EXTERNAL_TEMPERATURE] = self._convert_for_display( + self.external_temperature) + + if self.smart_temperature: + data[ATTR_SMART_TEMPERATURE] = self.smart_temperature + + if self.occupied: + data[ATTR_OCCUPIED] = self.occupied + + if self.eco_target: + data[ATTR_ECO_TARGET] = self.eco_target + + current_humidity = self.current_humidity + if current_humidity is not None: + data[ATTR_CURRENT_HUMIDITY] = current_humidity + + return data + + @property + def current_temperature(self): + """Return the current temperature.""" + return self.wink.current_temperature() + + @property + def current_humidity(self): + """Return the current humidity.""" + if self.wink.current_humidity() is not None: + # The API states humidity will be a float 0-1 + # the only example API response with humidity listed show an int + # This will address both possibilities + if self.wink.current_humidity() < 1: + return self.wink.current_humidity() * 100 + else: + return self.wink.current_humidity() + + @property + def external_temperature(self): + """Return the current external temperature.""" + return self.wink.current_external_temperature() + + @property + def smart_temperature(self): + """Return the current average temp of all remote sensor.""" + return self.wink.current_smart_temperature() + + @property + def eco_target(self): + """Return status of eco target (Is the termostat in eco mode).""" + return self.wink.eco_target() + + @property + def occupied(self): + """Return status of if the thermostat has detected occupancy.""" + return self.wink.occupied() + + @property + def current_operation(self): + """Return current operation ie. heat, cool, idle.""" + if not self.wink.is_on(): + current_op = STATE_OFF + elif self.wink.current_hvac_mode() == 'cool_only': + current_op = STATE_COOL + elif self.wink.current_hvac_mode() == 'heat_only': + current_op = STATE_HEAT + elif self.wink.current_hvac_mode() == 'aux': + current_op = STATE_HEAT + elif self.wink.current_hvac_mode() == 'auto': + current_op = STATE_AUTO + elif self.wink.current_hvac_mode() == 'eco': + current_op = STATE_ECO + else: + current_op = STATE_UNKNOWN + return current_op + + @property + def target_humidity(self): + """Return the humidity we try to reach.""" + target_hum = None + if self.wink.current_humidifier_mode() == 'on': + if self.wink.current_humidifier_set_point() is not None: + target_hum = self.wink.current_humidifier_set_point() * 100 + elif self.wink.current_dehumidifier_mode() == 'on': + if self.wink.current_dehumidifier_set_point() is not None: + target_hum = self.wink.current_dehumidifier_set_point() * 100 + else: + target_hum = None + return target_hum + + @property + def target_temperature(self): + """Return the temperature we try to reach.""" + if self.current_operation != STATE_AUTO and not self.is_away_mode_on: + if self.current_operation == STATE_COOL: + return self.wink.current_max_set_point() + elif self.current_operation == STATE_HEAT: + return self.wink.current_min_set_point() + else: + return None + else: + return None + + @property + def target_temperature_low(self): + """Return the lower bound temperature we try to reach.""" + if self.current_operation == STATE_AUTO: + return self.wink.current_min_set_point() + return None + + @property + def target_temperature_high(self): + """Return the higher bound temperature we try to reach.""" + if self.current_operation == STATE_AUTO: + return self.wink.current_max_set_point() + return None + + @property + def is_away_mode_on(self): + """Return if away mode is on.""" + return self.wink.away() + + @property + def is_aux_heat_on(self): + """Return true if aux heater.""" + if self.wink.current_hvac_mode() == 'aux' and self.wink.is_on(): + return True + elif self.wink.current_hvac_mode() == 'aux' and not self.wink.is_on(): + return False + else: + return None + + def set_temperature(self, **kwargs): + """Set new target temperature.""" + target_temp = kwargs.get(ATTR_TEMPERATURE) + target_temp_low = kwargs.get(ATTR_TARGET_TEMP_LOW) + target_temp_high = kwargs.get(ATTR_TARGET_TEMP_HIGH) + if target_temp is not None: + if self.current_operation == STATE_COOL: + target_temp_high = target_temp + if self.current_operation == STATE_HEAT: + target_temp_low = target_temp + if target_temp_low is not None: + target_temp_low = target_temp_low + if target_temp_high is not None: + target_temp_high = target_temp_high + self.wink.set_temperature(target_temp_low, target_temp_high) + + def set_operation_mode(self, operation_mode): + """Set operation mode.""" + if operation_mode == STATE_HEAT: + self.wink.set_operation_mode('heat_only') + elif operation_mode == STATE_COOL: + self.wink.set_operation_mode('cool_only') + elif operation_mode == STATE_AUTO: + self.wink.set_operation_mode('auto') + elif operation_mode == STATE_OFF: + self.wink.set_operation_mode('off') + elif operation_mode == STATE_AUX: + self.wink.set_operation_mode('aux') + elif operation_mode == STATE_ECO: + self.wink.set_operation_mode('eco') + + @property + def operation_list(self): + """List of available operation modes.""" + op_list = ['off'] + modes = self.wink.hvac_modes() + if 'cool_only' in modes: + op_list.append(STATE_COOL) + if 'heat_only' in modes or 'aux' in modes: + op_list.append(STATE_HEAT) + if 'auto' in modes: + op_list.append(STATE_AUTO) + if 'eco' in modes: + op_list.append(STATE_ECO) + return op_list + + def turn_away_mode_on(self): + """Turn away on.""" + self.wink.set_away_mode() + + def turn_away_mode_off(self): + """Turn away off.""" + self.wink.set_away_mode(False) + + @property + def current_fan_mode(self): + """Return whether the fan is on.""" + if self.wink.current_fan_mode() == 'on': + return STATE_ON + elif self.wink.current_fan_mode() == 'auto': + return STATE_AUTO + else: + # No Fan available so disable slider + return None + + @property + def fan_list(self): + """List of available fan modes.""" + if self.wink.has_fan(): + return self.wink.fan_modes() + return None + + def set_fan_mode(self, fan): + """Turn fan on/off.""" + self.wink.set_fan_mode(fan.lower()) + + def turn_aux_heat_on(self): + """Turn auxillary heater on.""" + self.set_operation_mode(STATE_AUX) + + def turn_aux_heat_off(self): + """Turn auxillary heater off.""" + self.set_operation_mode(STATE_AUTO) + + @property + def min_temp(self): + """Return the minimum temperature.""" + minimum = 7 # Default minimum + min_min = self.wink.min_min_set_point() + min_max = self.wink.min_max_set_point() + return_value = minimum + if self.current_operation == STATE_HEAT: + if min_min: + return_value = min_min + else: + return_value = minimum + elif self.current_operation == STATE_COOL: + if min_max: + return_value = min_max + else: + return_value = minimum + elif self.current_operation == STATE_AUTO: + if min_min and min_max: + return_value = min(min_min, min_max) + else: + return_value = minimum + else: + return_value = minimum + return return_value + + @property + def max_temp(self): + """Return the maximum temperature.""" + maximum = 35 # Default maximum + max_min = self.wink.max_min_set_point() + max_max = self.wink.max_max_set_point() + return_value = maximum + if self.current_operation == STATE_HEAT: + if max_min: + return_value = max_min + else: + return_value = maximum + elif self.current_operation == STATE_COOL: + if max_max: + return_value = max_max + else: + return_value = maximum + elif self.current_operation == STATE_AUTO: + if max_min and max_max: + return_value = min(max_min, max_max) + else: + return_value = maximum + else: + return_value = maximum + return return_value diff --git a/homeassistant/components/light/wink.py b/homeassistant/components/light/wink.py index d117b66df79..1d292a53419 100644 --- a/homeassistant/components/light/wink.py +++ b/homeassistant/components/light/wink.py @@ -41,7 +41,10 @@ class WinkLight(WinkDevice, Light): @property def brightness(self): """Return the brightness of the light.""" - return int(self.wink.brightness() * 255) + if self.wink.brightness() is not None: + return int(self.wink.brightness() * 255) + else: + return None @property def rgb_color(self): @@ -52,6 +55,8 @@ class WinkLight(WinkDevice, Light): hue = self.wink.color_hue() saturation = self.wink.color_saturation() value = int(self.wink.brightness() * 255) + if hue is None or saturation is None or value is None: + return None rgb = colorsys.hsv_to_rgb(hue, saturation, value) r_value = int(round(rgb[0])) g_value = int(round(rgb[1])) diff --git a/homeassistant/components/sensor/wink.py b/homeassistant/components/sensor/wink.py index 569beba4866..455b3b03290 100644 --- a/homeassistant/components/sensor/wink.py +++ b/homeassistant/components/sensor/wink.py @@ -6,8 +6,7 @@ at https://home-assistant.io/components/sensor.wink/ """ import logging -from homeassistant.const import ( - STATE_CLOSED, STATE_OPEN, TEMP_CELSIUS) +from homeassistant.const import TEMP_CELSIUS from homeassistant.helpers.entity import Entity from homeassistant.components.wink import WinkDevice from homeassistant.loader import get_component @@ -51,27 +50,29 @@ class WinkSensorDevice(WinkDevice, Entity): @property def state(self): """Return the state.""" + state = None if self.capability == 'humidity': - return round(self.wink.humidity_percentage()) + if self.wink.humidity_percentage() is not None: + state = round(self.wink.humidity_percentage()) elif self.capability == 'temperature': - return round(self.wink.temperature_float(), 1) + if self.wink.temperature_float() is not None: + state = round(self.wink.temperature_float(), 1) elif self.capability == 'balance': - return round(self.wink.balance() / 100, 2) + if self.wink.balance() is not None: + state = round(self.wink.balance() / 100, 2) elif self.capability == 'proximity': - return self.wink.proximity_float() + if self.wink.proximity_float() is not None: + state = self.wink.proximity_float() else: - return STATE_OPEN if self.is_open else STATE_CLOSED + # A sensor should never get here, anything that does + # will require an update to python-wink + logging.getLogger(__name__).error("Please report this as an issue") + state = None + return state @property def available(self): - """ - True if connection == True. - - Always return true for Wink porkfolio due to - bug in API. - """ - if self.capability == 'balance': - return True + """True if connection == True.""" return self.wink.available @property @@ -79,11 +80,6 @@ class WinkSensorDevice(WinkDevice, Entity): """Return the unit of measurement of this entity, if any.""" return self._unit_of_measurement - @property - def is_open(self): - """Return true if door is open.""" - return self.wink.state() - class WinkEggMinder(WinkDevice, Entity): """Representation of a Wink Egg Minder.""" diff --git a/homeassistant/components/wink.py b/homeassistant/components/wink.py index 22c6c992838..c678024f6a3 100644 --- a/homeassistant/components/wink.py +++ b/homeassistant/components/wink.py @@ -15,7 +15,7 @@ from homeassistant.const import CONF_ACCESS_TOKEN, ATTR_BATTERY_LEVEL, \ from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['python-wink==0.9.0', 'pubnub==3.8.2'] +REQUIREMENTS = ['python-wink==0.10.0', 'pubnub==3.8.2'] _LOGGER = logging.getLogger(__name__) @@ -50,7 +50,7 @@ CONFIG_SCHEMA = vol.Schema({ }, extra=vol.ALLOW_EXTRA) WINK_COMPONENTS = [ - 'binary_sensor', 'sensor', 'light', 'switch', 'lock', 'cover' + 'binary_sensor', 'sensor', 'light', 'switch', 'lock', 'cover', 'climate' ] @@ -108,8 +108,13 @@ class WinkDevice(Entity): error=self._pubnub_error) def _pubnub_update(self, message, channel): - self.wink.pubnub_update(json.loads(message)) - self.update_ha_state() + try: + self.wink.pubnub_update(json.loads(message)) + self.update_ha_state() + except (AttributeError, KeyError): + error = "Pubnub returned invalid json for " + self.name + logging.getLogger(__name__).error(error) + self.update_ha_state(True) def _pubnub_error(self, message): _LOGGER.error("Error on pubnub update for " + self.wink.name()) @@ -149,4 +154,5 @@ class WinkDevice(Entity): @property def _battery_level(self): """Return the battery level.""" - return self.wink.battery_level * 100 + if self.wink.battery_level is not None: + return self.wink.battery_level * 100 diff --git a/requirements_all.txt b/requirements_all.txt index 01b297c8c03..21573c114a3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -443,7 +443,7 @@ python-telegram-bot==5.2.0 python-twitch==1.3.0 # homeassistant.components.wink -python-wink==0.9.0 +python-wink==0.10.0 # homeassistant.components.keyboard # pyuserinput==0.1.11