From 312de6b3a369bdc58dda94b2a78167ef38821cee Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Wed, 27 Sep 2017 02:17:55 -0400 Subject: [PATCH] New Wink services. pair new device, rename, and delete, add new lock key code. Add water heater support (#9303) * Pair new device, rename, delete, and lock key code services. Also add water heater support. * Fixed tox --- .../components/binary_sensor/wink.py | 5 +- homeassistant/components/climate/__init__.py | 6 + homeassistant/components/climate/demo.py | 4 +- homeassistant/components/climate/wink.py | 251 +++++++++++------- homeassistant/components/lock/services.yaml | 23 +- homeassistant/components/lock/wink.py | 35 ++- homeassistant/components/services.yaml | 52 +++- homeassistant/components/wink.py | 109 +++++++- requirements_all.txt | 2 +- 9 files changed, 363 insertions(+), 124 deletions(-) diff --git a/homeassistant/components/binary_sensor/wink.py b/homeassistant/components/binary_sensor/wink.py index b4910687da7..05de0b51aa8 100644 --- a/homeassistant/components/binary_sensor/wink.py +++ b/homeassistant/components/binary_sensor/wink.py @@ -136,8 +136,9 @@ class WinkHub(WinkBinarySensorDevice): def device_state_attributes(self): """Return the state attributes.""" return { - 'update needed': self.wink.update_needed(), - 'firmware version': self.wink.firmware_version() + 'update_needed': self.wink.update_needed(), + 'firmware_version': self.wink.firmware_version(), + 'pairing_mode': self.wink.pairing_mode() } diff --git a/homeassistant/components/climate/__init__.py b/homeassistant/components/climate/__init__.py index 8ccc3b2d663..53e60380a38 100644 --- a/homeassistant/components/climate/__init__.py +++ b/homeassistant/components/climate/__init__.py @@ -44,6 +44,12 @@ STATE_IDLE = 'idle' STATE_AUTO = 'auto' STATE_DRY = 'dry' STATE_FAN_ONLY = 'fan_only' +STATE_ECO = 'eco' +STATE_ELECTRIC = 'electric' +STATE_PERFORMANCE = 'performance' +STATE_HIGH_DEMAND = 'high_demand' +STATE_HEAT_PUMP = 'heat_pump' +STATE_GAS = 'gas' ATTR_CURRENT_TEMPERATURE = 'current_temperature' ATTR_MAX_TEMP = 'max_temp' diff --git a/homeassistant/components/climate/demo.py b/homeassistant/components/climate/demo.py index 0880cb3db8f..377985aaa12 100644 --- a/homeassistant/components/climate/demo.py +++ b/homeassistant/components/climate/demo.py @@ -114,7 +114,7 @@ class DemoClimate(ClimateDevice): @property def is_aux_heat_on(self): - """Return true if away mode is on.""" + """Return true if aux heat is on.""" return self._aux @property @@ -183,7 +183,7 @@ class DemoClimate(ClimateDevice): self.schedule_update_ha_state() def turn_aux_heat_on(self): - """Turn away auxiliary heater on.""" + """Turn auxillary heater on.""" self._aux = True self.schedule_update_ha_state() diff --git a/homeassistant/components/climate/wink.py b/homeassistant/components/climate/wink.py index 90b101e1b7b..f72cefc0841 100644 --- a/homeassistant/components/climate/wink.py +++ b/homeassistant/components/climate/wink.py @@ -1,30 +1,45 @@ """ -Support for Wink thermostats. +Support for Wink thermostats, Air Conditioners, and Water Heaters. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/climate.wink/ """ +import logging import asyncio from homeassistant.components.wink import WinkDevice, DOMAIN 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) + ATTR_TEMPERATURE, STATE_FAN_ONLY, + ATTR_CURRENT_HUMIDITY, STATE_ECO, STATE_ELECTRIC, + STATE_PERFORMANCE, STATE_HIGH_DEMAND, + STATE_HEAT_PUMP, STATE_GAS) from homeassistant.const import ( TEMP_CELSIUS, STATE_ON, STATE_OFF, STATE_UNKNOWN) +_LOGGER = logging.getLogger(__name__) + DEPENDENCIES = ['wink'] -STATE_AUX = 'aux' -STATE_ECO = 'eco' -STATE_FAN = 'fan' SPEED_LOW = 'low' SPEED_MEDIUM = 'medium' SPEED_HIGH = 'high' +HA_STATE_TO_WINK = {STATE_AUTO: 'auto', + STATE_ECO: 'eco', + STATE_FAN_ONLY: 'fan_only', + STATE_HEAT: 'heat_only', + STATE_COOL: 'cool_only', + STATE_PERFORMANCE: 'performance', + STATE_HIGH_DEMAND: 'high_demand', + STATE_HEAT_PUMP: 'heat_pump', + STATE_ELECTRIC: 'electric_only', + STATE_GAS: 'gas', + STATE_OFF: 'off'} +WINK_STATE_TO_HA = {value: key for key, value in HA_STATE_TO_WINK.items()} + ATTR_EXTERNAL_TEMPERATURE = "external_temperature" ATTR_SMART_TEMPERATURE = "smart_temperature" ATTR_ECO_TARGET = "eco_target" @@ -32,28 +47,26 @@ ATTR_OCCUPIED = "occupied" def setup_platform(hass, config, add_devices, discovery_info=None): - """Set up the Wink thermostat.""" + """Set up the Wink climate devices.""" import pywink - temp_unit = hass.config.units.temperature_unit for climate in pywink.get_thermostats(): _id = climate.object_id() + climate.name() if _id not in hass.data[DOMAIN]['unique_ids']: - add_devices([WinkThermostat(climate, hass, temp_unit)]) + add_devices([WinkThermostat(climate, hass)]) for climate in pywink.get_air_conditioners(): _id = climate.object_id() + climate.name() if _id not in hass.data[DOMAIN]['unique_ids']: - add_devices([WinkAC(climate, hass, temp_unit)]) + add_devices([WinkAC(climate, hass)]) + for water_heater in pywink.get_water_heaters(): + _id = water_heater.object_id() + water_heater.name() + if _id not in hass.data[DOMAIN]['unique_ids']: + add_devices([WinkWaterHeater(water_heater, hass)]) # pylint: disable=abstract-method class WinkThermostat(WinkDevice, ClimateDevice): """Representation of a Wink thermostat.""" - def __init__(self, wink, hass, temp_unit): - """Initialize the Wink device.""" - super().__init__(wink, hass) - self._config_temp_unit = temp_unit - @asyncio.coroutine def async_added_to_hass(self): """Callback when entity is added to hass.""" @@ -139,18 +152,12 @@ class WinkThermostat(WinkDevice, ClimateDevice): """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 + current_op = WINK_STATE_TO_HA.get(self.wink.current_hvac_mode()) + if current_op == 'aux': + return STATE_HEAT + if current_op is None: + current_op = STATE_UNKNOWN return current_op @property @@ -199,11 +206,12 @@ class WinkThermostat(WinkDevice, ClimateDevice): @property def is_aux_heat_on(self): """Return true if aux heater.""" - if self.wink.current_hvac_mode() == 'aux' and self.wink.is_on(): + if 'aux' not in self.wink.hvac_modes(): + return None + + if self.wink.current_hvac_mode() == 'aux': return True - elif self.wink.current_hvac_mode() == 'aux' and not self.wink.is_on(): - return False - return None + return False def set_temperature(self, **kwargs): """Set new target temperature.""" @@ -223,32 +231,27 @@ class WinkThermostat(WinkDevice, ClimateDevice): 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') + op_mode_to_set = HA_STATE_TO_WINK.get(operation_mode) + # The only way to disable aux heat is with the toggle + if self.is_aux_heat_on and op_mode_to_set == STATE_HEAT: + return + self.wink.set_operation_mode(op_mode_to_set) @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) + for mode in modes: + if mode == 'aux': + continue + ha_mode = WINK_STATE_TO_HA.get(mode) + if ha_mode is not None: + op_list.append(ha_mode) + else: + error = "Invaid operation mode mapping. " + mode + \ + " doesn't map. Please report this." + _LOGGER.error(error) return op_list def turn_away_mode_on(self): @@ -282,11 +285,11 @@ class WinkThermostat(WinkDevice, ClimateDevice): def turn_aux_heat_on(self): """Turn auxiliary heater on.""" - self.set_operation_mode(STATE_AUX) + self.wink.set_operation_mode('aux') def turn_aux_heat_off(self): """Turn auxiliary heater off.""" - self.set_operation_mode(STATE_AUTO) + self.set_operation_mode(STATE_HEAT) @property def min_temp(self): @@ -344,11 +347,6 @@ class WinkThermostat(WinkDevice, ClimateDevice): class WinkAC(WinkDevice, ClimateDevice): """Representation of a Wink air conditioner.""" - def __init__(self, wink, hass, temp_unit): - """Initialize the Wink device.""" - super().__init__(wink, hass) - self._config_temp_unit = temp_unit - @property def temperature_unit(self): """Return the unit of measurement.""" @@ -382,14 +380,10 @@ class WinkAC(WinkDevice, ClimateDevice): """Return current operation ie. heat, cool, idle.""" if not self.wink.is_on(): current_op = STATE_OFF - elif self.wink.current_mode() == 'cool_only': - current_op = STATE_COOL - elif self.wink.current_mode() == 'auto_eco': - current_op = STATE_ECO - elif self.wink.current_mode() == 'fan_only': - current_op = STATE_FAN else: - current_op = STATE_UNKNOWN + current_op = WINK_STATE_TO_HA.get(self.wink.current_hvac_mode()) + if current_op is None: + current_op = STATE_UNKNOWN return current_op @property @@ -397,12 +391,14 @@ class WinkAC(WinkDevice, ClimateDevice): """List of available operation modes.""" op_list = ['off'] modes = self.wink.modes() - if 'cool_only' in modes: - op_list.append(STATE_COOL) - if 'auto_eco' in modes: - op_list.append(STATE_ECO) - if 'fan_only' in modes: - op_list.append(STATE_FAN) + for mode in modes: + ha_mode = WINK_STATE_TO_HA.get(mode) + if ha_mode is not None: + op_list.append(ha_mode) + else: + error = "Invaid operation mode mapping. " + mode + \ + " doesn't map. Please report this." + _LOGGER.error(error) return op_list def set_temperature(self, **kwargs): @@ -412,30 +408,16 @@ class WinkAC(WinkDevice, ClimateDevice): def set_operation_mode(self, operation_mode): """Set operation mode.""" - if operation_mode == STATE_COOL: - self.wink.set_operation_mode('cool_only') - elif operation_mode == STATE_ECO: - self.wink.set_operation_mode('auto_eco') - elif operation_mode == STATE_OFF: - self.wink.set_operation_mode('off') - elif operation_mode == STATE_FAN: - self.wink.set_operation_mode('fan_only') + op_mode_to_set = HA_STATE_TO_WINK.get(operation_mode) + if op_mode_to_set == 'eco': + op_mode_to_set = 'auto_eco' + self.wink.set_operation_mode(op_mode_to_set) @property def target_temperature(self): """Return the temperature we try to reach.""" return self.wink.current_max_set_point() - @property - def target_temperature_low(self): - """Only supports cool.""" - return None - - @property - def target_temperature_high(self): - """Only supports cool.""" - return None - @property def current_fan_mode(self): """Return the current fan mode.""" @@ -453,12 +435,97 @@ class WinkAC(WinkDevice, ClimateDevice): """Return a list of available fan modes.""" return [SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH] - def set_fan_mode(self, mode): + def set_fan_mode(self, fan): """Set fan speed.""" - if mode == SPEED_LOW: + if fan == SPEED_LOW: speed = 0.4 - elif mode == SPEED_MEDIUM: + elif fan == SPEED_MEDIUM: speed = 0.8 - elif mode == SPEED_HIGH: + elif fan == SPEED_HIGH: speed = 1.0 self.wink.set_ac_fan_speed(speed) + + +class WinkWaterHeater(WinkDevice, ClimateDevice): + """Representation of a Wink water heater.""" + + @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 = {} + data["vacation_mode"] = self.wink.vacation_mode_enabled() + data["rheem_type"] = self.wink.rheem_type() + + return data + + @property + def current_operation(self): + """ + Return current operation one of the following. + + ["eco", "performance", "heat_pump", + "high_demand", "electric_only", "gas] + """ + if not self.wink.is_on(): + current_op = STATE_OFF + else: + current_op = WINK_STATE_TO_HA.get(self.wink.current_mode()) + if current_op is None: + current_op = STATE_UNKNOWN + return current_op + + @property + def operation_list(self): + """List of available operation modes.""" + op_list = ['off'] + modes = self.wink.modes() + for mode in modes: + if mode == 'aux': + continue + ha_mode = WINK_STATE_TO_HA.get(mode) + if ha_mode is not None: + op_list.append(ha_mode) + else: + error = "Invaid operation mode mapping. " + mode + \ + " doesn't map. Please report this." + _LOGGER.error(error) + return op_list + + def set_temperature(self, **kwargs): + """Set new target temperature.""" + target_temp = kwargs.get(ATTR_TEMPERATURE) + self.wink.set_temperature(target_temp) + + def set_operation_mode(self, operation_mode): + """Set operation mode.""" + op_mode_to_set = HA_STATE_TO_WINK.get(operation_mode) + self.wink.set_operation_mode(op_mode_to_set) + + @property + def target_temperature(self): + """Return the temperature we try to reach.""" + return self.wink.current_set_point() + + def turn_away_mode_on(self): + """Turn away on.""" + self.wink.set_vacation_mode(True) + + def turn_away_mode_off(self): + """Turn away off.""" + self.wink.set_vacation_mode(False) + + @property + def min_temp(self): + """Return the minimum temperature.""" + return self.wink.min_set_point() + + @property + def max_temp(self): + """Return the maximum temperature.""" + return self.wink.max_set_point() diff --git a/homeassistant/components/lock/services.yaml b/homeassistant/components/lock/services.yaml index 3fde6a2d8ad..810ef5a2e5b 100644 --- a/homeassistant/components/lock/services.yaml +++ b/homeassistant/components/lock/services.yaml @@ -83,7 +83,7 @@ wink_set_lock_vacation_mode: description: Name of lock to unlock example: 'lock.front_door' enabled: - description: enable or disable. true or false. + description: enable or disable. true or false. example: true wink_set_lock_alarm_mode: @@ -94,7 +94,7 @@ wink_set_lock_alarm_mode: description: Name of lock to unlock example: 'lock.front_door' mode: - description: One of tamper, activity, or forced_entry + description: One of tamper, activity, or forced_entry example: tamper wink_set_lock_alarm_sensitivity: @@ -105,7 +105,7 @@ wink_set_lock_alarm_sensitivity: description: Name of lock to unlock example: 'lock.front_door' sensitivity: - description: One of low, medium_low, medium, medium_high, high + description: One of low, medium_low, medium, medium_high, high example: medium wink_set_lock_alarm_state: @@ -116,7 +116,7 @@ wink_set_lock_alarm_state: description: Name of lock to unlock example: 'lock.front_door' enabled: - description: enable or disable. true or false. + description: enable or disable. true or false. example: true wink_set_lock_beeper_state: @@ -127,6 +127,19 @@ wink_set_lock_beeper_state: description: Name of lock to unlock example: 'lock.front_door' enabled: - description: enable or disable. true or false. + description: enable or disable. true or false. example: true +wink_add_new_lock_key_code: + description: Add a new user key code. + + fields: + entity_id: + description: Name of lock to unlock + example: 'lock.front_door' + name: + description: name of the new key code. + example: Bob + code: + description: new key code, length must match length of other codes. Default length is 4. + example: 1234 diff --git a/homeassistant/components/lock/wink.py b/homeassistant/components/lock/wink.py index 020fc00ab9a..502592ac6f3 100644 --- a/homeassistant/components/lock/wink.py +++ b/homeassistant/components/lock/wink.py @@ -13,7 +13,7 @@ import voluptuous as vol from homeassistant.components.lock import LockDevice from homeassistant.components.wink import WinkDevice, DOMAIN import homeassistant.helpers.config_validation as cv -from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN +from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN, ATTR_CODE from homeassistant.config import load_yaml_config_file DEPENDENCIES = ['wink'] @@ -25,10 +25,12 @@ SERVICE_SET_ALARM_MODE = 'wink_set_lock_alarm_mode' SERVICE_SET_ALARM_SENSITIVITY = 'wink_set_lock_alarm_sensitivity' SERVICE_SET_ALARM_STATE = 'wink_set_lock_alarm_state' SERVICE_SET_BEEPER_STATE = 'wink_set_lock_beeper_state' +SERVICE_ADD_KEY = 'wink_add_new_lock_key_code' ATTR_ENABLED = 'enabled' ATTR_SENSITIVITY = 'sensitivity' ATTR_MODE = 'mode' +ATTR_NAME = 'name' ALARM_SENSITIVITY_MAP = {"low": 0.2, "medium_low": 0.4, "medium": 0.6, "medium_high": 0.8, @@ -53,6 +55,12 @@ SET_ALARM_MODES_SCHEMA = vol.Schema({ vol.Required(ATTR_MODE): vol.In(ALARM_MODES_MAP) }) +ADD_KEY_SCHEMA = vol.Schema({ + vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, + vol.Required(ATTR_NAME): cv.string, + vol.Required(ATTR_CODE): cv.positive_int, +}) + def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Wink platform.""" @@ -86,6 +94,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None): lock.set_alarm_mode(service.data.get(ATTR_MODE)) elif service.service == SERVICE_SET_ALARM_SENSITIVITY: lock.set_alarm_sensitivity(service.data.get(ATTR_SENSITIVITY)) + elif service.service == SERVICE_ADD_KEY: + name = service.data.get(ATTR_NAME) + code = service.data.get(ATTR_CODE) + lock.add_new_key(code, name) descriptions = load_yaml_config_file( path.join(path.dirname(__file__), 'services.yaml')) @@ -115,6 +127,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None): descriptions.get(SERVICE_SET_ALARM_SENSITIVITY), schema=SET_SENSITIVITY_SCHEMA) + hass.services.register(DOMAIN, SERVICE_ADD_KEY, + service_handle, + descriptions.get(SERVICE_ADD_KEY), + schema=ADD_KEY_SCHEMA) + class WinkLockDevice(WinkDevice, LockDevice): """Representation of a Wink lock.""" @@ -149,6 +166,10 @@ class WinkLockDevice(WinkDevice, LockDevice): """Set lock's beeper mode.""" self.wink.set_beeper_mode(enabled) + def add_new_key(self, code, name): + """Add a new user key code.""" + self.wink.add_new_key(code, name) + def set_alarm_sensitivity(self, sensitivity): """ Set lock's alarm sensitivity. @@ -176,14 +197,14 @@ class WinkLockDevice(WinkDevice, LockDevice): super_attrs = super().device_state_attributes sensitivity = dict_value_to_key(ALARM_SENSITIVITY_MAP, self.wink.alarm_sensitivity()) - super_attrs['alarm sensitivity'] = sensitivity - super_attrs['vacation mode'] = self.wink.vacation_mode_enabled() - super_attrs['beeper mode'] = self.wink.beeper_enabled() - super_attrs['auto lock'] = self.wink.auto_lock_enabled() + super_attrs['alarm_sensitivity'] = sensitivity + super_attrs['vacation_mode'] = self.wink.vacation_mode_enabled() + super_attrs['beeper_mode'] = self.wink.beeper_enabled() + super_attrs['auto_lock'] = self.wink.auto_lock_enabled() alarm_mode = dict_value_to_key(ALARM_MODES_MAP, self.wink.alarm_mode()) - super_attrs['alarm mode'] = alarm_mode - super_attrs['alarm enabled'] = self.wink.alarm_enabled() + super_attrs['alarm_mode'] = alarm_mode + super_attrs['alarm_enabled'] = self.wink.alarm_enabled() return super_attrs diff --git a/homeassistant/components/services.yaml b/homeassistant/components/services.yaml index 0c9f1daf70f..69a5982caeb 100644 --- a/homeassistant/components/services.yaml +++ b/homeassistant/components/services.yaml @@ -1,3 +1,4 @@ + foursquare: checkin: description: Check a user into a Foursquare venue @@ -115,7 +116,7 @@ persistent_notification: notification_id: description: Target ID of the notification, will replace a notification with the same Id. [Optional] example: 1234 - + dismiss: description: Remove a notification from the frontend @@ -580,22 +581,22 @@ abode: setting: description: Setting to change. example: 'beeper_mute' - + value: description: Value of the setting. example: '1' capture_image: description: Request a new image capture from a camera device. - + fields: entity_id: description: Entity id of the camera to request an image. example: 'camera.downstairs_motion_camera' - + trigger_quick_action: description: Trigger an Abode quick action. - + fields: entity_id: description: Entity id of the quick action to trigger. @@ -620,8 +621,47 @@ input_boolean: turn_on: description: Turns ON an input boolean - + fields: entity_id: description: Entity id of the input boolean to turn on example: 'input_boolean.notify_alerts' + +wink: + pair_new_device: + description: Pair a new device to a Wink Hub. + + fields: + hub_name: + description: The name of the hub to pair a new device to. + example: 'My hub' + pairing_mode: + description: One of ["zigbee", "zwave", "zwave_exclusion", "zwave_network_rediscovery", "lutron", "bluetooth", "kidde"] + example: 'zigbee' + kidde_radio_code: + description: A string of 8 1s and 0s one for each dip switch on the kidde device left --> right = 1 --> 8 + example: '10101010' + + rename_wink_device: + description: Rename the provided device. + + fields: + entity_id: + description: The entity_id of the device to rename. + example: binary_sensor.front_door_opened + name: + description: The name to change it to. + example: back_door + + delete_wink_device: + description: Remove/unpair device from Wink. + + fields: + entity_id: + description: The entity_id of the device to delete. + + pull_newly_added_devices_from_wink: + description: Pull newly pair devices from Wink. + + refresh_state_from_wink: + description: Pull the latest states for every device. diff --git a/homeassistant/components/wink.py b/homeassistant/components/wink.py index 23eb90daa89..0b3a006a8d2 100644 --- a/homeassistant/components/wink.py +++ b/homeassistant/components/wink.py @@ -20,11 +20,12 @@ from homeassistant.helpers.event import track_time_interval from homeassistant.const import ( ATTR_BATTERY_LEVEL, CONF_EMAIL, CONF_PASSWORD, EVENT_HOMEASSISTANT_START, - EVENT_HOMEASSISTANT_STOP, __version__) + EVENT_HOMEASSISTANT_STOP, __version__, ATTR_ENTITY_ID) from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv +from homeassistant.config import load_yaml_config_file -REQUIREMENTS = ['python-wink==1.5.1', 'pubnubsub-handler==1.0.2'] +REQUIREMENTS = ['python-wink==1.6.0', 'pubnubsub-handler==1.0.2'] _LOGGER = logging.getLogger(__name__) @@ -45,6 +46,10 @@ ATTR_ACCESS_TOKEN = 'access_token' ATTR_REFRESH_TOKEN = 'refresh_token' ATTR_CLIENT_ID = 'client_id' ATTR_CLIENT_SECRET = 'client_secret' +ATTR_NAME = 'name' +ATTR_PAIRING_MODE = 'pairing_mode' +ATTR_KIDDE_RADIO_CODE = 'kidde_radio_code' +ATTR_HUB_NAME = 'hub_name' WINK_AUTH_CALLBACK_PATH = '/auth/wink/callback' WINK_AUTH_START = '/auth/wink' @@ -56,9 +61,12 @@ DEFAULT_CONFIG = { 'client_secret': 'CLIENT_SECRET_HERE' } -SERVICE_ADD_NEW_DEVICES = 'add_new_devices' +SERVICE_ADD_NEW_DEVICES = 'pull_newly_added_devices_from_wink' SERVICE_REFRESH_STATES = 'refresh_state_from_wink' -SERVICE_KEEP_ALIVE = 'keep_pubnub_updates_flowing' +SERVICE_RENAME_DEVICE = 'rename_wink_device' +SERVICE_DELETE_DEVICE = 'delete_wink_device' +SERVICE_SET_PAIRING_MODE = 'pair_new_device' + CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ @@ -74,11 +82,29 @@ CONFIG_SCHEMA = vol.Schema({ }) }, extra=vol.ALLOW_EXTRA) + +RENAME_DEVICE_SCHEMA = vol.Schema({ + vol.Required(ATTR_ENTITY_ID): cv.entity_ids, + vol.Required(ATTR_NAME): cv.string +}, extra=vol.ALLOW_EXTRA) + +DELETE_DEVICE_SCHEMA = vol.Schema({ + vol.Required(ATTR_ENTITY_ID): cv.entity_ids +}, extra=vol.ALLOW_EXTRA) + +SET_PAIRING_MODE_SCHEMA = vol.Schema({ + vol.Required(ATTR_HUB_NAME): cv.string, + vol.Required(ATTR_PAIRING_MODE): cv.string, + vol.Optional(ATTR_KIDDE_RADIO_CODE): cv.string +}, extra=vol.ALLOW_EXTRA) + WINK_COMPONENTS = [ 'binary_sensor', 'sensor', 'light', 'switch', 'lock', 'cover', 'climate', 'fan', 'alarm_control_panel', 'scene' ] +WINK_HUBS = [] + def _write_config_file(file_path, config): try: @@ -177,6 +203,9 @@ def setup(hass, config): import pywink from pubnubsubhandler import PubNubSubscriptionHandler + descriptions = load_yaml_config_file( + os.path.join(os.path.dirname(__file__), 'services.yaml')).get(DOMAIN) + if hass.data.get(DOMAIN) is None: hass.data[DOMAIN] = { 'unique_ids': [], @@ -313,6 +342,7 @@ def setup(hass, config): def stop_subscription(event): """Stop the pubnub subscription.""" hass.data[DOMAIN]['pubnub'].unsubscribe() + hass.data[DOMAIN]['pubnub'] = None hass.bus.listen(EVENT_HOMEASSISTANT_STOP, stop_subscription) @@ -333,7 +363,9 @@ def setup(hass, config): for entity in entity_list: time.sleep(1) entity.schedule_update_ha_state(True) - hass.services.register(DOMAIN, SERVICE_REFRESH_STATES, force_update) + + hass.services.register(DOMAIN, SERVICE_REFRESH_STATES, force_update, + descriptions.get(SERVICE_REFRESH_STATES)) def pull_new_devices(call): """Pull new devices added to users Wink account since startup.""" @@ -341,12 +373,71 @@ def setup(hass, config): for _component in WINK_COMPONENTS: discovery.load_platform(hass, _component, DOMAIN, {}, config) - hass.services.register(DOMAIN, SERVICE_ADD_NEW_DEVICES, pull_new_devices) + hass.services.register(DOMAIN, SERVICE_ADD_NEW_DEVICES, pull_new_devices, + descriptions.get(SERVICE_ADD_NEW_DEVICES)) + + def set_pairing_mode(call): + """Put the hub in provided pairing mode.""" + hub_name = call.data.get('hub_name') + pairing_mode = call.data.get('pairing_mode') + kidde_code = call.data.get('kidde_radio_code') + for hub in WINK_HUBS: + if hub.name() == hub_name: + hub.pair_new_device(pairing_mode, + kidde_radio_code=kidde_code) + + def rename_device(call): + """Set specified device's name.""" + # This should only be called on one device at a time. + found_device = None + entity_id = call.data.get('entity_id')[0] + all_devices = [] + for list_of_devices in hass.data[DOMAIN]['entities'].values(): + all_devices += list_of_devices + for device in all_devices: + if device.entity_id == entity_id: + found_device = device + if found_device is not None: + name = call.data.get('name') + found_device.wink.set_name(name) + + hass.services.register(DOMAIN, SERVICE_RENAME_DEVICE, rename_device, + descriptions.get(SERVICE_RENAME_DEVICE), + schema=RENAME_DEVICE_SCHEMA) + + def delete_device(call): + """Delete specified device.""" + # This should only be called on one device at a time. + found_device = None + entity_id = call.data.get('entity_id')[0] + all_devices = [] + for list_of_devices in hass.data[DOMAIN]['entities'].values(): + all_devices += list_of_devices + for device in all_devices: + if device.entity_id == entity_id: + found_device = device + if found_device is not None: + found_device.wink.remove_device() + + hass.services.register(DOMAIN, SERVICE_DELETE_DEVICE, delete_device, + descriptions.get(SERVICE_DELETE_DEVICE), + schema=DELETE_DEVICE_SCHEMA) + + hubs = pywink.get_hubs() + for hub in hubs: + if hub.device_manufacturer() == 'wink': + WINK_HUBS.append(hub) + + if WINK_HUBS: + hass.services.register( + DOMAIN, SERVICE_SET_PAIRING_MODE, set_pairing_mode, + descriptions.get(SERVICE_SET_PAIRING_MODE), + schema=SET_PAIRING_MODE_SCHEMA) # Load components for the devices in Wink that we support - for component in WINK_COMPONENTS: - hass.data[DOMAIN]['entities'][component] = [] - discovery.load_platform(hass, component, DOMAIN, {}, config) + for wink_component in WINK_COMPONENTS: + hass.data[DOMAIN]['entities'][wink_component] = [] + discovery.load_platform(hass, wink_component, DOMAIN, {}, config) return True diff --git a/requirements_all.txt b/requirements_all.txt index b3f3398501a..a5aab8adf00 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -807,7 +807,7 @@ python-velbus==2.0.11 python-vlc==1.1.2 # homeassistant.components.wink -python-wink==1.5.1 +python-wink==1.6.0 # homeassistant.components.sensor.swiss_public_transport python_opendata_transport==0.0.2