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."""
|
||||
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(
|
||||
|
|
|
@ -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
|
||||
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."""
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"]
|
||||
}
|
||||
|
|
|
@ -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": {
|
||||
"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%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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%]"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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):
|
||||
"""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])
|
||||
|
|
|
@ -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 = {
|
||||
|
|
Loading…
Reference in New Issue