Add Hive config flow (#47300)

* Add Hive UI

* Fix tests and review updates

* Slimmed down config_flow

* Fix tests

* Updated Services.yaml with extra ui attributes

* cleanup config flow

* Update config entry

* Remove ATTR_AVAILABLE

* Fix Re-Auth  Test

* Added more tests.

* Update tests
This commit is contained in:
Khole 2021-03-15 11:27:10 +00:00 committed by GitHub
parent 1aa4fd4cc9
commit cfeb8eb06a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 1165 additions and 201 deletions

View File

@ -386,7 +386,13 @@ omit =
homeassistant/components/hikvisioncam/switch.py homeassistant/components/hikvisioncam/switch.py
homeassistant/components/hisense_aehw4a1/* homeassistant/components/hisense_aehw4a1/*
homeassistant/components/hitron_coda/device_tracker.py homeassistant/components/hitron_coda/device_tracker.py
homeassistant/components/hive/* homeassistant/components/hive/__init__.py
homeassistant/components/hive/climate.py
homeassistant/components/hive/binary_sensor.py
homeassistant/components/hive/light.py
homeassistant/components/hive/sensor.py
homeassistant/components/hive/switch.py
homeassistant/components/hive/water_heater.py
homeassistant/components/hlk_sw16/__init__.py homeassistant/components/hlk_sw16/__init__.py
homeassistant/components/hlk_sw16/switch.py homeassistant/components/hlk_sw16/switch.py
homeassistant/components/home_connect/* homeassistant/components/home_connect/*

View File

@ -1,43 +1,26 @@
"""Support for the Hive devices and services.""" """Support for the Hive devices and services."""
import asyncio
from functools import wraps from functools import wraps
import logging import logging
from pyhiveapi import Hive from aiohttp.web_exceptions import HTTPException
from apyhiveapi import Hive
from apyhiveapi.helper.hive_exceptions import HiveReauthRequired
import voluptuous as vol import voluptuous as vol
from homeassistant.const import ( from homeassistant import config_entries
ATTR_ENTITY_ID, from homeassistant.const import CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_USERNAME
ATTR_TEMPERATURE, from homeassistant.exceptions import ConfigEntryNotReady
CONF_PASSWORD, from homeassistant.helpers import aiohttp_client, config_validation as cv
CONF_SCAN_INTERVAL,
CONF_USERNAME,
)
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.discovery import async_load_platform
from homeassistant.helpers.dispatcher import ( from homeassistant.helpers.dispatcher import (
async_dispatcher_connect, async_dispatcher_connect,
async_dispatcher_send, async_dispatcher_send,
) )
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__) from .const import DOMAIN, PLATFORM_LOOKUP, PLATFORMS
ATTR_AVAILABLE = "available" _LOGGER = logging.getLogger(__name__)
DOMAIN = "hive"
DATA_HIVE = "data_hive"
SERVICES = ["Heating", "HotWater", "TRV"]
SERVICE_BOOST_HOT_WATER = "boost_hot_water"
SERVICE_BOOST_HEATING = "boost_heating"
ATTR_TIME_PERIOD = "time_period"
ATTR_MODE = "on_off"
DEVICETYPES = {
"binary_sensor": "device_list_binary_sensor",
"climate": "device_list_climate",
"water_heater": "device_list_water_heater",
"light": "device_list_light",
"switch": "device_list_plug",
"sensor": "device_list_sensor",
}
CONFIG_SCHEMA = vol.Schema( CONFIG_SCHEMA = vol.Schema(
{ {
@ -52,101 +35,88 @@ CONFIG_SCHEMA = vol.Schema(
extra=vol.ALLOW_EXTRA, extra=vol.ALLOW_EXTRA,
) )
BOOST_HEATING_SCHEMA = vol.Schema(
{
vol.Required(ATTR_ENTITY_ID): cv.entity_id,
vol.Required(ATTR_TIME_PERIOD): vol.All(
cv.time_period, cv.positive_timedelta, lambda td: td.total_seconds() // 60
),
vol.Optional(ATTR_TEMPERATURE, default="25.0"): vol.Coerce(float),
}
)
BOOST_HOT_WATER_SCHEMA = vol.Schema(
{
vol.Required(ATTR_ENTITY_ID): cv.entity_id,
vol.Optional(ATTR_TIME_PERIOD, default="00:30:00"): vol.All(
cv.time_period, cv.positive_timedelta, lambda td: td.total_seconds() // 60
),
vol.Required(ATTR_MODE): cv.string,
}
)
async def async_setup(hass, config): async def async_setup(hass, config):
"""Set up the Hive Component.""" """Hive configuration setup."""
hass.data[DOMAIN] = {}
async def heating_boost(service): if DOMAIN not in config:
"""Handle the service call.""" return True
entity_lookup = hass.data[DOMAIN]["entity_lookup"] conf = config[DOMAIN]
hive_id = entity_lookup.get(service.data[ATTR_ENTITY_ID])
if not hive_id:
# log or raise error
_LOGGER.error("Cannot boost entity id entered")
return
minutes = service.data[ATTR_TIME_PERIOD] if not hass.config_entries.async_entries(DOMAIN):
temperature = service.data[ATTR_TEMPERATURE] hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_IMPORT},
data={
CONF_USERNAME: conf[CONF_USERNAME],
CONF_PASSWORD: conf[CONF_PASSWORD],
},
)
)
return True
hive.heating.turn_boost_on(hive_id, minutes, temperature)
async def hot_water_boost(service): async def async_setup_entry(hass, entry):
"""Handle the service call.""" """Set up Hive from a config entry."""
entity_lookup = hass.data[DOMAIN]["entity_lookup"]
hive_id = entity_lookup.get(service.data[ATTR_ENTITY_ID])
if not hive_id:
# log or raise error
_LOGGER.error("Cannot boost entity id entered")
return
minutes = service.data[ATTR_TIME_PERIOD]
mode = service.data[ATTR_MODE]
if mode == "on": websession = aiohttp_client.async_get_clientsession(hass)
hive.hotwater.turn_boost_on(hive_id, minutes) hive = Hive(websession)
elif mode == "off": hive_config = dict(entry.data)
hive.hotwater.turn_boost_off(hive_id)
hive = Hive() hive_config["options"] = {}
hive_config["options"].update(
{CONF_SCAN_INTERVAL: dict(entry.options).get(CONF_SCAN_INTERVAL, 120)}
)
hass.data[DOMAIN][entry.entry_id] = hive
config = {} try:
config["username"] = config[DOMAIN][CONF_USERNAME] devices = await hive.session.startSession(hive_config)
config["password"] = config[DOMAIN][CONF_PASSWORD] except HTTPException as error:
config["update_interval"] = config[DOMAIN][CONF_SCAN_INTERVAL] _LOGGER.error("Could not connect to the internet: %s", error)
raise ConfigEntryNotReady() from error
devices = await hive.session.startSession(config) except HiveReauthRequired:
hass.async_create_task(
if devices is None: hass.config_entries.flow.async_init(
_LOGGER.error("Hive API initialization failed") DOMAIN,
context={
"source": config_entries.SOURCE_REAUTH,
"unique_id": entry.unique_id,
},
data=entry.data,
)
)
return False return False
hass.data[DOMAIN][DATA_HIVE] = hive for ha_type, hive_type in PLATFORM_LOOKUP.items():
hass.data[DOMAIN]["entity_lookup"] = {} device_list = devices.get(hive_type)
if device_list:
for ha_type in DEVICETYPES:
devicelist = devices.get(DEVICETYPES[ha_type])
if devicelist:
hass.async_create_task( hass.async_create_task(
async_load_platform(hass, ha_type, DOMAIN, devicelist, config) hass.config_entries.async_forward_entry_setup(entry, ha_type)
)
if ha_type == "climate":
hass.services.async_register(
DOMAIN,
SERVICE_BOOST_HEATING,
heating_boost,
schema=BOOST_HEATING_SCHEMA,
)
if ha_type == "water_heater":
hass.services.async_register(
DOMAIN,
SERVICE_BOOST_HOT_WATER,
hot_water_boost,
schema=BOOST_HOT_WATER_SCHEMA,
) )
return True return True
async def async_unload_entry(hass, entry):
"""Unload a config entry."""
unload_ok = all(
await asyncio.gather(
*[
hass.config_entries.async_forward_entry_unload(entry, component)
for component in PLATFORMS
]
)
)
if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok
def refresh_system(func): def refresh_system(func):
"""Force update all entities after state change.""" """Force update all entities after state change."""
@ -173,6 +143,3 @@ class HiveEntity(Entity):
self.async_on_remove( self.async_on_remove(
async_dispatcher_connect(self.hass, DOMAIN, self.async_write_ha_state) async_dispatcher_connect(self.hass, DOMAIN, self.async_write_ha_state)
) )
if self.device["hiveType"] in SERVICES:
entity_lookup = self.hass.data[DOMAIN]["entity_lookup"]
entity_lookup[self.entity_id] = self.device["hiveID"]

View File

@ -10,7 +10,8 @@ from homeassistant.components.binary_sensor import (
BinarySensorEntity, BinarySensorEntity,
) )
from . import ATTR_AVAILABLE, ATTR_MODE, DATA_HIVE, DOMAIN, HiveEntity from . import HiveEntity
from .const import ATTR_MODE, DOMAIN
DEVICETYPE = { DEVICETYPE = {
"contactsensor": DEVICE_CLASS_OPENING, "contactsensor": DEVICE_CLASS_OPENING,
@ -24,13 +25,11 @@ PARALLEL_UPDATES = 0
SCAN_INTERVAL = timedelta(seconds=15) SCAN_INTERVAL = timedelta(seconds=15)
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): async def async_setup_entry(hass, entry, async_add_entities):
"""Set up the Hive Binary Sensor.""" """Set up Hive thermostat based on a config entry."""
if discovery_info is None:
return
hive = hass.data[DOMAIN].get(DATA_HIVE) hive = hass.data[DOMAIN][entry.entry_id]
devices = hive.devices.get("binary_sensor") devices = hive.session.deviceList.get("binary_sensor")
entities = [] entities = []
if devices: if devices:
for dev in devices: for dev in devices:
@ -49,7 +48,14 @@ class HiveBinarySensorEntity(HiveEntity, BinarySensorEntity):
@property @property
def device_info(self): def device_info(self):
"""Return device information.""" """Return device information."""
return {"identifiers": {(DOMAIN, self.unique_id)}, "name": self.name} return {
"identifiers": {(DOMAIN, self.device["device_id"])},
"name": self.device["device_name"],
"model": self.device["deviceData"]["model"],
"manufacturer": self.device["deviceData"]["manufacturer"],
"sw_version": self.device["deviceData"]["version"],
"via_device": (DOMAIN, self.device["parentDevice"]),
}
@property @property
def device_class(self): def device_class(self):
@ -72,7 +78,6 @@ class HiveBinarySensorEntity(HiveEntity, BinarySensorEntity):
def extra_state_attributes(self): def extra_state_attributes(self):
"""Show Device Attributes.""" """Show Device Attributes."""
return { return {
ATTR_AVAILABLE: self.attributes.get(ATTR_AVAILABLE),
ATTR_MODE: self.attributes.get(ATTR_MODE), ATTR_MODE: self.attributes.get(ATTR_MODE),
} }
@ -84,5 +89,5 @@ class HiveBinarySensorEntity(HiveEntity, BinarySensorEntity):
async def async_update(self): async def async_update(self):
"""Update all Node data from Hive.""" """Update all Node data from Hive."""
await self.hive.session.updateData(self.device) await self.hive.session.updateData(self.device)
self.device = await self.hive.sensor.get_sensor(self.device) self.device = await self.hive.sensor.getSensor(self.device)
self.attributes = self.device.get("attributes", {}) self.attributes = self.device.get("attributes", {})

View File

@ -1,6 +1,8 @@
"""Support for the Hive climate devices.""" """Support for the Hive climate devices."""
from datetime import timedelta from datetime import timedelta
import voluptuous as vol
from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate import ClimateEntity
from homeassistant.components.climate.const import ( from homeassistant.components.climate.const import (
CURRENT_HVAC_HEAT, CURRENT_HVAC_HEAT,
@ -15,8 +17,10 @@ from homeassistant.components.climate.const import (
SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE,
) )
from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT
from homeassistant.helpers import config_validation as cv, entity_platform
from . import ATTR_AVAILABLE, DATA_HIVE, DOMAIN, HiveEntity, refresh_system from . import HiveEntity, refresh_system
from .const import ATTR_TIME_PERIOD, DOMAIN, SERVICE_BOOST_HEATING
HIVE_TO_HASS_STATE = { HIVE_TO_HASS_STATE = {
"SCHEDULE": HVAC_MODE_AUTO, "SCHEDULE": HVAC_MODE_AUTO,
@ -45,19 +49,32 @@ PARALLEL_UPDATES = 0
SCAN_INTERVAL = timedelta(seconds=15) SCAN_INTERVAL = timedelta(seconds=15)
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): async def async_setup_entry(hass, entry, async_add_entities):
"""Set up the Hive thermostat.""" """Set up Hive thermostat based on a config entry."""
if discovery_info is None:
return
hive = hass.data[DOMAIN].get(DATA_HIVE) hive = hass.data[DOMAIN][entry.entry_id]
devices = hive.devices.get("climate") devices = hive.session.deviceList.get("climate")
entities = [] entities = []
if devices: if devices:
for dev in devices: for dev in devices:
entities.append(HiveClimateEntity(hive, dev)) entities.append(HiveClimateEntity(hive, dev))
async_add_entities(entities, True) async_add_entities(entities, True)
platform = entity_platform.current_platform.get()
platform.async_register_entity_service(
SERVICE_BOOST_HEATING,
{
vol.Required(ATTR_TIME_PERIOD): vol.All(
cv.time_period,
cv.positive_timedelta,
lambda td: td.total_seconds() // 60,
),
vol.Optional(ATTR_TEMPERATURE, default="25.0"): vol.Coerce(float),
},
"async_heating_boost",
)
class HiveClimateEntity(HiveEntity, ClimateEntity): class HiveClimateEntity(HiveEntity, ClimateEntity):
"""Hive Climate Device.""" """Hive Climate Device."""
@ -76,7 +93,14 @@ class HiveClimateEntity(HiveEntity, ClimateEntity):
@property @property
def device_info(self): def device_info(self):
"""Return device information.""" """Return device information."""
return {"identifiers": {(DOMAIN, self.unique_id)}, "name": self.name} return {
"identifiers": {(DOMAIN, self.device["device_id"])},
"name": self.device["device_name"],
"model": self.device["deviceData"]["model"],
"manufacturer": self.device["deviceData"]["manufacturer"],
"sw_version": self.device["deviceData"]["version"],
"via_device": (DOMAIN, self.device["parentDevice"]),
}
@property @property
def supported_features(self): def supported_features(self):
@ -93,11 +117,6 @@ class HiveClimateEntity(HiveEntity, ClimateEntity):
"""Return if the device is available.""" """Return if the device is available."""
return self.device["deviceData"]["online"] return self.device["deviceData"]["online"]
@property
def extra_state_attributes(self):
"""Show Device Attributes."""
return {ATTR_AVAILABLE: self.attributes.get(ATTR_AVAILABLE)}
@property @property
def hvac_modes(self): def hvac_modes(self):
"""Return the list of available hvac operation modes. """Return the list of available hvac operation modes.
@ -160,27 +179,31 @@ class HiveClimateEntity(HiveEntity, ClimateEntity):
async def async_set_hvac_mode(self, hvac_mode): async def async_set_hvac_mode(self, hvac_mode):
"""Set new target hvac mode.""" """Set new target hvac mode."""
new_mode = HASS_TO_HIVE_STATE[hvac_mode] new_mode = HASS_TO_HIVE_STATE[hvac_mode]
await self.hive.heating.set_mode(self.device, new_mode) await self.hive.heating.setMode(self.device, new_mode)
@refresh_system @refresh_system
async def async_set_temperature(self, **kwargs): async def async_set_temperature(self, **kwargs):
"""Set new target temperature.""" """Set new target temperature."""
new_temperature = kwargs.get(ATTR_TEMPERATURE) new_temperature = kwargs.get(ATTR_TEMPERATURE)
if new_temperature is not None: if new_temperature is not None:
await self.hive.heating.set_target_temperature(self.device, new_temperature) await self.hive.heating.setTargetTemperature(self.device, new_temperature)
@refresh_system @refresh_system
async def async_set_preset_mode(self, preset_mode): async def async_set_preset_mode(self, preset_mode):
"""Set new preset mode.""" """Set new preset mode."""
if preset_mode == PRESET_NONE and self.preset_mode == PRESET_BOOST: if preset_mode == PRESET_NONE and self.preset_mode == PRESET_BOOST:
await self.hive.heating.turn_boost_off(self.device) await self.hive.heating.turnBoostOff(self.device)
elif preset_mode == PRESET_BOOST: elif preset_mode == PRESET_BOOST:
curtemp = round(self.current_temperature * 2) / 2 curtemp = round(self.current_temperature * 2) / 2
temperature = curtemp + 0.5 temperature = curtemp + 0.5
await self.hive.heating.turn_boost_on(self.device, 30, temperature) await self.hive.heating.turnBoostOn(self.device, 30, temperature)
@refresh_system
async def async_heating_boost(self, time_period, temperature):
"""Handle boost heating service call."""
await self.hive.heating.turnBoostOn(self.device, time_period, temperature)
async def async_update(self): async def async_update(self):
"""Update all Node data from Hive.""" """Update all Node data from Hive."""
await self.hive.session.updateData(self.device) await self.hive.session.updateData(self.device)
self.device = await self.hive.heating.get_heating(self.device) self.device = await self.hive.heating.getHeating(self.device)
self.attributes.update(self.device.get("attributes", {}))

View File

@ -0,0 +1,171 @@
"""Config Flow for Hive."""
from apyhiveapi import Auth
from apyhiveapi.helper.hive_exceptions import (
HiveApiError,
HiveInvalid2FACode,
HiveInvalidPassword,
HiveInvalidUsername,
)
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.const import CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_USERNAME
from homeassistant.core import callback
from .const import ( # pylint:disable=unused-import
CONF_CODE,
CONFIG_ENTRY_VERSION,
DOMAIN,
)
class HiveFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a Hive config flow."""
VERSION = CONFIG_ENTRY_VERSION
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
def __init__(self):
"""Initialize the config flow."""
self.hive_auth = None
self.data = {}
self.tokens = {}
self.entry = None
async def async_step_user(self, user_input=None):
"""Prompt user input. Create or edit entry."""
errors = {}
# Login to Hive with user data.
if user_input is not None:
self.data.update(user_input)
self.hive_auth = Auth(
username=self.data[CONF_USERNAME], password=self.data[CONF_PASSWORD]
)
# Get user from existing entry and abort if already setup
self.entry = await self.async_set_unique_id(self.data[CONF_USERNAME])
if self.context["source"] != config_entries.SOURCE_REAUTH:
self._abort_if_unique_id_configured()
# Login to the Hive.
try:
self.tokens = await self.hive_auth.login()
except HiveInvalidUsername:
errors["base"] = "invalid_username"
except HiveInvalidPassword:
errors["base"] = "invalid_password"
except HiveApiError:
errors["base"] = "no_internet_available"
if self.tokens.get("ChallengeName") == "SMS_MFA":
# Complete SMS 2FA.
return await self.async_step_2fa()
if not errors:
# Complete the entry setup.
try:
return await self.async_setup_hive_entry()
except UnknownHiveError:
errors["base"] = "unknown"
# Show User Input form.
schema = vol.Schema(
{vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str}
)
return self.async_show_form(step_id="user", data_schema=schema, errors=errors)
async def async_step_2fa(self, user_input=None):
"""Handle 2fa step."""
errors = {}
if user_input and user_input["2fa"] == "0000":
self.tokens = await self.hive_auth.login()
elif user_input:
try:
self.tokens = await self.hive_auth.sms_2fa(
user_input["2fa"], self.tokens
)
except HiveInvalid2FACode:
errors["base"] = "invalid_code"
except HiveApiError:
errors["base"] = "no_internet_available"
if not errors:
try:
return await self.async_setup_hive_entry()
except UnknownHiveError:
errors["base"] = "unknown"
schema = vol.Schema({vol.Required(CONF_CODE): str})
return self.async_show_form(step_id="2fa", data_schema=schema, errors=errors)
async def async_setup_hive_entry(self):
"""Finish setup and create the config entry."""
if "AuthenticationResult" not in self.tokens:
raise UnknownHiveError
# Setup the config entry
self.data["tokens"] = self.tokens
if self.context["source"] == config_entries.SOURCE_REAUTH:
self.hass.config_entries.async_update_entry(
self.entry, title=self.data["username"], data=self.data
)
await self.hass.config_entries.async_reload(self.entry.entry_id)
return self.async_abort(reason="reauth_successful")
return self.async_create_entry(title=self.data["username"], data=self.data)
async def async_step_reauth(self, user_input=None):
"""Re Authenticate a user."""
data = {
CONF_USERNAME: user_input[CONF_USERNAME],
CONF_PASSWORD: user_input[CONF_PASSWORD],
}
return await self.async_step_user(data)
async def async_step_import(self, user_input=None):
"""Import user."""
return await self.async_step_user(user_input)
@staticmethod
@callback
def async_get_options_flow(config_entry):
"""Hive options callback."""
return HiveOptionsFlowHandler(config_entry)
class HiveOptionsFlowHandler(config_entries.OptionsFlow):
"""Config flow options for Hive."""
def __init__(self, config_entry):
"""Initialize Hive options flow."""
self.hive = None
self.config_entry = config_entry
self.interval = config_entry.options.get(CONF_SCAN_INTERVAL, 120)
async def async_step_init(self, user_input=None):
"""Manage the options."""
return await self.async_step_user()
async def async_step_user(self, user_input=None):
"""Handle a flow initialized by the user."""
self.hive = self.hass.data["hive"][self.config_entry.entry_id]
errors = {}
if user_input is not None:
new_interval = user_input.get(CONF_SCAN_INTERVAL)
await self.hive.updateInterval(new_interval)
return self.async_create_entry(title="", data=user_input)
schema = vol.Schema(
{
vol.Optional(CONF_SCAN_INTERVAL, default=self.interval): vol.All(
vol.Coerce(int), vol.Range(min=30)
)
}
)
return self.async_show_form(step_id="user", data_schema=schema, errors=errors)
class UnknownHiveError(Exception):
"""Catch unknown hive error."""

View File

@ -0,0 +1,20 @@
"""Constants for Hive."""
ATTR_MODE = "mode"
ATTR_TIME_PERIOD = "time_period"
ATTR_ONOFF = "on_off"
CONF_CODE = "2fa"
CONFIG_ENTRY_VERSION = 1
DEFAULT_NAME = "Hive"
DOMAIN = "hive"
PLATFORMS = ["binary_sensor", "climate", "light", "sensor", "switch", "water_heater"]
PLATFORM_LOOKUP = {
"binary_sensor": "binary_sensor",
"climate": "climate",
"light": "light",
"sensor": "sensor",
"switch": "switch",
"water_heater": "water_heater",
}
SERVICE_BOOST_HOT_WATER = "boost_hot_water"
SERVICE_BOOST_HEATING = "boost_heating"
WATER_HEATER_MODES = ["on", "off"]

View File

@ -12,19 +12,18 @@ from homeassistant.components.light import (
) )
import homeassistant.util.color as color_util import homeassistant.util.color as color_util
from . import ATTR_AVAILABLE, ATTR_MODE, DATA_HIVE, DOMAIN, HiveEntity, refresh_system from . import HiveEntity, refresh_system
from .const import ATTR_MODE, DOMAIN
PARALLEL_UPDATES = 0 PARALLEL_UPDATES = 0
SCAN_INTERVAL = timedelta(seconds=15) SCAN_INTERVAL = timedelta(seconds=15)
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): async def async_setup_entry(hass, entry, async_add_entities):
"""Set up the Hive Light.""" """Set up Hive thermostat based on a config entry."""
if discovery_info is None:
return
hive = hass.data[DOMAIN].get(DATA_HIVE) hive = hass.data[DOMAIN][entry.entry_id]
devices = hive.devices.get("light") devices = hive.session.deviceList.get("light")
entities = [] entities = []
if devices: if devices:
for dev in devices: for dev in devices:
@ -43,7 +42,14 @@ class HiveDeviceLight(HiveEntity, LightEntity):
@property @property
def device_info(self): def device_info(self):
"""Return device information.""" """Return device information."""
return {"identifiers": {(DOMAIN, self.unique_id)}, "name": self.name} return {
"identifiers": {(DOMAIN, self.device["device_id"])},
"name": self.device["device_name"],
"model": self.device["deviceData"]["model"],
"manufacturer": self.device["deviceData"]["manufacturer"],
"sw_version": self.device["deviceData"]["version"],
"via_device": (DOMAIN, self.device["parentDevice"]),
}
@property @property
def name(self): def name(self):
@ -59,7 +65,6 @@ class HiveDeviceLight(HiveEntity, LightEntity):
def extra_state_attributes(self): def extra_state_attributes(self):
"""Show Device Attributes.""" """Show Device Attributes."""
return { return {
ATTR_AVAILABLE: self.attributes.get(ATTR_AVAILABLE),
ATTR_MODE: self.attributes.get(ATTR_MODE), ATTR_MODE: self.attributes.get(ATTR_MODE),
} }
@ -117,14 +122,14 @@ class HiveDeviceLight(HiveEntity, LightEntity):
saturation = int(get_new_color[1]) saturation = int(get_new_color[1])
new_color = (hue, saturation, 100) new_color = (hue, saturation, 100)
await self.hive.light.turn_on( await self.hive.light.turnOn(
self.device, new_brightness, new_color_temp, new_color self.device, new_brightness, new_color_temp, new_color
) )
@refresh_system @refresh_system
async def async_turn_off(self, **kwargs): async def async_turn_off(self, **kwargs):
"""Instruct the light to turn off.""" """Instruct the light to turn off."""
await self.hive.light.turn_off(self.device) await self.hive.light.turnOff(self.device)
@property @property
def supported_features(self): def supported_features(self):
@ -142,5 +147,5 @@ class HiveDeviceLight(HiveEntity, LightEntity):
async def async_update(self): async def async_update(self):
"""Update all Node data from Hive.""" """Update all Node data from Hive."""
await self.hive.session.updateData(self.device) await self.hive.session.updateData(self.device)
self.device = await self.hive.light.get_light(self.device) self.device = await self.hive.light.getLight(self.device)
self.attributes.update(self.device.get("attributes", {})) self.attributes.update(self.device.get("attributes", {}))

View File

@ -1,9 +1,10 @@
{ {
"domain": "hive", "domain": "hive",
"name": "Hive", "name": "Hive",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/hive", "documentation": "https://www.home-assistant.io/integrations/hive",
"requirements": [ "requirements": [
"pyhiveapi==0.3.4.4" "pyhiveapi==0.3.9"
], ],
"codeowners": [ "codeowners": [
"@Rendili", "@Rendili",

View File

@ -5,7 +5,8 @@ from datetime import timedelta
from homeassistant.components.sensor import DEVICE_CLASS_BATTERY from homeassistant.components.sensor import DEVICE_CLASS_BATTERY
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from . import ATTR_AVAILABLE, DATA_HIVE, DOMAIN, HiveEntity from . import HiveEntity
from .const import DOMAIN
PARALLEL_UPDATES = 0 PARALLEL_UPDATES = 0
SCAN_INTERVAL = timedelta(seconds=15) SCAN_INTERVAL = timedelta(seconds=15)
@ -14,17 +15,14 @@ DEVICETYPE = {
} }
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): async def async_setup_entry(hass, entry, async_add_entities):
"""Set up the Hive Sensor.""" """Set up Hive thermostat based on a config entry."""
if discovery_info is None:
return
hive = hass.data[DOMAIN].get(DATA_HIVE) hive = hass.data[DOMAIN][entry.entry_id]
devices = hive.devices.get("sensor") devices = hive.session.deviceList.get("sensor")
entities = [] entities = []
if devices: if devices:
for dev in devices: for dev in devices:
if dev["hiveType"] in DEVICETYPE:
entities.append(HiveSensorEntity(hive, dev)) entities.append(HiveSensorEntity(hive, dev))
async_add_entities(entities, True) async_add_entities(entities, True)
@ -40,7 +38,14 @@ class HiveSensorEntity(HiveEntity, Entity):
@property @property
def device_info(self): def device_info(self):
"""Return device information.""" """Return device information."""
return {"identifiers": {(DOMAIN, self.unique_id)}, "name": self.name} return {
"identifiers": {(DOMAIN, self.device["device_id"])},
"name": self.device["device_name"],
"model": self.device["deviceData"]["model"],
"manufacturer": self.device["deviceData"]["manufacturer"],
"sw_version": self.device["deviceData"]["version"],
"via_device": (DOMAIN, self.device["parentDevice"]),
}
@property @property
def available(self): def available(self):
@ -67,12 +72,7 @@ class HiveSensorEntity(HiveEntity, Entity):
"""Return the state of the sensor.""" """Return the state of the sensor."""
return self.device["status"]["state"] return self.device["status"]["state"]
@property
def extra_state_attributes(self):
"""Return the state attributes."""
return {ATTR_AVAILABLE: self.attributes.get(ATTR_AVAILABLE)}
async def async_update(self): async def async_update(self):
"""Update all Node data from Hive.""" """Update all Node data from Hive."""
await self.hive.session.updateData(self.device) await self.hive.session.updateData(self.device)
self.device = await self.hive.sensor.get_sensor(self.device) self.device = await self.hive.sensor.getSensor(self.device)

View File

@ -1,24 +1,62 @@
boost_heating: boost_heating:
name: Boost Heating
description: Set the boost mode ON defining the period of time and the desired target temperature for the boost. description: Set the boost mode ON defining the period of time and the desired target temperature for the boost.
fields: fields:
entity_id: entity_id:
description: Enter the entity_id for the device required to set the boost mode. name: Entity ID
example: "climate.heating" description: Select entity_id to boost.
required: true
example: climate.heating
selector:
entity:
integration: hive
domain: climate
time_period: time_period:
name: Time Period
description: Set the time period for the boost. description: Set the time period for the boost.
example: "01:30:00" required: true
example: 01:30:00
selector:
time:
temperature: temperature:
name: Temperature
description: Set the target temperature for the boost period. description: Set the target temperature for the boost period.
example: "20.5" required: true
example: 20.5
selector:
number:
min: 7
max: 35
step: 0.5
unit_of_measurement: degrees
mode: slider
boost_hot_water: boost_hot_water:
description: "Set the boost mode ON or OFF defining the period of time for the boost." name: Boost Hotwater
description: Set the boost mode ON or OFF defining the period of time for the boost.
fields: fields:
entity_id: entity_id:
description: Enter the entity_id for the device reuired to set the boost mode. name: Entity ID
example: "water_heater.hot_water" description: Select entity_id to boost.
required: true
example: water_heater.hot_water
selector:
entity:
integration: hive
domain: water_heater
time_period: time_period:
name: Time Period
description: Set the time period for the boost. description: Set the time period for the boost.
example: "01:30:00" required: true
example: 01:30:00
selector:
time:
on_off: on_off:
name: Mode
description: Set the boost function on or off. description: Set the boost function on or off.
required: true
example: "on" example: "on"
selector:
select:
options:
- "on"
- "off"

View File

@ -0,0 +1,53 @@
{
"config": {
"step": {
"user": {
"title": "Hive Login",
"description": "Enter your Hive login information and configuration.",
"data": {
"username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]",
"scan_interval": "Scan Interval (seconds)"
}
},
"2fa": {
"title": "Hive Two-factor Authentication.",
"description": "Enter your Hive authentication code. \n \n Please enter code 0000 to request another code.",
"data": {
"2fa": "Two-factor code"
}
},
"reauth": {
"title": "Hive Login",
"description": "Re-enter your Hive login information.",
"data": {
"username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]"
}
}
},
"error": {
"invalid_username": "Failed to sign into Hive. Your email address is not recognised.",
"invalid_password": "Failed to sign into Hive. Incorrect password please try again.",
"invalid_code": "Failed to sign into Hive. Your two-factor authentication code was incorrect.",
"no_internet_available": "An internet connection is required to connect to Hive.",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
"unknown_entry": "Unable to find existing entry.",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
}
},
"options": {
"step": {
"user": {
"title": "Options for Hive",
"description": "Update the scan interval to poll for data more often.",
"data": {
"scan_interval": "Scan Interval (seconds)"
}
}
}
}
}

View File

@ -3,19 +3,18 @@ from datetime import timedelta
from homeassistant.components.switch import SwitchEntity from homeassistant.components.switch import SwitchEntity
from . import ATTR_AVAILABLE, ATTR_MODE, DATA_HIVE, DOMAIN, HiveEntity, refresh_system from . import HiveEntity, refresh_system
from .const import ATTR_MODE, DOMAIN
PARALLEL_UPDATES = 0 PARALLEL_UPDATES = 0
SCAN_INTERVAL = timedelta(seconds=15) SCAN_INTERVAL = timedelta(seconds=15)
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): async def async_setup_entry(hass, entry, async_add_entities):
"""Set up the Hive Switch.""" """Set up Hive thermostat based on a config entry."""
if discovery_info is None:
return
hive = hass.data[DOMAIN].get(DATA_HIVE) hive = hass.data[DOMAIN][entry.entry_id]
devices = hive.devices.get("switch") devices = hive.session.deviceList.get("switch")
entities = [] entities = []
if devices: if devices:
for dev in devices: for dev in devices:
@ -34,7 +33,15 @@ class HiveDevicePlug(HiveEntity, SwitchEntity):
@property @property
def device_info(self): def device_info(self):
"""Return device information.""" """Return device information."""
return {"identifiers": {(DOMAIN, self.unique_id)}, "name": self.name} if self.device["hiveType"] == "activeplug":
return {
"identifiers": {(DOMAIN, self.device["device_id"])},
"name": self.device["device_name"],
"model": self.device["deviceData"]["model"],
"manufacturer": self.device["deviceData"]["manufacturer"],
"sw_version": self.device["deviceData"]["version"],
"via_device": (DOMAIN, self.device["parentDevice"]),
}
@property @property
def name(self): def name(self):
@ -50,7 +57,6 @@ class HiveDevicePlug(HiveEntity, SwitchEntity):
def extra_state_attributes(self): def extra_state_attributes(self):
"""Show Device Attributes.""" """Show Device Attributes."""
return { return {
ATTR_AVAILABLE: self.attributes.get(ATTR_AVAILABLE),
ATTR_MODE: self.attributes.get(ATTR_MODE), ATTR_MODE: self.attributes.get(ATTR_MODE),
} }
@ -67,16 +73,14 @@ class HiveDevicePlug(HiveEntity, SwitchEntity):
@refresh_system @refresh_system
async def async_turn_on(self, **kwargs): async def async_turn_on(self, **kwargs):
"""Turn the switch on.""" """Turn the switch on."""
if self.device["hiveType"] == "activeplug": await self.hive.switch.turnOn(self.device)
await self.hive.switch.turn_on(self.device)
@refresh_system @refresh_system
async def async_turn_off(self, **kwargs): async def async_turn_off(self, **kwargs):
"""Turn the device off.""" """Turn the device off."""
if self.device["hiveType"] == "activeplug": await self.hive.switch.turnOff(self.device)
await self.hive.switch.turn_off(self.device)
async def async_update(self): async def async_update(self):
"""Update all Node data from Hive.""" """Update all Node data from Hive."""
await self.hive.session.updateData(self.device) await self.hive.session.updateData(self.device)
self.device = await self.hive.switch.get_plug(self.device) self.device = await self.hive.switch.getPlug(self.device)

View File

@ -0,0 +1,53 @@
{
"config": {
"step": {
"user": {
"title": "Hive Login",
"description": "Enter your Hive login information and configuration.",
"data": {
"username": "Username",
"password": "Password",
"scan_interval": "Scan Interval (seconds)"
}
},
"2fa": {
"title": "Hive Two-factor Authentication.",
"description": "Enter your Hive authentication code. \n \n Please enter code 0000 to request another code.",
"data": {
"2fa": "Two-factor code"
}
},
"reauth": {
"title": "Hive Login",
"description": "Re-enter your Hive login information.",
"data": {
"username": "Username",
"password": "Password"
}
}
},
"error": {
"invalid_username": "Failed to sign into Hive. Your email address is not recognised.",
"invalid_password": "Failed to sign into Hive. Incorrect password please try again.",
"invalid_code": "Failed to sign into Hive. Your two-factor authentication code was incorrect.",
"no_internet_available": "An internet connection is required to connect to Hive.",
"unknown": "Unexpected error"
},
"abort": {
"already_configured": "Account is already configured",
"unknown_entry": "Unable to find existing entry.",
"reauth_successful": "Re-authentication was successful"
}
},
"options": {
"step": {
"user": {
"title": "Options for Hive",
"description": "Update the scan interval to poll for data more often.",
"data": {
"scan_interval": "Scan Interval (seconds)"
}
}
}
}
}

View File

@ -2,6 +2,8 @@
from datetime import timedelta from datetime import timedelta
import voluptuous as vol
from homeassistant.components.water_heater import ( from homeassistant.components.water_heater import (
STATE_ECO, STATE_ECO,
STATE_OFF, STATE_OFF,
@ -10,8 +12,16 @@ from homeassistant.components.water_heater import (
WaterHeaterEntity, WaterHeaterEntity,
) )
from homeassistant.const import TEMP_CELSIUS from homeassistant.const import TEMP_CELSIUS
from homeassistant.helpers import config_validation as cv, entity_platform
from . import DATA_HIVE, DOMAIN, HiveEntity, refresh_system from . import HiveEntity, refresh_system
from .const import (
ATTR_ONOFF,
ATTR_TIME_PERIOD,
DOMAIN,
SERVICE_BOOST_HOT_WATER,
WATER_HEATER_MODES,
)
SUPPORT_FLAGS_HEATER = SUPPORT_OPERATION_MODE SUPPORT_FLAGS_HEATER = SUPPORT_OPERATION_MODE
HOTWATER_NAME = "Hot Water" HOTWATER_NAME = "Hot Water"
@ -32,19 +42,32 @@ HASS_TO_HIVE_STATE = {
SUPPORT_WATER_HEATER = [STATE_ECO, STATE_ON, STATE_OFF] SUPPORT_WATER_HEATER = [STATE_ECO, STATE_ON, STATE_OFF]
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): async def async_setup_entry(hass, entry, async_add_entities):
"""Set up the Hive Hotwater.""" """Set up Hive thermostat based on a config entry."""
if discovery_info is None:
return
hive = hass.data[DOMAIN].get(DATA_HIVE) hive = hass.data[DOMAIN][entry.entry_id]
devices = hive.devices.get("water_heater") devices = hive.session.deviceList.get("water_heater")
entities = [] entities = []
if devices: if devices:
for dev in devices: for dev in devices:
entities.append(HiveWaterHeater(hive, dev)) entities.append(HiveWaterHeater(hive, dev))
async_add_entities(entities, True) async_add_entities(entities, True)
platform = entity_platform.current_platform.get()
platform.async_register_entity_service(
SERVICE_BOOST_HOT_WATER,
{
vol.Optional(ATTR_TIME_PERIOD, default="00:30:00"): vol.All(
cv.time_period,
cv.positive_timedelta,
lambda td: td.total_seconds() // 60,
),
vol.Required(ATTR_ONOFF): vol.In(WATER_HEATER_MODES),
},
"async_hot_water_boost",
)
class HiveWaterHeater(HiveEntity, WaterHeaterEntity): class HiveWaterHeater(HiveEntity, WaterHeaterEntity):
"""Hive Water Heater Device.""" """Hive Water Heater Device."""
@ -57,7 +80,14 @@ class HiveWaterHeater(HiveEntity, WaterHeaterEntity):
@property @property
def device_info(self): def device_info(self):
"""Return device information.""" """Return device information."""
return {"identifiers": {(DOMAIN, self.unique_id)}, "name": self.name} return {
"identifiers": {(DOMAIN, self.device["device_id"])},
"name": self.device["device_name"],
"model": self.device["deviceData"]["model"],
"manufacturer": self.device["deviceData"]["manufacturer"],
"sw_version": self.device["deviceData"]["version"],
"via_device": (DOMAIN, self.device["parentDevice"]),
}
@property @property
def supported_features(self): def supported_features(self):
@ -92,20 +122,28 @@ class HiveWaterHeater(HiveEntity, WaterHeaterEntity):
@refresh_system @refresh_system
async def async_turn_on(self, **kwargs): async def async_turn_on(self, **kwargs):
"""Turn on hotwater.""" """Turn on hotwater."""
await self.hive.hotwater.set_mode(self.device, "MANUAL") await self.hive.hotwater.setMode(self.device, "MANUAL")
@refresh_system @refresh_system
async def async_turn_off(self, **kwargs): async def async_turn_off(self, **kwargs):
"""Turn on hotwater.""" """Turn on hotwater."""
await self.hive.hotwater.set_mode(self.device, "OFF") await self.hive.hotwater.setMode(self.device, "OFF")
@refresh_system @refresh_system
async def async_set_operation_mode(self, operation_mode): async def async_set_operation_mode(self, operation_mode):
"""Set operation mode.""" """Set operation mode."""
new_mode = HASS_TO_HIVE_STATE[operation_mode] new_mode = HASS_TO_HIVE_STATE[operation_mode]
await self.hive.hotwater.set_mode(self.device, new_mode) await self.hive.hotwater.setMode(self.device, new_mode)
@refresh_system
async def async_hot_water_boost(self, time_period, on_off):
"""Handle the service call."""
if on_off == "on":
await self.hive.hotwater.turnBoostOn(self.device, time_period)
elif on_off == "off":
await self.hive.hotwater.turnBoostOff(self.device)
async def async_update(self): async def async_update(self):
"""Update all Node data from Hive.""" """Update all Node data from Hive."""
await self.hive.session.updateData(self.device) await self.hive.session.updateData(self.device)
self.device = await self.hive.hotwater.get_hotwater(self.device) self.device = await self.hive.hotwater.getHotwater(self.device)

View File

@ -92,6 +92,7 @@ FLOWS = [
"harmony", "harmony",
"heos", "heos",
"hisense_aehw4a1", "hisense_aehw4a1",
"hive",
"hlk_sw16", "hlk_sw16",
"home_connect", "home_connect",
"homekit", "homekit",

View File

@ -1428,7 +1428,7 @@ pyheos==0.7.2
pyhik==0.2.8 pyhik==0.2.8
# homeassistant.components.hive # homeassistant.components.hive
pyhiveapi==0.3.4.4 pyhiveapi==0.3.9
# homeassistant.components.homematic # homeassistant.components.homematic
pyhomematic==0.1.72 pyhomematic==0.1.72

View File

@ -747,6 +747,9 @@ pyhaversion==3.4.2
# homeassistant.components.heos # homeassistant.components.heos
pyheos==0.7.2 pyheos==0.7.2
# homeassistant.components.hive
pyhiveapi==0.3.9
# homeassistant.components.homematic # homeassistant.components.homematic
pyhomematic==0.1.72 pyhomematic==0.1.72

View File

@ -0,0 +1,576 @@
"""Test the Hive config flow."""
from unittest.mock import patch
from apyhiveapi.helper import hive_exceptions
from homeassistant import config_entries, data_entry_flow, setup
from homeassistant.components.hive.const import CONF_CODE, DOMAIN
from homeassistant.const import CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_USERNAME
from tests.common import MockConfigEntry
USERNAME = "username@home-assistant.com"
UPDATED_USERNAME = "updated_username@home-assistant.com"
PASSWORD = "test-password"
UPDATED_PASSWORD = "updated-password"
INCORRECT_PASSWORD = "incoreect-password"
SCAN_INTERVAL = 120
UPDATED_SCAN_INTERVAL = 60
MFA_CODE = "1234"
MFA_RESEND_CODE = "0000"
MFA_INVALID_CODE = "HIVE"
async def test_import_flow(hass):
"""Check import flow."""
with patch(
"homeassistant.components.hive.config_flow.Auth.login",
return_value={
"ChallengeName": "SUCCESS",
"AuthenticationResult": {
"RefreshToken": "mock-refresh-token",
"AccessToken": "mock-access-token",
},
},
), patch(
"homeassistant.components.hive.async_setup", return_value=True
) as mock_setup, patch(
"homeassistant.components.hive.async_setup_entry",
return_value=True,
) as mock_setup_entry:
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_IMPORT},
data={CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD},
)
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result["title"] == USERNAME
assert result["data"] == {
CONF_USERNAME: USERNAME,
CONF_PASSWORD: PASSWORD,
"tokens": {
"AuthenticationResult": {
"AccessToken": "mock-access-token",
"RefreshToken": "mock-refresh-token",
},
"ChallengeName": "SUCCESS",
},
}
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
assert len(mock_setup.mock_calls) == 1
assert len(mock_setup_entry.mock_calls) == 1
async def test_user_flow(hass):
"""Test the user flow."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["errors"] == {}
with patch(
"homeassistant.components.hive.config_flow.Auth.login",
return_value={
"ChallengeName": "SUCCESS",
"AuthenticationResult": {
"RefreshToken": "mock-refresh-token",
"AccessToken": "mock-access-token",
},
},
), patch(
"homeassistant.components.hive.async_setup", return_value=True
) as mock_setup, patch(
"homeassistant.components.hive.async_setup_entry",
return_value=True,
) as mock_setup_entry:
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD},
)
await hass.async_block_till_done()
assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result2["title"] == USERNAME
assert result2["data"] == {
CONF_USERNAME: USERNAME,
CONF_PASSWORD: PASSWORD,
"tokens": {
"AuthenticationResult": {
"AccessToken": "mock-access-token",
"RefreshToken": "mock-refresh-token",
},
"ChallengeName": "SUCCESS",
},
}
assert len(mock_setup.mock_calls) == 1
assert len(mock_setup_entry.mock_calls) == 1
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
async def test_user_flow_2fa(hass):
"""Test user flow with 2FA."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["errors"] == {}
with patch(
"homeassistant.components.hive.config_flow.Auth.login",
return_value={
"ChallengeName": "SMS_MFA",
},
):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_USERNAME: USERNAME,
CONF_PASSWORD: PASSWORD,
},
)
assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result2["step_id"] == CONF_CODE
assert result2["errors"] == {}
with patch(
"homeassistant.components.hive.config_flow.Auth.sms_2fa",
return_value={
"ChallengeName": "SUCCESS",
"AuthenticationResult": {
"RefreshToken": "mock-refresh-token",
"AccessToken": "mock-access-token",
},
},
), patch(
"homeassistant.components.hive.async_setup", return_value=True
) as mock_setup, patch(
"homeassistant.components.hive.async_setup_entry",
return_value=True,
) as mock_setup_entry:
result3 = await hass.config_entries.flow.async_configure(
result2["flow_id"], {CONF_CODE: MFA_CODE}
)
await hass.async_block_till_done()
assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result3["title"] == USERNAME
assert result3["data"] == {
CONF_USERNAME: USERNAME,
CONF_PASSWORD: PASSWORD,
"tokens": {
"AuthenticationResult": {
"AccessToken": "mock-access-token",
"RefreshToken": "mock-refresh-token",
},
"ChallengeName": "SUCCESS",
},
}
assert len(mock_setup.mock_calls) == 1
assert len(mock_setup_entry.mock_calls) == 1
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
async def test_reauth_flow(hass):
"""Test the reauth flow."""
await setup.async_setup_component(hass, "persistent_notification", {})
mock_config = MockConfigEntry(
domain=DOMAIN,
unique_id=USERNAME,
data={
CONF_USERNAME: USERNAME,
CONF_PASSWORD: INCORRECT_PASSWORD,
"tokens": {
"AccessToken": "mock-access-token",
"RefreshToken": "mock-refresh-token",
},
},
)
mock_config.add_to_hass(hass)
with patch(
"homeassistant.components.hive.config_flow.Auth.login",
side_effect=hive_exceptions.HiveInvalidPassword(),
):
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={
"source": config_entries.SOURCE_REAUTH,
"unique_id": mock_config.unique_id,
},
data=mock_config.data,
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["errors"] == {"base": "invalid_password"}
with patch(
"homeassistant.components.hive.config_flow.Auth.login",
return_value={
"ChallengeName": "SUCCESS",
"AuthenticationResult": {
"RefreshToken": "mock-refresh-token",
"AccessToken": "mock-access-token",
},
},
):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_USERNAME: USERNAME,
CONF_PASSWORD: UPDATED_PASSWORD,
},
)
await hass.async_block_till_done()
assert mock_config.data.get("username") == USERNAME
assert mock_config.data.get("password") == UPDATED_PASSWORD
assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT
assert result2["reason"] == "reauth_successful"
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
async def test_option_flow(hass):
"""Test config flow options."""
entry = MockConfigEntry(
domain=DOMAIN,
title=USERNAME,
data={CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD},
)
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
result = await hass.config_entries.options.async_init(
entry.entry_id,
data=None,
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "user"
result = await hass.config_entries.options.async_configure(
result["flow_id"], user_input={CONF_SCAN_INTERVAL: UPDATED_SCAN_INTERVAL}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result["data"][CONF_SCAN_INTERVAL] == UPDATED_SCAN_INTERVAL
async def test_user_flow_2fa_send_new_code(hass):
"""Resend a 2FA code if it didn't arrive."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["errors"] == {}
with patch(
"homeassistant.components.hive.config_flow.Auth.login",
return_value={
"ChallengeName": "SMS_MFA",
},
):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_USERNAME: USERNAME,
CONF_PASSWORD: PASSWORD,
},
)
assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result2["step_id"] == CONF_CODE
assert result2["errors"] == {}
with patch(
"homeassistant.components.hive.config_flow.Auth.login",
return_value={
"ChallengeName": "SMS_MFA",
},
):
result3 = await hass.config_entries.flow.async_configure(
result2["flow_id"], {CONF_CODE: MFA_RESEND_CODE}
)
await hass.async_block_till_done()
assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result3["step_id"] == CONF_CODE
assert result3["errors"] == {}
with patch(
"homeassistant.components.hive.config_flow.Auth.sms_2fa",
return_value={
"ChallengeName": "SUCCESS",
"AuthenticationResult": {
"RefreshToken": "mock-refresh-token",
"AccessToken": "mock-access-token",
},
},
), patch(
"homeassistant.components.hive.async_setup", return_value=True
) as mock_setup, patch(
"homeassistant.components.hive.async_setup_entry",
return_value=True,
) as mock_setup_entry:
result4 = await hass.config_entries.flow.async_configure(
result3["flow_id"], {CONF_CODE: MFA_CODE}
)
await hass.async_block_till_done()
assert result4["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result4["title"] == USERNAME
assert result4["data"] == {
CONF_USERNAME: USERNAME,
CONF_PASSWORD: PASSWORD,
"tokens": {
"AuthenticationResult": {
"AccessToken": "mock-access-token",
"RefreshToken": "mock-refresh-token",
},
"ChallengeName": "SUCCESS",
},
}
assert len(mock_setup.mock_calls) == 1
assert len(mock_setup_entry.mock_calls) == 1
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
async def test_abort_if_existing_entry(hass):
"""Check flow abort when an entry already exist."""
config_entry = MockConfigEntry(
domain=DOMAIN,
unique_id=USERNAME,
data={CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD},
options={CONF_SCAN_INTERVAL: SCAN_INTERVAL},
)
config_entry.add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_USER},
data={
CONF_USERNAME: USERNAME,
CONF_PASSWORD: PASSWORD,
},
)
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
assert result["reason"] == "already_configured"
async def test_user_flow_invalid_username(hass):
"""Test user flow with invalid username."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["errors"] == {}
with patch(
"homeassistant.components.hive.config_flow.Auth.login",
side_effect=hive_exceptions.HiveInvalidUsername(),
):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD},
)
assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result2["step_id"] == "user"
assert result2["errors"] == {"base": "invalid_username"}
async def test_user_flow_invalid_password(hass):
"""Test user flow with invalid password."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["errors"] == {}
with patch(
"homeassistant.components.hive.config_flow.Auth.login",
side_effect=hive_exceptions.HiveInvalidPassword(),
):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD},
)
assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result2["step_id"] == "user"
assert result2["errors"] == {"base": "invalid_password"}
async def test_user_flow_no_internet_connection(hass):
"""Test user flow with no internet connection."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["errors"] == {}
with patch(
"homeassistant.components.hive.config_flow.Auth.login",
side_effect=hive_exceptions.HiveApiError(),
):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD},
)
assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result2["step_id"] == "user"
assert result2["errors"] == {"base": "no_internet_available"}
async def test_user_flow_2fa_no_internet_connection(hass):
"""Test user flow with no internet connection."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["errors"] == {}
with patch(
"homeassistant.components.hive.config_flow.Auth.login",
return_value={
"ChallengeName": "SMS_MFA",
},
):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD},
)
assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result2["step_id"] == CONF_CODE
assert result2["errors"] == {}
with patch(
"homeassistant.components.hive.config_flow.Auth.sms_2fa",
side_effect=hive_exceptions.HiveApiError(),
):
result3 = await hass.config_entries.flow.async_configure(
result2["flow_id"],
{CONF_CODE: MFA_CODE},
)
assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result3["step_id"] == CONF_CODE
assert result3["errors"] == {"base": "no_internet_available"}
async def test_user_flow_2fa_invalid_code(hass):
"""Test user flow with 2FA."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["errors"] == {}
with patch(
"homeassistant.components.hive.config_flow.Auth.login",
return_value={
"ChallengeName": "SMS_MFA",
},
):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD},
)
assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result2["step_id"] == CONF_CODE
assert result2["errors"] == {}
with patch(
"homeassistant.components.hive.config_flow.Auth.sms_2fa",
side_effect=hive_exceptions.HiveInvalid2FACode(),
):
result3 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{CONF_CODE: MFA_INVALID_CODE},
)
assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result3["step_id"] == CONF_CODE
assert result3["errors"] == {"base": "invalid_code"}
async def test_user_flow_unknown_error(hass):
"""Test user flow when unknown error occurs."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["errors"] == {}
with patch(
"homeassistant.components.hive.config_flow.Auth.login",
return_value={"ChallengeName": "FAILED", "InvalidAuthenticationResult": {}},
):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD},
)
await hass.async_block_till_done()
assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result2["errors"] == {"base": "unknown"}
async def test_user_flow_2fa_unknown_error(hass):
"""Test 2fa flow when unknown error occurs."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["errors"] == {}
with patch(
"homeassistant.components.hive.config_flow.Auth.login",
return_value={
"ChallengeName": "SMS_MFA",
},
):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD},
)
assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result2["step_id"] == CONF_CODE
with patch(
"homeassistant.components.hive.config_flow.Auth.sms_2fa",
return_value={"ChallengeName": "FAILED", "InvalidAuthenticationResult": {}},
):
result3 = await hass.config_entries.flow.async_configure(
result2["flow_id"],
{CONF_CODE: MFA_CODE},
)
await hass.async_block_till_done()
assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result3["errors"] == {"base": "unknown"}