From 330ae0d88548ba14d75faedd0adc36ec55c0240d Mon Sep 17 00:00:00 2001 From: Alexander Date: Tue, 3 Sep 2019 19:09:25 +0200 Subject: [PATCH] Add support Slide cover (#25913) * Add support GoSlide cover * Fixed Parameters differ from overridden Fixed Removed other pylint warnings * Renamed GoSlide to Slide, because of Innovation in Motion rebranding * Fixed codeowners file * Fixed requirements file * Removed pylint: disable=unused-argument Removed DOMAIN not exist check Changed if to min/max Changed 3rd party import to top of the module Removed timeout/retry parameters Removed unused constants Added check for discovery_info is none Changed pass slide object instead of full hass object Changed pass api object instead of full hass object Added unique_id functionality Removed entity_id/name properties Removed supported_features/state functions * Fixed unused variables * Changed Slide API uses snake names Changed Improved exception handling Changed Updated Slide API to 0.50.0 * Changed moved exceptions into goslide-api Changed retry setup into coroutine * Changed str(err) to err Changed invert if result to if not result --- .coveragerc | 1 + CODEOWNERS | 1 + homeassistant/components/slide/__init__.py | 157 +++++++++++++++++++ homeassistant/components/slide/const.py | 7 + homeassistant/components/slide/cover.py | 124 +++++++++++++++ homeassistant/components/slide/manifest.json | 12 ++ requirements_all.txt | 3 + 7 files changed, 305 insertions(+) create mode 100755 homeassistant/components/slide/__init__.py create mode 100644 homeassistant/components/slide/const.py create mode 100644 homeassistant/components/slide/cover.py create mode 100644 homeassistant/components/slide/manifest.json diff --git a/.coveragerc b/.coveragerc index df87a5a1f71..6583f6e0ae5 100644 --- a/.coveragerc +++ b/.coveragerc @@ -564,6 +564,7 @@ omit = homeassistant/components/skybeacon/sensor.py homeassistant/components/skybell/* homeassistant/components/slack/notify.py + homeassistant/components/slide/* homeassistant/components/sma/sensor.py homeassistant/components/smappee/* homeassistant/components/smarty/* diff --git a/CODEOWNERS b/CODEOWNERS index 27c4f03ae93..9c2c8673e6e 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -236,6 +236,7 @@ homeassistant/components/shell_command/* @home-assistant/core homeassistant/components/shiftr/* @fabaff homeassistant/components/shodan/* @fabaff homeassistant/components/simplisafe/* @bachya +homeassistant/components/slide/* @ualex73 homeassistant/components/sma/* @kellerza homeassistant/components/smarthab/* @outadoc homeassistant/components/smartthings/* @andrewsayre diff --git a/homeassistant/components/slide/__init__.py b/homeassistant/components/slide/__init__.py new file mode 100755 index 00000000000..54154ae863e --- /dev/null +++ b/homeassistant/components/slide/__init__.py @@ -0,0 +1,157 @@ +"""Component for the Go Slide API.""" + +import logging +from datetime import timedelta + +import voluptuous as vol +from goslideapi import GoSlideCloud, goslideapi + +from homeassistant.const import ( + CONF_USERNAME, + CONF_PASSWORD, + CONF_SCAN_INTERVAL, + STATE_OPEN, + STATE_CLOSED, + STATE_OPENING, + STATE_CLOSING, +) +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.discovery import async_load_platform +from homeassistant.helpers.event import async_track_time_interval, async_call_later +from .const import DOMAIN, SLIDES, API, COMPONENT, DEFAULT_RETRY + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_SCAN_INTERVAL = timedelta(seconds=30) + +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional( + CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL + ): cv.time_period, + } + ) + }, + extra=vol.ALLOW_EXTRA, +) + + +async def async_setup(hass, config): + """Set up the Slide platform.""" + + async def update_slides(now=None): + """Update slide information.""" + result = await hass.data[DOMAIN][API].slides_overview() + + if result is None: + _LOGGER.error("Slide API does not work or returned an error") + return + + if result: + _LOGGER.debug("Slide API returned %d slide(s)", len(result)) + else: + _LOGGER.warning("Slide API returned 0 slides") + + for slide in result: + if "device_id" not in slide: + _LOGGER.error( + "Found invalid Slide entry, device_id is " "missing. Entry=%s", + slide, + ) + continue + + uid = slide["device_id"].replace("slide_", "") + slidenew = hass.data[DOMAIN][SLIDES].setdefault(uid, {}) + slidenew["mac"] = uid + slidenew["id"] = slide["id"] + slidenew["name"] = slide["device_name"] + slidenew["state"] = None + oldpos = slidenew.get("pos") + slidenew["pos"] = None + slidenew["online"] = False + + if "device_info" not in slide: + _LOGGER.error( + "Slide %s (%s) has no device_info Entry=%s", + slide["id"], + slidenew["mac"], + slide, + ) + continue + + # Check if we have pos (OK) or code (NOK) + if "pos" in slide["device_info"]: + slidenew["online"] = True + slidenew["pos"] = slide["device_info"]["pos"] + slidenew["pos"] = max(0, min(1, slidenew["pos"])) + + if oldpos is None or oldpos == slidenew["pos"]: + slidenew["state"] = ( + STATE_CLOSED if slidenew["pos"] > 0.95 else STATE_OPEN + ) + elif oldpos < slidenew["pos"]: + slidenew["state"] = ( + STATE_CLOSED if slidenew["pos"] >= 0.95 else STATE_CLOSING + ) + else: + slidenew["state"] = ( + STATE_OPEN if slidenew["pos"] <= 0.05 else STATE_OPENING + ) + elif "code" in slide["device_info"]: + _LOGGER.warning( + "Slide %s (%s) is offline with " "code=%s", + slide["id"], + slidenew["mac"], + slide["device_info"]["code"], + ) + else: + _LOGGER.error( + "Slide %s (%s) has invalid device_info %s", + slide["id"], + slidenew["mac"], + slide["device_info"], + ) + + _LOGGER.debug("Updated entry=%s", slidenew) + + async def retry_setup(now): + """Retry setup if a connection/timeout happens on Slide API.""" + await async_setup(hass, config) + + hass.data[DOMAIN] = {} + hass.data[DOMAIN][SLIDES] = {} + + username = config[DOMAIN][CONF_USERNAME] + password = config[DOMAIN][CONF_PASSWORD] + scaninterval = config[DOMAIN][CONF_SCAN_INTERVAL] + + hass.data[DOMAIN][API] = GoSlideCloud(username, password) + + try: + result = await hass.data[DOMAIN][API].login() + except (goslideapi.ClientConnectionError, goslideapi.ClientTimeoutError) as err: + _LOGGER.error( + "Error connecting to Slide Cloud: %s, going to retry in %s seconds", + err, + DEFAULT_RETRY, + ) + async_call_later(hass, DEFAULT_RETRY, retry_setup) + return True + + if not result: + _LOGGER.error("Slide API returned unknown error during authentication") + return False + + _LOGGER.debug("Slide API successfully authenticated") + + await update_slides() + + hass.async_create_task(async_load_platform(hass, COMPONENT, DOMAIN, {}, config)) + + async_track_time_interval(hass, update_slides, scaninterval) + + return True diff --git a/homeassistant/components/slide/const.py b/homeassistant/components/slide/const.py new file mode 100644 index 00000000000..de3d2e560c1 --- /dev/null +++ b/homeassistant/components/slide/const.py @@ -0,0 +1,7 @@ +"""Define constants for the Go Slide component.""" + +API = "api" +COMPONENT = "cover" +DOMAIN = "slide" +SLIDES = "slides" +DEFAULT_RETRY = 120 diff --git a/homeassistant/components/slide/cover.py b/homeassistant/components/slide/cover.py new file mode 100644 index 00000000000..1c4e6da5aac --- /dev/null +++ b/homeassistant/components/slide/cover.py @@ -0,0 +1,124 @@ +"""Support for Go Slide slides.""" + +import logging + +from homeassistant.const import ATTR_ID +from homeassistant.components.cover import ( + ATTR_POSITION, + STATE_CLOSED, + STATE_OPENING, + STATE_CLOSING, + DEVICE_CLASS_CURTAIN, + CoverDevice, +) +from .const import API, DOMAIN, SLIDES + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up cover(s) for Go Slide platform.""" + + if discovery_info is None: + return + + entities = [] + + for slide in hass.data[DOMAIN][SLIDES].values(): + _LOGGER.debug("Setting up Slide entity: %s", slide) + entities.append(SlideCover(hass.data[DOMAIN][API], slide)) + + async_add_entities(entities) + + +class SlideCover(CoverDevice): + """Representation of a Go Slide cover.""" + + def __init__(self, api, slide): + """Initialize the cover.""" + self._api = api + self._slide = slide + self._id = slide["id"] + self._unique_id = slide["mac"] + self._name = slide["name"] + + @property + def unique_id(self): + """Return the device unique id.""" + return self._unique_id + + @property + def name(self): + """Return the device name.""" + return self._name + + @property + def device_state_attributes(self): + """Return device specific state attributes.""" + return {ATTR_ID: self._id} + + @property + def is_opening(self): + """Return if the cover is opening or not.""" + return self._slide["state"] == STATE_OPENING + + @property + def is_closing(self): + """Return if the cover is closing or not.""" + return self._slide["state"] == STATE_CLOSING + + @property + def is_closed(self): + """Return None if status is unknown, True if closed, else False.""" + if self._slide["state"] is None: + return None + return self._slide["state"] == STATE_CLOSED + + @property + def available(self): + """Return False if state is not available.""" + return self._slide["online"] + + @property + def assumed_state(self): + """Let HA know the integration is assumed state.""" + return True + + @property + def device_class(self): + """Return the device class of the cover.""" + return DEVICE_CLASS_CURTAIN + + @property + def current_cover_position(self): + """Return the current position of cover shutter.""" + pos = self._slide["pos"] + if pos is not None: + pos = int(pos * 100) + return pos + + async def async_open_cover(self, **kwargs): + """Open the cover.""" + self._slide["state"] = STATE_OPENING + await self._api.slide_open(self._id) + + async def async_close_cover(self, **kwargs): + """Close the cover.""" + self._slide["state"] = STATE_CLOSING + await self._api.slide_close(self._id) + + async def async_stop_cover(self, **kwargs): + """Stop the cover.""" + await self._api.slide_stop(self._id) + + async def async_set_cover_position(self, **kwargs): + """Move the cover to a specific position.""" + position = kwargs[ATTR_POSITION] / 100 + + if self._slide["pos"] is not None: + if position > self._slide["pos"]: + self._slide["state"] = STATE_CLOSING + else: + self._slide["state"] = STATE_OPENING + + await self._api.slide_set_position(self._id, position) diff --git a/homeassistant/components/slide/manifest.json b/homeassistant/components/slide/manifest.json new file mode 100644 index 00000000000..f9fd7f242b6 --- /dev/null +++ b/homeassistant/components/slide/manifest.json @@ -0,0 +1,12 @@ +{ + "domain": "slide", + "name": "Slide", + "documentation": "https://www.home-assistant.io/components/slide", + "requirements": [ + "goslide-api==0.5.1" + ], + "dependencies": [], + "codeowners": [ + "@ualex73" + ] +} diff --git a/requirements_all.txt b/requirements_all.txt index 60e580296d0..c22370e576d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -567,6 +567,9 @@ google-cloud-texttospeech==0.4.0 # homeassistant.components.google_travel_time googlemaps==2.5.1 +# homeassistant.components.slide +goslide-api==0.5.1 + # homeassistant.components.remote_rpi_gpio gpiozero==1.4.1