forked from home-assistant/core
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add sharkiq integration for Shark IQ robot vacuums (home-assistant#38272
- Loading branch information
Showing
17 changed files
with
1,183 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,119 @@ | ||
"""Shark IQ Integration.""" | ||
|
||
import asyncio | ||
|
||
import async_timeout | ||
from sharkiqpy import ( | ||
AylaApi, | ||
SharkIqAuthError, | ||
SharkIqAuthExpiringError, | ||
SharkIqNotAuthedError, | ||
get_ayla_api, | ||
) | ||
import voluptuous as vol | ||
|
||
from homeassistant import exceptions | ||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME | ||
|
||
from .const import API_TIMEOUT, COMPONENTS, DOMAIN, LOGGER | ||
from .update_coordinator import SharkIqUpdateCoordinator | ||
|
||
CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA) | ||
|
||
|
||
class CannotConnect(exceptions.HomeAssistantError): | ||
"""Error to indicate we cannot connect.""" | ||
|
||
|
||
async def async_setup(hass, config): | ||
"""Set up the sharkiq environment.""" | ||
hass.data.setdefault(DOMAIN, {}) | ||
if DOMAIN not in config: | ||
return True | ||
|
||
|
||
async def async_connect_or_timeout(ayla_api: AylaApi) -> bool: | ||
"""Connect to vacuum.""" | ||
try: | ||
with async_timeout.timeout(API_TIMEOUT): | ||
LOGGER.debug("Initialize connection to Ayla networks API") | ||
await ayla_api.async_sign_in() | ||
except SharkIqAuthError as exc: | ||
LOGGER.error("Authentication error connecting to Shark IQ api", exc_info=exc) | ||
return False | ||
except asyncio.TimeoutError as exc: | ||
LOGGER.error("Timeout expired", exc_info=exc) | ||
raise CannotConnect from exc | ||
|
||
return True | ||
|
||
|
||
async def async_setup_entry(hass, config_entry): | ||
"""Initialize the sharkiq platform via config entry.""" | ||
ayla_api = get_ayla_api( | ||
username=config_entry.data[CONF_USERNAME], | ||
password=config_entry.data[CONF_PASSWORD], | ||
websession=hass.helpers.aiohttp_client.async_get_clientsession(), | ||
) | ||
|
||
try: | ||
if not await async_connect_or_timeout(ayla_api): | ||
return False | ||
except CannotConnect as exc: | ||
raise exceptions.ConfigEntryNotReady from exc | ||
|
||
shark_vacs = await ayla_api.async_get_devices(False) | ||
device_names = ", ".join([d.name for d in shark_vacs]) | ||
LOGGER.debug("Found %d Shark IQ device(s): %s", len(device_names), device_names) | ||
coordinator = SharkIqUpdateCoordinator(hass, config_entry, ayla_api, shark_vacs) | ||
|
||
await coordinator.async_refresh() | ||
|
||
if not coordinator.last_update_success: | ||
raise exceptions.ConfigEntryNotReady | ||
|
||
hass.data[DOMAIN][config_entry.entry_id] = coordinator | ||
|
||
for component in COMPONENTS: | ||
hass.async_create_task( | ||
hass.config_entries.async_forward_entry_setup(config_entry, component) | ||
) | ||
|
||
return True | ||
|
||
|
||
async def async_disconnect_or_timeout(coordinator: SharkIqUpdateCoordinator): | ||
"""Disconnect to vacuum.""" | ||
LOGGER.debug("Disconnecting from Ayla Api") | ||
with async_timeout.timeout(5): | ||
try: | ||
await coordinator.ayla_api.async_sign_out() | ||
except (SharkIqAuthError, SharkIqAuthExpiringError, SharkIqNotAuthedError): | ||
pass | ||
return True | ||
|
||
|
||
async def async_update_options(hass, config_entry): | ||
"""Update options.""" | ||
await hass.config_entries.async_reload(config_entry.entry_id) | ||
|
||
|
||
async def async_unload_entry(hass, config_entry): | ||
"""Unload a config entry.""" | ||
unload_ok = all( | ||
await asyncio.gather( | ||
*[ | ||
hass.config_entries.async_forward_entry_unload(config_entry, component) | ||
for component in COMPONENTS | ||
] | ||
) | ||
) | ||
if unload_ok: | ||
domain_data = hass.data[DOMAIN][config_entry.entry_id] | ||
try: | ||
await async_disconnect_or_timeout(coordinator=domain_data) | ||
except SharkIqAuthError: | ||
pass | ||
hass.data[DOMAIN].pop(config_entry.entry_id) | ||
|
||
return unload_ok |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
"""Config flow for Shark IQ integration.""" | ||
|
||
import asyncio | ||
from typing import Dict, Optional | ||
|
||
import aiohttp | ||
import async_timeout | ||
from sharkiqpy import SharkIqAuthError, get_ayla_api | ||
import voluptuous as vol | ||
|
||
from homeassistant import config_entries, core, exceptions | ||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME | ||
|
||
from .const import DOMAIN, LOGGER # pylint:disable=unused-import | ||
|
||
SHARKIQ_SCHEMA = vol.Schema( | ||
{vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str} | ||
) | ||
|
||
|
||
async def validate_input(hass: core.HomeAssistant, data): | ||
"""Validate the user input allows us to connect.""" | ||
ayla_api = get_ayla_api( | ||
username=data[CONF_USERNAME], | ||
password=data[CONF_PASSWORD], | ||
websession=hass.helpers.aiohttp_client.async_get_clientsession(hass), | ||
) | ||
|
||
try: | ||
with async_timeout.timeout(10): | ||
LOGGER.debug("Initialize connection to Ayla networks API") | ||
await ayla_api.async_sign_in() | ||
except (asyncio.TimeoutError, aiohttp.ClientError): | ||
raise CannotConnect | ||
except SharkIqAuthError: | ||
raise InvalidAuth | ||
|
||
# Return info that you want to store in the config entry. | ||
return {"title": data[CONF_USERNAME]} | ||
|
||
|
||
class SharkIqConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): | ||
"""Handle a config flow for Shark IQ.""" | ||
|
||
VERSION = 1 | ||
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL | ||
|
||
async def _async_validate_input(self, user_input): | ||
"""Validate form input.""" | ||
errors = {} | ||
info = None | ||
|
||
if user_input is not None: | ||
# noinspection PyBroadException | ||
try: | ||
info = await validate_input(self.hass, user_input) | ||
except CannotConnect: | ||
errors["base"] = "cannot_connect" | ||
except InvalidAuth: | ||
errors["base"] = "invalid_auth" | ||
except Exception: # pylint: disable=broad-except | ||
LOGGER.exception("Unexpected exception") | ||
errors["base"] = "unknown" | ||
return info, errors | ||
|
||
async def async_step_user(self, user_input: Optional[Dict] = None): | ||
"""Handle the initial step.""" | ||
errors = {} | ||
if user_input is not None: | ||
info, errors = await self._async_validate_input(user_input) | ||
if info: | ||
return self.async_create_entry(title=info["title"], data=user_input) | ||
|
||
return self.async_show_form( | ||
step_id="user", data_schema=SHARKIQ_SCHEMA, errors=errors | ||
) | ||
|
||
async def async_step_reauth(self, user_input: Optional[dict] = None): | ||
"""Handle re-auth if login is invalid.""" | ||
errors = {} | ||
|
||
if user_input is not None: | ||
_, errors = await self._async_validate_input(user_input) | ||
|
||
if not errors: | ||
for entry in self._async_current_entries(): | ||
if entry.unique_id == self.unique_id: | ||
self.hass.config_entries.async_update_entry( | ||
entry, data=user_input | ||
) | ||
|
||
return self.async_abort(reason="reauth_successful") | ||
|
||
if errors["base"] != "invalid_auth": | ||
return self.async_abort(reason=errors["base"]) | ||
|
||
return self.async_show_form( | ||
step_id="reauth", data_schema=SHARKIQ_SCHEMA, errors=errors, | ||
) | ||
|
||
|
||
class CannotConnect(exceptions.HomeAssistantError): | ||
"""Error to indicate we cannot connect.""" | ||
|
||
|
||
class InvalidAuth(exceptions.HomeAssistantError): | ||
"""Error to indicate there is invalid auth.""" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
"""Shark IQ Constants.""" | ||
|
||
from datetime import timedelta | ||
import logging | ||
|
||
API_TIMEOUT = 20 | ||
COMPONENTS = ["vacuum"] | ||
DOMAIN = "sharkiq" | ||
LOGGER = logging.getLogger(__package__) | ||
SHARK = "Shark" | ||
UPDATE_INTERVAL = timedelta(seconds=30) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
{ | ||
"domain": "sharkiq", | ||
"name": "Shark IQ", | ||
"config_flow": true, | ||
"documentation": "https://www.home-assistant.io/integrations/sharkiq", | ||
"requirements": ["sharkiqpy==0.1.8"], | ||
"dependencies": [], | ||
"codeowners": ["@ajmarks"] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
{ | ||
"config": { | ||
"step": { | ||
"user": { | ||
"data": { | ||
"username": "[%key:common::config_flow::data::username%]", | ||
"password": "[%key:common::config_flow::data::password%]" | ||
} | ||
} | ||
}, | ||
"error": { | ||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", | ||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", | ||
"unknown": "[%key:common::config_flow::error::unknown%]" | ||
}, | ||
"abort": { | ||
"already_configured_account": "[%key:common::config_flow::abort::already_configured_account%]" | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
{ | ||
"title": "Shark IQ", | ||
"config": { | ||
"step": { | ||
"init": { | ||
"data": { | ||
"username": "Username", | ||
"password": "Password" | ||
} | ||
}, | ||
"user": { | ||
"data": { | ||
"username": "Username", | ||
"password": "Password" | ||
} | ||
} | ||
}, | ||
"error": { | ||
"cannot_connect": "Failed to connect", | ||
"invalid_auth": "Invalid authentication", | ||
"unknown": "Unexpected error" | ||
}, | ||
"abort": { | ||
"already_configured_account": "[%key:common::config_flow::abort::already_configured_account%]" | ||
} | ||
} | ||
} |
Oops, something went wrong.