From 90d894a499a8471181fb44b00b073fdaeadc6104 Mon Sep 17 00:00:00 2001 From: Jason Carter Date: Wed, 2 Nov 2016 00:49:27 -0400 Subject: [PATCH] Garadget (#4031) * Initial attempt at implementation * Adding Garadget cover component * Updating Device to be Required * Updating .coveragerc to exclude from testing * Updating code review items * Updating per 2nd code review * Updating configuration to be more like command-line --- .coveragerc | 1 + homeassistant/components/cover/garadget.py | 275 +++++++++++++++++++++ 2 files changed, 276 insertions(+) create mode 100644 homeassistant/components/cover/garadget.py diff --git a/.coveragerc b/.coveragerc index 3b821a8eeaf..37add2fb2ec 100644 --- a/.coveragerc +++ b/.coveragerc @@ -132,6 +132,7 @@ omit = homeassistant/components/climate/knx.py homeassistant/components/climate/proliphix.py homeassistant/components/climate/radiotherm.py + homeassistant/components/cover/garadget.py homeassistant/components/cover/homematic.py homeassistant/components/cover/rpi_gpio.py homeassistant/components/cover/scsgate.py diff --git a/homeassistant/components/cover/garadget.py b/homeassistant/components/cover/garadget.py new file mode 100644 index 00000000000..813ddea7170 --- /dev/null +++ b/homeassistant/components/cover/garadget.py @@ -0,0 +1,275 @@ +""" +Platform for the garadget cover component. + +For more details about this platform, please refer to the documentation +https://home-assistant.io/components/garadget/ +""" +import logging + +import voluptuous as vol + +import requests + +from homeassistant.components.cover import CoverDevice, PLATFORM_SCHEMA +from homeassistant.helpers.event import track_utc_time_change +from homeassistant.const import CONF_DEVICE, CONF_USERNAME, CONF_PASSWORD,\ + CONF_ACCESS_TOKEN, CONF_NAME, STATE_UNKNOWN, STATE_CLOSED, STATE_OPEN,\ + CONF_COVERS +import homeassistant.helpers.config_validation as cv + +DEFAULT_NAME = 'Garadget' + +ATTR_SIGNAL_STRENGTH = "wifi signal strength (dB)" +ATTR_TIME_IN_STATE = "time in state" +ATTR_SENSOR_STRENGTH = "sensor reflection rate" +ATTR_AVAILABLE = "available" + +STATE_OPENING = "opening" +STATE_CLOSING = "closing" +STATE_STOPPED = "stopped" +STATE_OFFLINE = "offline" + +STATES_MAP = { + "open": STATE_OPEN, + "opening": STATE_OPENING, + "closed": STATE_CLOSED, + "closing": STATE_CLOSING, + "stopped": STATE_STOPPED +} + + +# Validation of the user's configuration +COVER_SCHEMA = vol.Schema({ + vol.Optional(CONF_DEVICE): cv.string, + vol.Optional(CONF_USERNAME): cv.string, + vol.Optional(CONF_PASSWORD): cv.string, + vol.Optional(CONF_ACCESS_TOKEN): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string +}) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_COVERS): vol.Schema({cv.slug: COVER_SCHEMA}), +}) + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the Demo covers.""" + covers = [] + devices = config.get(CONF_COVERS, {}) + + _LOGGER.debug(devices) + + for device_id, device_config in devices.items(): + args = { + "name": device_config.get(CONF_NAME), + "device_id": device_config.get(CONF_DEVICE, device_id), + "username": device_config.get(CONF_USERNAME), + "password": device_config.get(CONF_PASSWORD), + "access_token": device_config.get(CONF_ACCESS_TOKEN) + } + + covers.append(GaradgetCover(hass, args)) + + add_devices(covers) + + +class GaradgetCover(CoverDevice): + """Representation of a demo cover.""" + + # pylint: disable=no-self-use, too-many-instance-attributes + def __init__(self, hass, args): + """Initialize the cover.""" + self.particle_url = 'https://api.particle.io' + self.hass = hass + self._name = args['name'] + self.device_id = args['device_id'] + self.access_token = args['access_token'] + self.obtained_token = False + self._username = args['username'] + self._password = args['password'] + self._state = STATE_UNKNOWN + self.time_in_state = None + self.signal = None + self.sensor = None + self._unsub_listener_cover = None + self._available = True + + if self.access_token is None: + self.access_token = self.get_token() + self._obtained_token = True + + # Lets try to get the configured name if not provided. + try: + if self._name is None: + doorconfig = self._get_variable("doorConfig") + if doorconfig["nme"] is not None: + self._name = doorconfig["nme"] + self.update() + except requests.exceptions.ConnectionError as ex: + _LOGGER.error('Unable to connect to server: %(reason)s', + dict(reason=ex)) + self._state = STATE_OFFLINE + self._available = False + self._name = DEFAULT_NAME + except KeyError as ex: + _LOGGER.warning('Garadget device %(device)s seems to be offline', + dict(device=self.device_id)) + self._name = DEFAULT_NAME + self._state = STATE_OFFLINE + self._available = False + + def __del__(self): + """Try to remove token.""" + if self._obtained_token is True: + if self.access_token is not None: + self.remove_token() + + @property + def name(self): + """Return the name of the cover.""" + return self._name + + @property + def should_poll(self): + """No polling needed for a demo cover.""" + return True + + @property + def available(self): + """Return True if entity is available.""" + return self._available + + @property + def device_state_attributes(self): + """Return the device state attributes.""" + data = {} + + if self.signal is not None: + data[ATTR_SIGNAL_STRENGTH] = self.signal + + if self.time_in_state is not None: + data[ATTR_TIME_IN_STATE] = self.time_in_state + + if self.sensor is not None: + data[ATTR_SENSOR_STRENGTH] = self.sensor + + if self.access_token is not None: + data[CONF_ACCESS_TOKEN] = self.access_token + + return data + + @property + def is_closed(self): + """Return if the cover is closed.""" + if self._state == STATE_UNKNOWN: + return None + else: + return self._state == STATE_CLOSED + + def get_token(self): + """Get new token for usage during this session.""" + args = { + 'grant_type': 'password', + 'username': self._username, + 'password': self._password + } + url = '{}/oauth/token'.format(self.particle_url) + ret = requests.post(url, + auth=('particle', 'particle'), + data=args) + + return ret.json()['access_token'] + + def remove_token(self): + """Remove authorization token from API.""" + ret = requests.delete('{}/v1/access_tokens/{}'.format( + self.particle_url, + self.access_token), + auth=(self._username, self._password)) + return ret.text + + def _start_watcher(self, command): + """Start watcher.""" + _LOGGER.debug("Starting Watcher for command: %s ", command) + if self._unsub_listener_cover is None: + self._unsub_listener_cover = track_utc_time_change( + self.hass, self._check_state) + + def _check_state(self, now): + """Check the state of the service during an operation.""" + self.update() + self.update_ha_state() + + def close_cover(self): + """Close the cover.""" + if self._state not in ["close", "closing"]: + ret = self._put_command("setState", "close") + self._start_watcher('close') + return ret.get('return_value') == 1 + + def open_cover(self): + """Open the cover.""" + if self._state not in ["open", "opening"]: + ret = self._put_command("setState", "open") + self._start_watcher('open') + return ret.get('return_value') == 1 + + def stop_cover(self): + """Stop the door where it is.""" + if self._state not in ["stopped"]: + ret = self._put_command("setState", "stop") + self._start_watcher('stop') + return ret['return_value'] == 1 + + def update(self): + """Get updated status from API.""" + try: + status = self._get_variable("doorStatus") + _LOGGER.debug("Current Status: %s", status['status']) + self._state = STATES_MAP.get(status['status'], STATE_UNKNOWN) + self.time_in_state = status['time'] + self.signal = status['signal'] + self.sensor = status['sensor'] + self._availble = True + except requests.exceptions.ConnectionError as ex: + _LOGGER.error('Unable to connect to server: %(reason)s', + dict(reason=ex)) + self._state = STATE_OFFLINE + except KeyError as ex: + _LOGGER.warning('Garadget device %(device)s seems to be offline', + dict(device=self.device_id)) + self._state = STATE_OFFLINE + + if self._state not in [STATE_CLOSING, STATE_OPENING]: + if self._unsub_listener_cover is not None: + self._unsub_listener_cover() + self._unsub_listener_cover = None + + def _get_variable(self, var): + """Get latest status.""" + url = '{}/v1/devices/{}/{}?access_token={}'.format( + self.particle_url, + self.device_id, + var, + self.access_token, + ) + ret = requests.get(url) + result = {} + for pairs in ret.json()['result'].split('|'): + key = pairs.split('=') + result[key[0]] = key[1] + return result + + def _put_command(self, func, arg=None): + """Send commands to API.""" + params = {'access_token': self.access_token} + if arg: + params['command'] = arg + url = '{}/v1/devices/{}/{}'.format( + self.particle_url, + self.device_id, + func) + ret = requests.post(url, data=params) + return ret.json()