diff --git a/.coveragerc b/.coveragerc index 2d2e4530b88..8be447086e1 100644 --- a/.coveragerc +++ b/.coveragerc @@ -152,7 +152,7 @@ omit = homeassistant/components/thermostat/honeywell.py homeassistant/components/thermostat/proliphix.py homeassistant/components/thermostat/radiotherm.py - + homeassistant/components/garage_door/wink.py [report] # Regexes for lines to exclude from consideration diff --git a/homeassistant/components/demo.py b/homeassistant/components/demo.py index 37f93c0625d..e63f5f49551 100644 --- a/homeassistant/components/demo.py +++ b/homeassistant/components/demo.py @@ -21,6 +21,7 @@ COMPONENTS_WITH_DEMO_PLATFORM = [ 'binary_sensor', 'camera', 'device_tracker', + 'garage_door', 'light', 'lock', 'media_player', diff --git a/homeassistant/components/garage_door/__init__.py b/homeassistant/components/garage_door/__init__.py new file mode 100644 index 00000000000..705b4ce547e --- /dev/null +++ b/homeassistant/components/garage_door/__init__.py @@ -0,0 +1,108 @@ +""" +homeassistant.components.garage_door +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Component to interface with garage doors that can be controlled remotely. + +For more details about this component, please refer to the documentation +at https://home-assistant.io/components/garage_door/ +""" + +import logging +import os + +from homeassistant.config import load_yaml_config_file +from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.helpers.entity import Entity + +from homeassistant.const import ( + STATE_CLOSED, STATE_OPEN, STATE_UNKNOWN, SERVICE_CLOSE, SERVICE_OPEN, + ATTR_ENTITY_ID) +from homeassistant.components import (group, wink) + +DOMAIN = 'garage_door' +SCAN_INTERVAL = 30 + +GROUP_NAME_ALL_GARAGE_DOORS = 'all garage doors' +ENTITY_ID_ALL_GARAGE_DOORS = group.ENTITY_ID_FORMAT.format('all_garage_doors') + +ENTITY_ID_FORMAT = DOMAIN + '.{}' + +# Maps discovered services to their platforms +DISCOVERY_PLATFORMS = { + wink.DISCOVER_GARAGE_DOORS: 'wink' +} + +_LOGGER = logging.getLogger(__name__) + + +def is_closed(hass, entity_id=None): + """ Returns if the garage door is closed based on the statemachine. """ + entity_id = entity_id or ENTITY_ID_ALL_GARAGE_DOORS + return hass.states.is_state(entity_id, STATE_CLOSED) + + +def close_door(hass, entity_id=None): + """ Closes all or specified garage door. """ + data = {ATTR_ENTITY_ID: entity_id} if entity_id else None + hass.services.call(DOMAIN, SERVICE_CLOSE, data) + + +def open_door(hass, entity_id=None): + """ Open all or specified garage door. """ + data = {ATTR_ENTITY_ID: entity_id} if entity_id else None + hass.services.call(DOMAIN, SERVICE_OPEN, data) + + +def setup(hass, config): + """ Track states and offer events for garage door. """ + component = EntityComponent( + _LOGGER, DOMAIN, hass, SCAN_INTERVAL, DISCOVERY_PLATFORMS, + GROUP_NAME_ALL_GARAGE_DOORS) + component.setup(config) + + def handle_garage_door_service(service): + """ Handles calls to the garage door services. """ + target_locks = component.extract_from_service(service) + + for item in target_locks: + if service.service == SERVICE_CLOSE: + item.close_door() + else: + item.open_door() + + if item.should_poll: + item.update_ha_state(True) + + descriptions = load_yaml_config_file( + os.path.join(os.path.dirname(__file__), 'services.yaml')) + hass.services.register(DOMAIN, SERVICE_OPEN, handle_garage_door_service, + descriptions.get(SERVICE_OPEN)) + hass.services.register(DOMAIN, SERVICE_CLOSE, handle_garage_door_service, + descriptions.get(SERVICE_CLOSE)) + + return True + + +class GarageDoorDevice(Entity): + """ Represents a garage door within Home Assistant. """ + # pylint: disable=no-self-use + + @property + def is_closed(self): + """ Is the garage door closed or opened. """ + return None + + def close_door(self): + """ Closes the garage door. """ + raise NotImplementedError() + + def open_door(self): + """ Opens the garage door. """ + raise NotImplementedError() + + @property + def state(self): + closed = self.is_closed + if closed is None: + return STATE_UNKNOWN + return STATE_CLOSED if closed else STATE_OPEN diff --git a/homeassistant/components/garage_door/demo.py b/homeassistant/components/garage_door/demo.py new file mode 100644 index 00000000000..0562383a8bd --- /dev/null +++ b/homeassistant/components/garage_door/demo.py @@ -0,0 +1,49 @@ +""" +homeassistant.components.garage_door.demo +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Demo platform that has two fake garage doors. +""" +from homeassistant.components.garage_door import GarageDoorDevice +from homeassistant.const import STATE_CLOSED, STATE_OPEN + + +# pylint: disable=unused-argument +def setup_platform(hass, config, add_devices_callback, discovery_info=None): + """ Find and return demo garage doors. """ + add_devices_callback([ + DemoGarageDoor('Left Garage Door', STATE_CLOSED), + DemoGarageDoor('Right Garage Door', STATE_OPEN) + ]) + + +class DemoGarageDoor(GarageDoorDevice): + """ Provides a demo garage door. """ + def __init__(self, name, state): + self._name = name + self._state = state + + @property + def should_poll(self): + """ No polling needed for a demo garage door. """ + return False + + @property + def name(self): + """ Returns the name of the device if any. """ + return self._name + + @property + def is_closed(self): + """ True if device is closed. """ + return self._state == STATE_CLOSED + + def close_door(self, **kwargs): + """ Close the device. """ + self._state = STATE_CLOSED + self.update_ha_state() + + def open_door(self, **kwargs): + """ Open the device. """ + self._state = STATE_OPEN + self.update_ha_state() diff --git a/homeassistant/components/garage_door/services.yaml b/homeassistant/components/garage_door/services.yaml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/homeassistant/components/garage_door/wink.py b/homeassistant/components/garage_door/wink.py new file mode 100644 index 00000000000..73d6928b9e2 --- /dev/null +++ b/homeassistant/components/garage_door/wink.py @@ -0,0 +1,67 @@ +""" +homeassistant.components.garage_door.wink +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Support for Wink garage doors. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/garage_door.wink/ +""" +import logging + +from homeassistant.components.garage_door import GarageDoorDevice +from homeassistant.const import CONF_ACCESS_TOKEN + +REQUIREMENTS = ['python-wink==0.5.0'] + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """ Sets up the Wink platform. """ + import pywink + + if discovery_info is None: + token = config.get(CONF_ACCESS_TOKEN) + + if token is None: + logging.getLogger(__name__).error( + "Missing wink access_token. " + "Get one at https://winkbearertoken.appspot.com/") + return + + pywink.set_bearer_token(token) + + add_devices(WinkGarageDoorDevice(door) for door in + pywink.get_garage_doors()) + + +class WinkGarageDoorDevice(GarageDoorDevice): + """ Represents a Wink garage door. """ + + def __init__(self, wink): + self.wink = wink + + @property + def unique_id(self): + """ Returns the id of this wink garage door """ + return "{}.{}".format(self.__class__, self.wink.device_id()) + + @property + def name(self): + """ Returns the name of the garage door if any. """ + return self.wink.name() + + def update(self): + """ Update the state of the garage door. """ + self.wink.update_state() + + @property + def is_closed(self): + """ True if device is closed. """ + return self.wink.state() == 0 + + def close_door(self): + """ Close the device. """ + self.wink.set_state(0) + + def open_door(self): + """ Open the device. """ + self.wink.set_state(1) diff --git a/homeassistant/components/wink.py b/homeassistant/components/wink.py index 88881ad1ab7..6fa2b9287e9 100644 --- a/homeassistant/components/wink.py +++ b/homeassistant/components/wink.py @@ -22,6 +22,7 @@ DISCOVER_LIGHTS = "wink.lights" DISCOVER_SWITCHES = "wink.switches" DISCOVER_SENSORS = "wink.sensors" DISCOVER_LOCKS = "wink.locks" +DISCOVER_GARAGE_DOORS = "wink.garage_doors" def setup(hass, config): @@ -42,7 +43,8 @@ def setup(hass, config): pywink.get_powerstrip_outlets, DISCOVER_SWITCHES), ('sensor', lambda: pywink.get_sensors or pywink.get_eggtrays, DISCOVER_SENSORS), - ('lock', pywink.get_locks, DISCOVER_LOCKS)): + ('lock', pywink.get_locks, DISCOVER_LOCKS), + ('garage_door', pywink.get_garage_doors, DISCOVER_GARAGE_DOORS)): if func_exists(): component = get_component(component_name) diff --git a/homeassistant/const.py b/homeassistant/const.py index 42146895a01..d89b4021180 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -151,6 +151,9 @@ SERVICE_ALARM_TRIGGER = "alarm_trigger" SERVICE_LOCK = "lock" SERVICE_UNLOCK = "unlock" +SERVICE_OPEN = "open" +SERVICE_CLOSE = "close" + SERVICE_MOVE_UP = 'move_up' SERVICE_MOVE_DOWN = 'move_down' SERVICE_STOP = 'stop' diff --git a/requirements_all.txt b/requirements_all.txt index b5313e5b7c2..5ea5c6ec73f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -194,6 +194,7 @@ python-telegram-bot==3.2.0 python-twitch==1.2.0 # homeassistant.components.wink +# homeassistant.components.garage_door.wink # homeassistant.components.light.wink # homeassistant.components.lock.wink # homeassistant.components.sensor.wink diff --git a/tests/components/garage_door/__init__.py b/tests/components/garage_door/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/components/garage_door/test_demo.py b/tests/components/garage_door/test_demo.py new file mode 100644 index 00000000000..7c959709c48 --- /dev/null +++ b/tests/components/garage_door/test_demo.py @@ -0,0 +1,51 @@ +""" +tests.components.garage_door.test_demo +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Tests demo garage door component. +""" +import unittest + +import homeassistant.core as ha +import homeassistant.components.garage_door as gd + + +LEFT = 'garage_door.left_garage_door' +RIGHT = 'garage_door.right_garage_door' + + +class TestGarageDoorDemo(unittest.TestCase): + """ Test the demo garage door. """ + + def setUp(self): # pylint: disable=invalid-name + self.hass = ha.HomeAssistant() + self.assertTrue(gd.setup(self.hass, { + 'garage_door': { + 'platform': 'demo' + } + })) + + def tearDown(self): # pylint: disable=invalid-name + """ Stop down stuff we started. """ + self.hass.stop() + + def test_is_closed(self): + self.assertTrue(gd.is_closed(self.hass, LEFT)) + self.hass.states.is_state(LEFT, 'close') + + self.assertFalse(gd.is_closed(self.hass, RIGHT)) + self.hass.states.is_state(RIGHT, 'open') + + def test_open_door(self): + gd.open_door(self.hass, LEFT) + + self.hass.pool.block_till_done() + + self.assertFalse(gd.is_closed(self.hass, LEFT)) + + def test_close_door(self): + gd.close_door(self.hass, RIGHT) + + self.hass.pool.block_till_done() + + self.assertTrue(gd.is_closed(self.hass, RIGHT))