mirror of https://github.com/simbaja/ha_gehome.git
Basically working
This commit is contained in:
parent
cf856c5f86
commit
5f8dafa623
|
@ -1,74 +1,60 @@
|
||||||
"""The ge_kitchen integration."""
|
"""The ge_kitchen integration."""
|
||||||
import asyncio
|
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import async_timeout
|
||||||
|
import logging
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry, SOURCE_IMPORT
|
||||||
from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET
|
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers import (
|
from gekitchen.async_login_flow import async_do_full_login_flow
|
||||||
aiohttp_client,
|
from . import auth_api, config_flow
|
||||||
config_entry_oauth2_flow,
|
from .const import (
|
||||||
config_validation as cv,
|
AUTH_HANDLER,
|
||||||
|
COORDINATOR,
|
||||||
|
DOMAIN,
|
||||||
|
OAUTH2_AUTH_URL,
|
||||||
|
OAUTH2_TOKEN_URL,
|
||||||
)
|
)
|
||||||
|
|
||||||
from . import api, config_flow
|
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||||
from .const import DOMAIN, OAUTH2_AUTHORIZE, OAUTH2_TOKEN
|
from .update_coordinator import GeKitchenUpdateCoordinator
|
||||||
|
|
||||||
CONFIG_SCHEMA = vol.Schema(
|
CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA)
|
||||||
{
|
PLATFORMS = ["sensor"]
|
||||||
DOMAIN: vol.Schema(
|
|
||||||
{
|
|
||||||
vol.Required(CONF_CLIENT_ID): cv.string,
|
|
||||||
vol.Required(CONF_CLIENT_SECRET): cv.string,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
},
|
|
||||||
extra=vol.ALLOW_EXTRA,
|
|
||||||
)
|
|
||||||
|
|
||||||
# TODO List the platforms that you want to support.
|
_LOGGER = logging.getLogger(__name__)
|
||||||
# For your initial PR, limit it to 1 platform.
|
|
||||||
PLATFORMS = ["light"]
|
|
||||||
|
|
||||||
|
|
||||||
async def async_setup(hass: HomeAssistant, config: dict):
|
async def async_setup(hass: HomeAssistant, config: dict):
|
||||||
"""Set up the ge_kitchen component."""
|
"""Set up the ge_kitchen component."""
|
||||||
hass.data[DOMAIN] = {}
|
hass.data.setdefault(DOMAIN, {})
|
||||||
|
|
||||||
if DOMAIN not in config:
|
if DOMAIN not in config:
|
||||||
return True
|
return True
|
||||||
|
for index, conf in enumerate(config[DOMAIN]):
|
||||||
config_flow.OAuth2FlowHandler.async_register_implementation(
|
_LOGGER.debug(
|
||||||
hass,
|
"Importing GE Kitchen Account #%d (Username: %s)", index, conf[CONF_USERNAME]
|
||||||
config_entry_oauth2_flow.LocalOAuth2Implementation(
|
)
|
||||||
hass,
|
hass.async_create_task(
|
||||||
DOMAIN,
|
hass.config_entries.flow.async_init(
|
||||||
config[DOMAIN][CONF_CLIENT_ID],
|
DOMAIN, context={"source": SOURCE_IMPORT}, data=conf,
|
||||||
config[DOMAIN][CONF_CLIENT_SECRET],
|
)
|
||||||
OAUTH2_AUTHORIZE,
|
)
|
||||||
OAUTH2_TOKEN,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
|
||||||
"""Set up ge_kitchen from a config entry."""
|
"""Set up ge_kitchen from a config entry."""
|
||||||
implementation = await config_entry_oauth2_flow.async_get_config_entry_implementation(
|
session = hass.helpers.aiohttp_client.async_get_clientsession()
|
||||||
hass, entry
|
xmpp_credentials = await async_do_full_login_flow(
|
||||||
|
session, entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD]
|
||||||
)
|
)
|
||||||
|
coordinator = GeKitchenUpdateCoordinator(hass, entry, xmpp_credentials)
|
||||||
|
hass.data[DOMAIN][entry.entry_id] = coordinator
|
||||||
|
|
||||||
session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation)
|
coordinator.start_client()
|
||||||
|
with async_timeout.timeout(120):
|
||||||
# If using a requests-based API lib
|
await coordinator.initialization_future
|
||||||
hass.data[DOMAIN][entry.entry_id] = api.ConfigEntryAuth(hass, entry, session)
|
|
||||||
|
|
||||||
# If using an aiohttp-based API lib
|
|
||||||
hass.data[DOMAIN][entry.entry_id] = api.AsyncConfigEntryAuth(
|
|
||||||
aiohttp_client.async_get_clientsession(hass), session
|
|
||||||
)
|
|
||||||
|
|
||||||
for component in PLATFORMS:
|
for component in PLATFORMS:
|
||||||
hass.async_create_task(
|
hass.async_create_task(
|
||||||
|
|
|
@ -1,58 +0,0 @@
|
||||||
"""API for ge_kitchen bound to Home Assistant OAuth."""
|
|
||||||
from asyncio import run_coroutine_threadsafe
|
|
||||||
|
|
||||||
from aiohttp import ClientSession
|
|
||||||
import my_pypi_package
|
|
||||||
|
|
||||||
from homeassistant import config_entries, core
|
|
||||||
from homeassistant.helpers import config_entry_oauth2_flow
|
|
||||||
|
|
||||||
# TODO the following two API examples are based on our suggested best practices
|
|
||||||
# for libraries using OAuth2 with requests or aiohttp. Delete the one you won't use.
|
|
||||||
# For more info see the docs at <insert url>.
|
|
||||||
|
|
||||||
|
|
||||||
class ConfigEntryAuth(my_pypi_package.AbstractAuth):
|
|
||||||
"""Provide ge_kitchen authentication tied to an OAuth2 based config entry."""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
hass: core.HomeAssistant,
|
|
||||||
config_entry: config_entries.ConfigEntry,
|
|
||||||
implementation: config_entry_oauth2_flow.AbstractOAuth2Implementation,
|
|
||||||
):
|
|
||||||
"""Initialize ge_kitchen Auth."""
|
|
||||||
self.hass = hass
|
|
||||||
self.config_entry = config_entry
|
|
||||||
self.session = config_entry_oauth2_flow.OAuth2Session(
|
|
||||||
hass, config_entry, implementation
|
|
||||||
)
|
|
||||||
super().__init__(self.session.token)
|
|
||||||
|
|
||||||
def refresh_tokens(self) -> dict:
|
|
||||||
"""Refresh and return new ge_kitchen tokens using Home Assistant OAuth2 session."""
|
|
||||||
run_coroutine_threadsafe(
|
|
||||||
self.session.async_ensure_token_valid(), self.hass.loop
|
|
||||||
).result()
|
|
||||||
|
|
||||||
return self.session.token
|
|
||||||
|
|
||||||
|
|
||||||
class AsyncConfigEntryAuth(my_pypi_package.AbstractAuth):
|
|
||||||
"""Provide ge_kitchen authentication tied to an OAuth2 based config entry."""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
websession: ClientSession,
|
|
||||||
oauth_session: config_entry_oauth2_flow.OAuth2Session,
|
|
||||||
):
|
|
||||||
"""Initialize ge_kitchen auth."""
|
|
||||||
super().__init__(websession)
|
|
||||||
self._oauth_session = oauth_session
|
|
||||||
|
|
||||||
async def async_get_access_token(self):
|
|
||||||
"""Return a valid access token."""
|
|
||||||
if not self._oauth_session.is_valid:
|
|
||||||
await self._oauth_session.async_ensure_token_valid()
|
|
||||||
|
|
||||||
return self._oauth_session.token
|
|
|
@ -0,0 +1,303 @@
|
||||||
|
"""Oven state representation."""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
from typing import Any, Dict, List, Optional, Type
|
||||||
|
|
||||||
|
from gekitchen import ErdCodeType, GeAppliance, translate_erd_code
|
||||||
|
from gekitchen.erd_types import OvenCookSetting, OvenConfiguration
|
||||||
|
from gekitchen.erd_constants import (
|
||||||
|
ErdCode,
|
||||||
|
ErdApplianceType,
|
||||||
|
ErdMeasurementUnits,
|
||||||
|
ErdOvenState,
|
||||||
|
)
|
||||||
|
from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.entity import Entity
|
||||||
|
|
||||||
|
from .const import DOMAIN
|
||||||
|
from .erd_string_utils import (
|
||||||
|
oven_display_state_to_str,
|
||||||
|
oven_cook_setting_to_str,
|
||||||
|
)
|
||||||
|
|
||||||
|
RAW_TEMPERATURE_ERD_CODES = {
|
||||||
|
ErdCode.HOT_WATER_SET_TEMP,
|
||||||
|
ErdCode.LOWER_OVEN_RAW_TEMPERATURE,
|
||||||
|
ErdCode.LOWER_OVEN_USER_TEMP_OFFSET,
|
||||||
|
ErdCode.UPPER_OVEN_RAW_TEMPERATURE,
|
||||||
|
ErdCode.UPPER_OVEN_USER_TEMP_OFFSET,
|
||||||
|
}
|
||||||
|
NONZERO_TEMPERATURE_ERD_CODES = {
|
||||||
|
ErdCode.LOWER_OVEN_DISPLAY_TEMPERATURE,
|
||||||
|
ErdCode.LOWER_OVEN_PROBE_DISPLAY_TEMP,
|
||||||
|
ErdCode.UPPER_OVEN_DISPLAY_TEMPERATURE,
|
||||||
|
ErdCode.UPPER_OVEN_PROBE_DISPLAY_TEMP,
|
||||||
|
}
|
||||||
|
TEMPERATURE_ERD_CODES = RAW_TEMPERATURE_ERD_CODES.union(NONZERO_TEMPERATURE_ERD_CODES)
|
||||||
|
TIMER_ERD_CODES = {
|
||||||
|
ErdCode.LOWER_OVEN_ELAPSED_COOK_TIME,
|
||||||
|
ErdCode.LOWER_OVEN_KITCHEN_TIMER,
|
||||||
|
ErdCode.LOWER_OVEN_DELAY_TIME_REMAINING,
|
||||||
|
ErdCode.LOWER_OVEN_COOK_TIME_REMAINING,
|
||||||
|
ErdCode.ELAPSED_ON_TIME,
|
||||||
|
ErdCode.TIME_REMAINING,
|
||||||
|
ErdCode.UPPER_OVEN_ELAPSED_COOK_TIME,
|
||||||
|
ErdCode.UPPER_OVEN_KITCHEN_TIMER,
|
||||||
|
ErdCode.UPPER_OVEN_DELAY_TIME_REMAINING,
|
||||||
|
ErdCode.UPPER_OVEN_COOK_TIME_REMAINING,
|
||||||
|
}
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def get_appliance_api_type(appliance_type: ErdApplianceType) -> Type:
|
||||||
|
"""Get the appropriate appliance type"""
|
||||||
|
if appliance_type == ErdApplianceType.OVEN:
|
||||||
|
return OvenApi
|
||||||
|
# Fallback
|
||||||
|
return ApplianceApi
|
||||||
|
|
||||||
|
|
||||||
|
def stringify_erd_value(erd_code: ErdCodeType, value: Any, units: str) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
Convert an erd property value to a nice string
|
||||||
|
|
||||||
|
:param erd_code:
|
||||||
|
:param value: The current value in its native format
|
||||||
|
:param units: Units to apply, if applicable
|
||||||
|
:return: The value converted to a string
|
||||||
|
"""
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
erd_code = translate_erd_code(erd_code)
|
||||||
|
|
||||||
|
if isinstance(value, ErdOvenState):
|
||||||
|
return oven_display_state_to_str(value)
|
||||||
|
if isinstance(value, OvenCookSetting):
|
||||||
|
return oven_cook_setting_to_str(value, units)
|
||||||
|
|
||||||
|
if erd_code == ErdCode.CLOCK_TIME:
|
||||||
|
return value.strftime('%H:%M:%S')
|
||||||
|
if erd_code in RAW_TEMPERATURE_ERD_CODES:
|
||||||
|
return f"{value}{units}"
|
||||||
|
if erd_code in NONZERO_TEMPERATURE_ERD_CODES:
|
||||||
|
return f"{value}{units}" if value else None
|
||||||
|
if erd_code in TIMER_ERD_CODES:
|
||||||
|
return str(value) if value else None
|
||||||
|
|
||||||
|
|
||||||
|
def get_erd_units(erd_code: ErdCodeType, measurement_units: ErdMeasurementUnits):
|
||||||
|
erd_code = translate_erd_code(erd_code)
|
||||||
|
if not measurement_units:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if erd_code in TEMPERATURE_ERD_CODES:
|
||||||
|
if measurement_units == ErdMeasurementUnits.METRIC:
|
||||||
|
return TEMP_CELSIUS
|
||||||
|
return TEMP_FAHRENHEIT
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class ApplianceApi:
|
||||||
|
"""
|
||||||
|
API class to represent a single physical device.
|
||||||
|
|
||||||
|
Since a physical device can have many entities, we'll pool common elements here
|
||||||
|
"""
|
||||||
|
APPLIANCE_TYPE = None # type: Optional[ErdApplianceType]
|
||||||
|
|
||||||
|
def __init__(self, hass: HomeAssistant, appliance: GeAppliance):
|
||||||
|
if not appliance.initialized:
|
||||||
|
raise RuntimeError('Appliance not ready')
|
||||||
|
self._appliance = appliance
|
||||||
|
self._loop = appliance.client.loop
|
||||||
|
self._hass = hass
|
||||||
|
self.initial_update = False
|
||||||
|
self._entities = {} # type: Optional[Dict[str, Entity]]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def hass(self) -> HomeAssistant:
|
||||||
|
return self._hass
|
||||||
|
|
||||||
|
@property
|
||||||
|
def loop(self) -> Optional[asyncio.AbstractEventLoop]:
|
||||||
|
if self._loop is None:
|
||||||
|
self._loop = self._appliance.client.loop
|
||||||
|
return self._loop
|
||||||
|
|
||||||
|
@property
|
||||||
|
def appliance(self) -> GeAppliance:
|
||||||
|
return self._appliance
|
||||||
|
|
||||||
|
@property
|
||||||
|
def serial_number(self) -> str:
|
||||||
|
return self.appliance.get_erd_value(ErdCode.SERIAL_NUMBER)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def model_number(self) -> str:
|
||||||
|
return self.appliance.get_erd_value(ErdCode.MODEL_NUMBER)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self) -> str:
|
||||||
|
return f"GE Appliance {self.serial_number}"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_info(self) -> Dict:
|
||||||
|
"""Device info dictionary."""
|
||||||
|
return {
|
||||||
|
"identifiers": {(DOMAIN, self.serial_number)},
|
||||||
|
"name": self.name,
|
||||||
|
"manufacturer": "GE",
|
||||||
|
"model": self.model_number
|
||||||
|
}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def entities(self) -> List[Entity]:
|
||||||
|
return list(self._entities.values())
|
||||||
|
|
||||||
|
def get_all_entities(self) -> List[Entity]:
|
||||||
|
"""Create Entities for this device."""
|
||||||
|
entities = [
|
||||||
|
GeSensor(self, ErdCode.CLOCK_TIME),
|
||||||
|
GeBinarySensor(self, ErdCode.SABBATH_MODE),
|
||||||
|
]
|
||||||
|
return entities
|
||||||
|
|
||||||
|
def build_entities_list(self) -> None:
|
||||||
|
"""Build the entities list, adding anything new."""
|
||||||
|
entities = self.get_all_entities()
|
||||||
|
for entity in entities:
|
||||||
|
if entity.unique_id not in self._entities:
|
||||||
|
self._entities[entity.unique_id] = entity
|
||||||
|
|
||||||
|
|
||||||
|
class OvenApi(ApplianceApi):
|
||||||
|
"""API class for oven objects"""
|
||||||
|
APPLIANCE_TYPE = ErdApplianceType.OVEN
|
||||||
|
|
||||||
|
def get_all_entities(self) -> List[Entity]:
|
||||||
|
base_entities = super().get_all_entities()
|
||||||
|
oven_config = self.appliance.get_erd_value(ErdCode.OVEN_CONFIGURATION) # type: OvenConfiguration
|
||||||
|
_LOGGER.debug(f'Oven Config: {oven_config}')
|
||||||
|
oven_entities = [
|
||||||
|
GeSensor(self, ErdCode.UPPER_OVEN_COOK_MODE),
|
||||||
|
GeSensor(self, ErdCode.UPPER_OVEN_COOK_TIME_REMAINING),
|
||||||
|
GeSensor(self, ErdCode.UPPER_OVEN_CURRENT_STATE),
|
||||||
|
GeSensor(self, ErdCode.UPPER_OVEN_DELAY_TIME_REMAINING),
|
||||||
|
GeSensor(self, ErdCode.UPPER_OVEN_DISPLAY_TEMPERATURE),
|
||||||
|
GeSensor(self, ErdCode.UPPER_OVEN_ELAPSED_COOK_TIME),
|
||||||
|
GeSensor(self, ErdCode.UPPER_OVEN_KITCHEN_TIMER),
|
||||||
|
GeSensor(self, ErdCode.UPPER_OVEN_PROBE_DISPLAY_TEMP),
|
||||||
|
GeSensor(self, ErdCode.UPPER_OVEN_USER_TEMP_OFFSET),
|
||||||
|
GeSensor(self, ErdCode.UPPER_OVEN_RAW_TEMPERATURE),
|
||||||
|
GeBinarySensor(self, ErdCode.UPPER_OVEN_PROBE_PRESENT),
|
||||||
|
GeBinarySensor(self, ErdCode.UPPER_OVEN_REMOTE_ENABLED),
|
||||||
|
]
|
||||||
|
|
||||||
|
if oven_config.has_lower_oven:
|
||||||
|
oven_entities.extend([
|
||||||
|
GeSensor(self, ErdCode.LOWER_OVEN_COOK_MODE),
|
||||||
|
GeSensor(self, ErdCode.LOWER_OVEN_COOK_TIME_REMAINING),
|
||||||
|
GeSensor(self, ErdCode.LOWER_OVEN_CURRENT_STATE),
|
||||||
|
GeSensor(self, ErdCode.LOWER_OVEN_DELAY_TIME_REMAINING),
|
||||||
|
GeSensor(self, ErdCode.LOWER_OVEN_DISPLAY_TEMPERATURE),
|
||||||
|
GeSensor(self, ErdCode.LOWER_OVEN_ELAPSED_COOK_TIME),
|
||||||
|
GeSensor(self, ErdCode.LOWER_OVEN_KITCHEN_TIMER),
|
||||||
|
GeSensor(self, ErdCode.LOWER_OVEN_PROBE_DISPLAY_TEMP),
|
||||||
|
GeSensor(self, ErdCode.LOWER_OVEN_USER_TEMP_OFFSET),
|
||||||
|
GeSensor(self, ErdCode.LOWER_OVEN_RAW_TEMPERATURE),
|
||||||
|
GeBinarySensor(self, ErdCode.LOWER_OVEN_PROBE_PRESENT),
|
||||||
|
GeBinarySensor(self, ErdCode.LOWER_OVEN_REMOTE_ENABLED),
|
||||||
|
])
|
||||||
|
return base_entities + oven_entities
|
||||||
|
|
||||||
|
|
||||||
|
class GeEntity(Entity):
|
||||||
|
"""Base class for all GE Entities"""
|
||||||
|
def __init__(self, api: ApplianceApi):
|
||||||
|
self._api = api
|
||||||
|
self.hass = None # type: Optional[HomeAssistant]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unique_id(self) -> str:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@property
|
||||||
|
def api(self) -> ApplianceApi:
|
||||||
|
return self._api
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_info(self) -> Optional[Dict[str, Any]]:
|
||||||
|
return self.api.device_info
|
||||||
|
|
||||||
|
@property
|
||||||
|
def serial_number(self):
|
||||||
|
return self.api.serial_number
|
||||||
|
|
||||||
|
@property
|
||||||
|
def should_poll(self) -> bool:
|
||||||
|
"""Don't poll."""
|
||||||
|
return False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def available(self) -> bool:
|
||||||
|
return self.appliance.available
|
||||||
|
|
||||||
|
@property
|
||||||
|
def appliance(self) -> GeAppliance:
|
||||||
|
return self.api.appliance
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self) -> Optional[str]:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
class GeErdEntity(GeEntity):
|
||||||
|
"""Parent class for GE entities tied to a specific ERD"""
|
||||||
|
def __init__(self, api: ApplianceApi, erd_code: ErdCodeType):
|
||||||
|
super().__init__(api)
|
||||||
|
self._erd_code = translate_erd_code(erd_code)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def erd_code(self) -> ErdCodeType:
|
||||||
|
return self._erd_code
|
||||||
|
|
||||||
|
@property
|
||||||
|
def erd_string(self) -> str:
|
||||||
|
erd_code = self.erd_code
|
||||||
|
if isinstance(self.erd_code, ErdCode):
|
||||||
|
return erd_code.name
|
||||||
|
return erd_code
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self) -> Optional[str]:
|
||||||
|
erd_string = self.erd_string
|
||||||
|
return ' '.join(erd_string.split('_')).title()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unique_id(self) -> Optional[str]:
|
||||||
|
return f'{DOMAIN}_{self.serial_number}_{self.erd_string.lower()}'
|
||||||
|
|
||||||
|
|
||||||
|
class GeSensor(GeErdEntity):
|
||||||
|
"""GE Entity for sensors"""
|
||||||
|
@property
|
||||||
|
def state(self) -> Optional[str]:
|
||||||
|
value = self.appliance.get_erd_value(self.erd_code)
|
||||||
|
return stringify_erd_value(self.erd_code, value, self.units)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def measurement_system(self) -> Optional[ErdMeasurementUnits]:
|
||||||
|
return self.appliance.get_erd_value(ErdCode.TEMPERATURE_UNIT)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def units(self) -> Optional[str]:
|
||||||
|
return get_erd_units(self.erd_code, self.measurement_system)
|
||||||
|
|
||||||
|
|
||||||
|
class GeBinarySensor(GeErdEntity):
|
||||||
|
@property
|
||||||
|
def is_on(self) -> bool:
|
||||||
|
return bool(self.appliance.get_erd_value(self.erd_code))
|
|
@ -0,0 +1,31 @@
|
||||||
|
"""GE Kitchen Sensor Entities"""
|
||||||
|
import async_timeout
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from typing import Callable
|
||||||
|
from .update_coordinator import GeKitchenUpdateCoordinator
|
||||||
|
from .appliance_api import GeBinarySensor
|
||||||
|
from .const import DOMAIN
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable):
|
||||||
|
"""GE Kitchen sensors."""
|
||||||
|
|
||||||
|
coordinator = hass.data[DOMAIN][config_entry.entry_id] # type: GeKitchenUpdateCoordinator
|
||||||
|
|
||||||
|
# This should be a NOP, but let's be safe
|
||||||
|
with async_timeout.timeout(20):
|
||||||
|
await coordinator.initialization_future
|
||||||
|
|
||||||
|
apis = coordinator.appliance_apis.values()
|
||||||
|
sensors = [
|
||||||
|
entity
|
||||||
|
for api in apis
|
||||||
|
for entity in api.entities
|
||||||
|
if isinstance(entity, GeBinarySensor)
|
||||||
|
]
|
||||||
|
async_add_entities(sensors)
|
|
@ -1,24 +1,107 @@
|
||||||
"""Config flow for ge_kitchen."""
|
"""Config flow for GE Kitchen integration."""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
|
from typing import Dict, Optional
|
||||||
|
|
||||||
from homeassistant import config_entries
|
import aiohttp
|
||||||
from homeassistant.helpers import config_entry_oauth2_flow
|
import async_timeout
|
||||||
|
from gekitchen import async_get_oauth2_token
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
from .const import DOMAIN
|
from homeassistant import config_entries, core, exceptions
|
||||||
|
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||||
|
|
||||||
|
from .const import DOMAIN # pylint:disable=unused-import
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
GEKITCHEN_SCHEMA = vol.Schema(
|
||||||
|
{vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str}
|
||||||
|
)
|
||||||
|
|
||||||
class OAuth2FlowHandler(
|
|
||||||
config_entry_oauth2_flow.AbstractOAuth2FlowHandler, domain=DOMAIN
|
|
||||||
):
|
|
||||||
"""Config flow to handle ge_kitchen OAuth2 authentication."""
|
|
||||||
|
|
||||||
DOMAIN = DOMAIN
|
async def validate_input(hass: core.HomeAssistant, data):
|
||||||
# TODO Pick one from config_entries.CONN_CLASS_*
|
"""Validate the user input allows us to connect."""
|
||||||
CONNECTION_CLASS = config_entries.CONN_CLASS_UNKNOWN
|
|
||||||
|
|
||||||
@property
|
session = hass.helpers.aiohttp_client.async_get_clientsession(hass)
|
||||||
def logger(self) -> logging.Logger:
|
|
||||||
"""Return logger."""
|
# noinspection PyBroadException
|
||||||
return logging.getLogger(__name__)
|
try:
|
||||||
|
with async_timeout.timeout(10):
|
||||||
|
_ = await async_get_oauth2_token(session, data[CONF_USERNAME], data[CONF_PASSWORD])
|
||||||
|
except (asyncio.TimeoutError, aiohttp.ClientError):
|
||||||
|
raise CannotConnect
|
||||||
|
except Exception: # pylint: disable=broad-except
|
||||||
|
raise InvalidAuth
|
||||||
|
|
||||||
|
# Return info that you want to store in the config entry.
|
||||||
|
return {"title": f"GE Kitchen ({data[CONF_USERNAME]:s})"}
|
||||||
|
|
||||||
|
|
||||||
|
class GeKitchenConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||||
|
"""Handle a config flow for GE Kitchen."""
|
||||||
|
|
||||||
|
VERSION = 1
|
||||||
|
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_PUSH
|
||||||
|
|
||||||
|
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=GEKITCHEN_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=GEKITCHEN_SCHEMA, errors=errors,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class CannotConnect(exceptions.HomeAssistantError):
|
||||||
|
"""Error to indicate we cannot connect."""
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidAuth(exceptions.HomeAssistantError):
|
||||||
|
"""Error to indicate there is invalid auth."""
|
||||||
|
|
|
@ -1,7 +1,18 @@
|
||||||
"""Constants for the ge_kitchen integration."""
|
"""Constants for the ge_kitchen integration."""
|
||||||
|
from gekitchen.const import LOGIN_URL
|
||||||
|
|
||||||
DOMAIN = "ge_kitchen"
|
DOMAIN = "ge_kitchen"
|
||||||
|
|
||||||
# TODO Update with your own urls
|
# TODO Update with your own urls
|
||||||
OAUTH2_AUTHORIZE = "https://www.example.com/auth/authorize"
|
# OAUTH2_AUTHORIZE = f"{LOGIN_URL}/oauth2/auth"
|
||||||
OAUTH2_TOKEN = "https://www.example.com/auth/token"
|
OAUTH2_AUTH_URL = f"{LOGIN_URL}/oauth2/auth"
|
||||||
|
OAUTH2_TOKEN_URL = f"{LOGIN_URL}/oauth2/token"
|
||||||
|
|
||||||
|
AUTH_HANDLER = "auth_handler"
|
||||||
|
EVENT_ALL_APPLIANCES_READY = 'all_appliances_ready'
|
||||||
|
COORDINATOR = "coordinator"
|
||||||
|
GE_TOKEN = "ge_token"
|
||||||
|
MOBILE_DEVICE_TOKEN = "mdt"
|
||||||
|
XMPP_CREDENTIALS = "xmpp_credentials"
|
||||||
|
|
||||||
|
UPDATE_INTERVAL = 20
|
||||||
|
|
|
@ -1,16 +1,9 @@
|
||||||
{
|
{
|
||||||
"domain": "ge_kitchen",
|
"domain": "ge_kitchen",
|
||||||
"name": "ge_kitchen",
|
"name": "GE Kitchen",
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/ge_kitchen",
|
"documentation": "https://www.home-assistant.io/integrations/ge_kitchen",
|
||||||
"requirements": [
|
"requirements": ["gekitchen==0.1.8"],
|
||||||
"gekitchen==0.1.2"
|
|
||||||
],
|
|
||||||
"ssdp": [],
|
|
||||||
"zeroconf": [],
|
|
||||||
"homekit": {},
|
|
||||||
"dependencies": [],
|
"dependencies": [],
|
||||||
"codeowners": [
|
"codeowners": ["@ajmarks"]
|
||||||
"@ajmarks"
|
}
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,34 @@
|
||||||
|
"""GE Kitchen Sensor Entities"""
|
||||||
|
import async_timeout
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from typing import Callable
|
||||||
|
from .update_coordinator import GeKitchenUpdateCoordinator
|
||||||
|
from .appliance_api import GeSensor
|
||||||
|
from .const import DOMAIN
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable):
|
||||||
|
"""GE Kitchen sensors."""
|
||||||
|
_LOGGER.debug('Adding GE Kitchen sensors')
|
||||||
|
coordinator = hass.data[DOMAIN][config_entry.entry_id] # type: GeKitchenUpdateCoordinator
|
||||||
|
|
||||||
|
# This should be a NOP, but let's be safe
|
||||||
|
with async_timeout.timeout(20):
|
||||||
|
await coordinator.initialization_future
|
||||||
|
_LOGGER.debug('Coordinator init future finished')
|
||||||
|
|
||||||
|
apis = list(coordinator.appliance_apis.values())
|
||||||
|
_LOGGER.debug(f'Found {len(apis):d} appliance APIs')
|
||||||
|
entities = [
|
||||||
|
entity
|
||||||
|
for api in apis
|
||||||
|
for entity in api.entities
|
||||||
|
if isinstance(entity, GeSensor) and entity.erd_code in api.appliance._property_cache
|
||||||
|
]
|
||||||
|
_LOGGER.debug(f'Found {len(entities):d} sensors')
|
||||||
|
async_add_entities(entities)
|
|
@ -1,17 +1,27 @@
|
||||||
{
|
{
|
||||||
"title": "ge_kitchen",
|
"title": "GE Kitchen",
|
||||||
"config": {
|
"config": {
|
||||||
"step": {
|
"step": {
|
||||||
"pick_implementation": {
|
"init": {
|
||||||
"title": "[%key:common::config_flow::title::oauth2_pick_implementation%]"
|
"data": {
|
||||||
|
"username": "[%key:common::config_flow::data::username%]",
|
||||||
|
"password": "[%key:common::config_flow::data::password%]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"username": "[%key:common::config_flow::data::username%]",
|
||||||
|
"password": "[%key:common::config_flow::data::password%]"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"abort": {
|
"error": {
|
||||||
"missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]",
|
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||||
"authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]"
|
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||||
|
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||||
},
|
},
|
||||||
"create_entry": {
|
"abort": {
|
||||||
"default": "[%key:common::config_flow::create_entry::authenticated%]"
|
"already_configured_account": "[%key:common::config_flow::abort::already_configured_account%]"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,17 +1,27 @@
|
||||||
{
|
{
|
||||||
"config": {
|
"title": "GE Kitchen",
|
||||||
"abort": {
|
"config": {
|
||||||
"authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]",
|
"step": {
|
||||||
"missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]"
|
"init": {
|
||||||
},
|
"data": {
|
||||||
"create_entry": {
|
"username": "Username",
|
||||||
"default": "[%key:common::config_flow::create_entry::authenticated%]"
|
"password": "Password"
|
||||||
},
|
|
||||||
"step": {
|
|
||||||
"pick_implementation": {
|
|
||||||
"title": "[%key:common::config_flow::title::oauth2_pick_implementation%]"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"username": "Username",
|
||||||
|
"password": "Password"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"title": "ge_kitchen"
|
"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%]"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,150 @@
|
||||||
|
"""Data update coordinator for shark iq vacuums."""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
from typing import Any, Dict, Iterable, Optional, Tuple
|
||||||
|
|
||||||
|
from gekitchen import (
|
||||||
|
EVENT_APPLIANCE_STATE_CHANGE,
|
||||||
|
EVENT_APPLIANCE_INITIAL_UPDATE,
|
||||||
|
ErdCodeType,
|
||||||
|
GeAppliance,
|
||||||
|
GeClient
|
||||||
|
)
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||||
|
|
||||||
|
from .const import DOMAIN, EVENT_ALL_APPLIANCES_READY, UPDATE_INTERVAL
|
||||||
|
from .appliance_api import ApplianceApi, get_appliance_api_type
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class GeKitchenUpdateCoordinator(DataUpdateCoordinator):
|
||||||
|
"""Define a wrapper class to update Shark IQ data."""
|
||||||
|
|
||||||
|
def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry, xmpp_credentials: Dict) -> None:
|
||||||
|
"""Set up the SharkIqUpdateCoordinator class."""
|
||||||
|
self._hass = hass
|
||||||
|
self._config_entry = config_entry
|
||||||
|
self._xmpp_credentials = xmpp_credentials
|
||||||
|
self.client = None # type: Optional[GeClient]
|
||||||
|
self._appliance_apis = {} # type: Dict[str, ApplianceApi]
|
||||||
|
|
||||||
|
# Some record keeping to let us know when we can start generating entities
|
||||||
|
self._got_roster = True
|
||||||
|
self._init_done = False
|
||||||
|
self.initialization_future = asyncio.Future()
|
||||||
|
|
||||||
|
super().__init__(hass, _LOGGER, name=DOMAIN)
|
||||||
|
|
||||||
|
def create_ge_client(self, xmpp_credentials: Dict, event_loop: Optional[asyncio.AbstractEventLoop]) -> GeClient:
|
||||||
|
"""
|
||||||
|
Create a new GeClient object with some helfpull callbacks.
|
||||||
|
|
||||||
|
:param xmpp_credentials: XMPP credentials
|
||||||
|
:param event_loop: Event loop
|
||||||
|
:return: GeClient
|
||||||
|
"""
|
||||||
|
client = GeClient(xmpp_credentials, event_loop=event_loop)
|
||||||
|
client.ssl_context.set_ciphers('HIGH:!DH:!aNULL') # GE's XMPP server uses a weak cipher.
|
||||||
|
client.add_event_handler(EVENT_APPLIANCE_INITIAL_UPDATE, self.on_device_initial_update)
|
||||||
|
client.add_event_handler(EVENT_APPLIANCE_STATE_CHANGE, self.on_device_update)
|
||||||
|
client.add_event_handler('session_start', self.on_start)
|
||||||
|
client.add_event_handler('roster_update', self.on_roster_update)
|
||||||
|
client.use_ssl = True
|
||||||
|
return client
|
||||||
|
|
||||||
|
@property
|
||||||
|
def appliances(self) -> Iterable[GeAppliance]:
|
||||||
|
return self.client.appliances.values()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def appliance_apis(self) -> Dict[str, ApplianceApi]:
|
||||||
|
return self._appliance_apis
|
||||||
|
|
||||||
|
def _get_appliance_api(self, appliance: GeAppliance) -> ApplianceApi:
|
||||||
|
api_type = get_appliance_api_type(appliance.appliance_type)
|
||||||
|
return api_type(self.hass, appliance)
|
||||||
|
|
||||||
|
def regenerate_appliance_apis(self):
|
||||||
|
"""Regenerate the appliance_apis dictionary, adding elements as necessary."""
|
||||||
|
for jid, appliance in self.client.appliances.keys():
|
||||||
|
if jid not in self._appliance_apis:
|
||||||
|
self._appliance_apis[jid] = self._get_appliance_api(appliance)
|
||||||
|
|
||||||
|
def maybe_add_appliance_api(self, appliance: GeAppliance):
|
||||||
|
bare_jid = appliance.jid.bare
|
||||||
|
if bare_jid not in self.appliance_apis:
|
||||||
|
_LOGGER.debug(f"Adding appliance api for appliance {bare_jid} ({appliance.appliance_type})")
|
||||||
|
api = self._get_appliance_api(appliance)
|
||||||
|
api.build_entities_list()
|
||||||
|
self.appliance_apis[bare_jid] = api
|
||||||
|
|
||||||
|
def get_new_client(self, xmpp_credentials: Optional[Dict] = None):
|
||||||
|
if self.client is not None:
|
||||||
|
self.client.disconnect()
|
||||||
|
if xmpp_credentials is not None:
|
||||||
|
self._xmpp_credentials = xmpp_credentials
|
||||||
|
|
||||||
|
loop = self._hass.loop
|
||||||
|
self.client = self.create_ge_client(self._xmpp_credentials, event_loop=loop)
|
||||||
|
|
||||||
|
def start_client(self, xmpp_credentials: Optional[Dict] = None):
|
||||||
|
"""Start a new GeClient in the HASS event loop."""
|
||||||
|
_LOGGER.debug('Running client')
|
||||||
|
self.get_new_client(xmpp_credentials)
|
||||||
|
self.client.connect()
|
||||||
|
asyncio.ensure_future(self.client.process_in_running_loop(120), loop=self._hass.loop)
|
||||||
|
_LOGGER.debug('Client running')
|
||||||
|
|
||||||
|
async def on_start(self, _):
|
||||||
|
"""When we join, announce ourselves and request a roster update"""
|
||||||
|
self.client.send_presence()
|
||||||
|
self.client.get_roster()
|
||||||
|
|
||||||
|
async def on_device_update(self, data: Tuple[GeAppliance, Dict[ErdCodeType, Any]]):
|
||||||
|
"""Let HA know there's new state."""
|
||||||
|
appliance, _ = data
|
||||||
|
try:
|
||||||
|
api = self.appliance_apis[appliance.jid.bare]
|
||||||
|
except KeyError:
|
||||||
|
return
|
||||||
|
for entity in api.entities:
|
||||||
|
entity.async_write_ha_state()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def all_appliances_updated(self) -> bool:
|
||||||
|
"""True if all appliances have had an initial update."""
|
||||||
|
return all([a.initialized for a in self.appliances])
|
||||||
|
|
||||||
|
async def on_roster_update(self, _):
|
||||||
|
"""When there's a roster update, mark it and maybe trigger all ready."""
|
||||||
|
_LOGGER.debug('Got roster update')
|
||||||
|
self._got_roster = True
|
||||||
|
await self.async_maybe_trigger_all_ready()
|
||||||
|
|
||||||
|
async def on_device_initial_update(self, appliance: GeAppliance):
|
||||||
|
"""When an appliance first becomes ready, let the system know and schedule periodic updates."""
|
||||||
|
_LOGGER.debug(f'Got initial update for {appliance.jid}')
|
||||||
|
_LOGGER.debug(f'Known appliances: {", ".join(self.client.appliances.keys())}')
|
||||||
|
self.maybe_add_appliance_api(appliance)
|
||||||
|
await self.async_maybe_trigger_all_ready()
|
||||||
|
while self.client.is_connected() and appliance.available:
|
||||||
|
await asyncio.sleep(UPDATE_INTERVAL)
|
||||||
|
appliance.request_update()
|
||||||
|
|
||||||
|
async def async_maybe_trigger_all_ready(self):
|
||||||
|
"""See if we're all ready to go, and if so, let the games begin."""
|
||||||
|
if self._init_done or self.initialization_future.done():
|
||||||
|
# Been here, done this
|
||||||
|
return
|
||||||
|
if self._got_roster and self.all_appliances_updated:
|
||||||
|
_LOGGER.debug('Ready to go. Waiting 2 seconds and setting init future result.')
|
||||||
|
# The the flag and wait to prevent two different fun race conditions
|
||||||
|
self._init_done = True
|
||||||
|
await asyncio.sleep(2)
|
||||||
|
self.initialization_future.set_result(True)
|
||||||
|
self.client.event(EVENT_ALL_APPLIANCES_READY, None)
|
|
@ -18,8 +18,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
async def async_setup_entry(hass: "HomeAssistant", config_entry, async_add_entities: Callable):
|
async def async_setup_entry(hass: "HomeAssistant", config_entry, async_add_entities: Callable):
|
||||||
"""Set up the Shark IQ battery sensor"""
|
"""Set up the Shark IQ battery sensor"""
|
||||||
domain_data = hass.data[DOMAIN][config_entry.entry_id]
|
ayla_api = hass.data[DOMAIN][config_entry.entry_id] # type: AylaApi
|
||||||
ayla_api = domain_data[SHARKIQ_SESSION] # type: AylaApi
|
|
||||||
|
|
||||||
devices = await ayla_api.async_get_devices() # type: List[SharkIqVacuum]
|
devices = await ayla_api.async_get_devices() # type: List[SharkIqVacuum]
|
||||||
device_names = ', '.join([d.name for d in devices])
|
device_names = ', '.join([d.name for d in devices])
|
||||||
|
|
|
@ -50,7 +50,7 @@ class SharkIqUpdateCoordinator(DataUpdateCoordinator):
|
||||||
await sharkiq.async_update()
|
await sharkiq.async_update()
|
||||||
|
|
||||||
async def _async_update_data(self) -> bool:
|
async def _async_update_data(self) -> bool:
|
||||||
"""Update data via Awair client library."""
|
"""Update data device by device."""
|
||||||
try:
|
try:
|
||||||
all_vacuums = await self.ayla_api.async_list_devices()
|
all_vacuums = await self.ayla_api.async_list_devices()
|
||||||
self._online_dsns = {
|
self._online_dsns = {
|
||||||
|
|
Loading…
Reference in New Issue