Basically working

This commit is contained in:
Andrew Marks 2020-08-13 17:10:55 -04:00
parent cf856c5f86
commit 5f8dafa623
13 changed files with 712 additions and 160 deletions

View File

@ -1,74 +1,60 @@
"""The ge_kitchen integration."""
import asyncio
import asyncio
import async_timeout
import logging
import voluptuous as vol
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET
from homeassistant.config_entries import ConfigEntry, SOURCE_IMPORT
from homeassistant.core import HomeAssistant
from homeassistant.helpers import (
aiohttp_client,
config_entry_oauth2_flow,
config_validation as cv,
from gekitchen.async_login_flow import async_do_full_login_flow
from . import auth_api, config_flow
from .const import (
AUTH_HANDLER,
COORDINATOR,
DOMAIN,
OAUTH2_AUTH_URL,
OAUTH2_TOKEN_URL,
)
from . import api, config_flow
from .const import DOMAIN, OAUTH2_AUTHORIZE, OAUTH2_TOKEN
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from .update_coordinator import GeKitchenUpdateCoordinator
CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: vol.Schema(
{
vol.Required(CONF_CLIENT_ID): cv.string,
vol.Required(CONF_CLIENT_SECRET): cv.string,
}
)
},
extra=vol.ALLOW_EXTRA,
)
CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA)
PLATFORMS = ["sensor"]
# TODO List the platforms that you want to support.
# For your initial PR, limit it to 1 platform.
PLATFORMS = ["light"]
_LOGGER = logging.getLogger(__name__)
async def async_setup(hass: HomeAssistant, config: dict):
"""Set up the ge_kitchen component."""
hass.data[DOMAIN] = {}
hass.data.setdefault(DOMAIN, {})
if DOMAIN not in config:
return True
config_flow.OAuth2FlowHandler.async_register_implementation(
hass,
config_entry_oauth2_flow.LocalOAuth2Implementation(
hass,
DOMAIN,
config[DOMAIN][CONF_CLIENT_ID],
config[DOMAIN][CONF_CLIENT_SECRET],
OAUTH2_AUTHORIZE,
OAUTH2_TOKEN,
),
)
for index, conf in enumerate(config[DOMAIN]):
_LOGGER.debug(
"Importing GE Kitchen Account #%d (Username: %s)", index, conf[CONF_USERNAME]
)
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_IMPORT}, data=conf,
)
)
return True
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Set up ge_kitchen from a config entry."""
implementation = await config_entry_oauth2_flow.async_get_config_entry_implementation(
hass, entry
session = hass.helpers.aiohttp_client.async_get_clientsession()
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)
# If using a requests-based API lib
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
)
coordinator.start_client()
with async_timeout.timeout(120):
await coordinator.initialization_future
for component in PLATFORMS:
hass.async_create_task(

View File

@ -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

303
ge_kitchen/appliance_api.py Normal file
View File

@ -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))

View File

@ -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)

View File

@ -1,24 +1,107 @@
"""Config flow for ge_kitchen."""
"""Config flow for GE Kitchen integration."""
import asyncio
import logging
from typing import Dict, Optional
from homeassistant import config_entries
from homeassistant.helpers import config_entry_oauth2_flow
import aiohttp
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__)
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
# TODO Pick one from config_entries.CONN_CLASS_*
CONNECTION_CLASS = config_entries.CONN_CLASS_UNKNOWN
async def validate_input(hass: core.HomeAssistant, data):
"""Validate the user input allows us to connect."""
@property
def logger(self) -> logging.Logger:
"""Return logger."""
return logging.getLogger(__name__)
session = hass.helpers.aiohttp_client.async_get_clientsession(hass)
# noinspection PyBroadException
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."""

View File

@ -1,7 +1,18 @@
"""Constants for the ge_kitchen integration."""
from gekitchen.const import LOGIN_URL
DOMAIN = "ge_kitchen"
# TODO Update with your own urls
OAUTH2_AUTHORIZE = "https://www.example.com/auth/authorize"
OAUTH2_TOKEN = "https://www.example.com/auth/token"
# OAUTH2_AUTHORIZE = f"{LOGIN_URL}/oauth2/auth"
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

View File

@ -1,16 +1,9 @@
{
"domain": "ge_kitchen",
"name": "ge_kitchen",
"name": "GE Kitchen",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/ge_kitchen",
"requirements": [
"gekitchen==0.1.2"
],
"ssdp": [],
"zeroconf": [],
"homekit": {},
"requirements": ["gekitchen==0.1.8"],
"dependencies": [],
"codeowners": [
"@ajmarks"
]
"codeowners": ["@ajmarks"]
}

34
ge_kitchen/sensor.py Normal file
View File

@ -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)

View File

@ -1,17 +1,27 @@
{
"title": "ge_kitchen",
"title": "GE Kitchen",
"config": {
"step": {
"pick_implementation": {
"title": "[%key:common::config_flow::title::oauth2_pick_implementation%]"
"init": {
"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": {
"missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]",
"authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]"
"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%]"
},
"create_entry": {
"default": "[%key:common::config_flow::create_entry::authenticated%]"
"abort": {
"already_configured_account": "[%key:common::config_flow::abort::already_configured_account%]"
}
}
}

View File

@ -1,17 +1,27 @@
{
"config": {
"abort": {
"authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]",
"missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]"
},
"create_entry": {
"default": "[%key:common::config_flow::create_entry::authenticated%]"
},
"step": {
"pick_implementation": {
"title": "[%key:common::config_flow::title::oauth2_pick_implementation%]"
}
"title": "GE Kitchen",
"config": {
"step": {
"init": {
"data": {
"username": "Username",
"password": "Password"
}
},
"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%]"
}
}
}

View File

@ -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)

View File

@ -18,8 +18,7 @@ _LOGGER = logging.getLogger(__name__)
async def async_setup_entry(hass: "HomeAssistant", config_entry, async_add_entities: Callable):
"""Set up the Shark IQ battery sensor"""
domain_data = hass.data[DOMAIN][config_entry.entry_id]
ayla_api = domain_data[SHARKIQ_SESSION] # type: AylaApi
ayla_api = hass.data[DOMAIN][config_entry.entry_id] # type: AylaApi
devices = await ayla_api.async_get_devices() # type: List[SharkIqVacuum]
device_names = ', '.join([d.name for d in devices])

View File

@ -50,7 +50,7 @@ class SharkIqUpdateCoordinator(DataUpdateCoordinator):
await sharkiq.async_update()
async def _async_update_data(self) -> bool:
"""Update data via Awair client library."""
"""Update data device by device."""
try:
all_vacuums = await self.ayla_api.async_list_devices()
self._online_dsns = {