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
pull/47946/head
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/hisense_aehw4a1/*
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/switch.py
homeassistant/components/home_connect/*

View File

@ -1,43 +1,26 @@
"""Support for the Hive devices and services."""
import asyncio
from functools import wraps
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
from homeassistant.const import (
ATTR_ENTITY_ID,
ATTR_TEMPERATURE,
CONF_PASSWORD,
CONF_SCAN_INTERVAL,
CONF_USERNAME,
)
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.discovery import async_load_platform
from homeassistant import config_entries
from homeassistant.const import CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_USERNAME
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import aiohttp_client, config_validation as cv
from homeassistant.helpers.dispatcher import (
async_dispatcher_connect,
async_dispatcher_send,
)
from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
from .const import DOMAIN, PLATFORM_LOOKUP, PLATFORMS
ATTR_AVAILABLE = "available"
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",
}
_LOGGER = logging.getLogger(__name__)
CONFIG_SCHEMA = vol.Schema(
{
@ -52,101 +35,88 @@ CONFIG_SCHEMA = vol.Schema(
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):
"""Set up the Hive Component."""
"""Hive configuration setup."""
hass.data[DOMAIN] = {}
async def heating_boost(service):
"""Handle the service call."""
if DOMAIN not in config:
return True
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
conf = config[DOMAIN]
minutes = service.data[ATTR_TIME_PERIOD]
temperature = service.data[ATTR_TEMPERATURE]
if not hass.config_entries.async_entries(DOMAIN):
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):
"""Handle the service call."""
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]
async def async_setup_entry(hass, entry):
"""Set up Hive from a config entry."""
if mode == "on":
hive.hotwater.turn_boost_on(hive_id, minutes)
elif mode == "off":
hive.hotwater.turn_boost_off(hive_id)
websession = aiohttp_client.async_get_clientsession(hass)
hive = Hive(websession)
hive_config = dict(entry.data)
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 = {}
config["username"] = config[DOMAIN][CONF_USERNAME]
config["password"] = config[DOMAIN][CONF_PASSWORD]
config["update_interval"] = config[DOMAIN][CONF_SCAN_INTERVAL]
devices = await hive.session.startSession(config)
if devices is None:
_LOGGER.error("Hive API initialization failed")
try:
devices = await hive.session.startSession(hive_config)
except HTTPException as error:
_LOGGER.error("Could not connect to the internet: %s", error)
raise ConfigEntryNotReady() from error
except HiveReauthRequired:
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN,
context={
"source": config_entries.SOURCE_REAUTH,
"unique_id": entry.unique_id,
},
data=entry.data,
)
)
return False
hass.data[DOMAIN][DATA_HIVE] = hive
hass.data[DOMAIN]["entity_lookup"] = {}
for ha_type in DEVICETYPES:
devicelist = devices.get(DEVICETYPES[ha_type])
if devicelist:
for ha_type, hive_type in PLATFORM_LOOKUP.items():
device_list = devices.get(hive_type)
if device_list:
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
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):
"""Force update all entities after state change."""
@ -173,6 +143,3 @@ class HiveEntity(Entity):
self.async_on_remove(
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,
)
from . import ATTR_AVAILABLE, ATTR_MODE, DATA_HIVE, DOMAIN, HiveEntity
from . import HiveEntity
from .const import ATTR_MODE, DOMAIN
DEVICETYPE = {
"contactsensor": DEVICE_CLASS_OPENING,
@ -24,13 +25,11 @@ PARALLEL_UPDATES = 0
SCAN_INTERVAL = timedelta(seconds=15)
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up the Hive Binary Sensor."""
if discovery_info is None:
return
async def async_setup_entry(hass, entry, async_add_entities):
"""Set up Hive thermostat based on a config entry."""
hive = hass.data[DOMAIN].get(DATA_HIVE)
devices = hive.devices.get("binary_sensor")
hive = hass.data[DOMAIN][entry.entry_id]
devices = hive.session.deviceList.get("binary_sensor")
entities = []
if devices:
for dev in devices:
@ -49,7 +48,14 @@ class HiveBinarySensorEntity(HiveEntity, BinarySensorEntity):
@property
def device_info(self):
"""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
def device_class(self):
@ -72,7 +78,6 @@ class HiveBinarySensorEntity(HiveEntity, BinarySensorEntity):
def extra_state_attributes(self):
"""Show Device Attributes."""
return {
ATTR_AVAILABLE: self.attributes.get(ATTR_AVAILABLE),
ATTR_MODE: self.attributes.get(ATTR_MODE),
}
@ -84,5 +89,5 @@ class HiveBinarySensorEntity(HiveEntity, BinarySensorEntity):
async def async_update(self):
"""Update all Node data from Hive."""
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", {})

View File

@ -1,6 +1,8 @@
"""Support for the Hive climate devices."""
from datetime import timedelta
import voluptuous as vol
from homeassistant.components.climate import ClimateEntity
from homeassistant.components.climate.const import (
CURRENT_HVAC_HEAT,
@ -15,8 +17,10 @@ from homeassistant.components.climate.const import (
SUPPORT_TARGET_TEMPERATURE,
)
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 = {
"SCHEDULE": HVAC_MODE_AUTO,
@ -45,19 +49,32 @@ PARALLEL_UPDATES = 0
SCAN_INTERVAL = timedelta(seconds=15)
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up the Hive thermostat."""
if discovery_info is None:
return
async def async_setup_entry(hass, entry, async_add_entities):
"""Set up Hive thermostat based on a config entry."""
hive = hass.data[DOMAIN].get(DATA_HIVE)
devices = hive.devices.get("climate")
hive = hass.data[DOMAIN][entry.entry_id]
devices = hive.session.deviceList.get("climate")
entities = []
if devices:
for dev in devices:
entities.append(HiveClimateEntity(hive, dev))
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):
"""Hive Climate Device."""
@ -76,7 +93,14 @@ class HiveClimateEntity(HiveEntity, ClimateEntity):
@property
def device_info(self):
"""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
def supported_features(self):
@ -93,11 +117,6 @@ class HiveClimateEntity(HiveEntity, ClimateEntity):
"""Return if the device is available."""
return self.device["deviceData"]["online"]
@property
def extra_state_attributes(self):
"""Show Device Attributes."""
return {ATTR_AVAILABLE: self.attributes.get(ATTR_AVAILABLE)}
@property
def hvac_modes(self):
"""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):
"""Set new target 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
async def async_set_temperature(self, **kwargs):
"""Set new target temperature."""
new_temperature = kwargs.get(ATTR_TEMPERATURE)
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
async def async_set_preset_mode(self, preset_mode):
"""Set new preset mode."""
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:
curtemp = round(self.current_temperature * 2) / 2
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):
"""Update all Node data from Hive."""
await self.hive.session.updateData(self.device)
self.device = await self.hive.heating.get_heating(self.device)
self.attributes.update(self.device.get("attributes", {}))
self.device = await self.hive.heating.getHeating(self.device)

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
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
SCAN_INTERVAL = timedelta(seconds=15)
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up the Hive Light."""
if discovery_info is None:
return
async def async_setup_entry(hass, entry, async_add_entities):
"""Set up Hive thermostat based on a config entry."""
hive = hass.data[DOMAIN].get(DATA_HIVE)
devices = hive.devices.get("light")
hive = hass.data[DOMAIN][entry.entry_id]
devices = hive.session.deviceList.get("light")
entities = []
if devices:
for dev in devices:
@ -43,7 +42,14 @@ class HiveDeviceLight(HiveEntity, LightEntity):
@property
def device_info(self):
"""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
def name(self):
@ -59,7 +65,6 @@ class HiveDeviceLight(HiveEntity, LightEntity):
def extra_state_attributes(self):
"""Show Device Attributes."""
return {
ATTR_AVAILABLE: self.attributes.get(ATTR_AVAILABLE),
ATTR_MODE: self.attributes.get(ATTR_MODE),
}
@ -117,14 +122,14 @@ class HiveDeviceLight(HiveEntity, LightEntity):
saturation = int(get_new_color[1])
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
)
@refresh_system
async def async_turn_off(self, **kwargs):
"""Instruct the light to turn off."""
await self.hive.light.turn_off(self.device)
await self.hive.light.turnOff(self.device)
@property
def supported_features(self):
@ -142,5 +147,5 @@ class HiveDeviceLight(HiveEntity, LightEntity):
async def async_update(self):
"""Update all Node data from Hive."""
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", {}))

View File

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

View File

@ -5,7 +5,8 @@ from datetime import timedelta
from homeassistant.components.sensor import DEVICE_CLASS_BATTERY
from homeassistant.helpers.entity import Entity
from . import ATTR_AVAILABLE, DATA_HIVE, DOMAIN, HiveEntity
from . import HiveEntity
from .const import DOMAIN
PARALLEL_UPDATES = 0
SCAN_INTERVAL = timedelta(seconds=15)
@ -14,18 +15,15 @@ DEVICETYPE = {
}
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up the Hive Sensor."""
if discovery_info is None:
return
async def async_setup_entry(hass, entry, async_add_entities):
"""Set up Hive thermostat based on a config entry."""
hive = hass.data[DOMAIN].get(DATA_HIVE)
devices = hive.devices.get("sensor")
hive = hass.data[DOMAIN][entry.entry_id]
devices = hive.session.deviceList.get("sensor")
entities = []
if 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)
@ -40,7 +38,14 @@ class HiveSensorEntity(HiveEntity, Entity):
@property
def device_info(self):
"""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
def available(self):
@ -67,12 +72,7 @@ class HiveSensorEntity(HiveEntity, Entity):
"""Return the state of the sensor."""
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):
"""Update all Node data from Hive."""
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:
name: Boost Heating
description: Set the boost mode ON defining the period of time and the desired target temperature for the boost.
fields:
entity_id:
description: Enter the entity_id for the device required to set the boost mode.
example: "climate.heating"
name: Entity ID
description: Select entity_id to boost.
required: true
example: climate.heating
selector:
entity:
integration: hive
domain: climate
time_period:
name: Time Period
description: Set the time period for the boost.
example: "01:30:00"
required: true
example: 01:30:00
selector:
time:
temperature:
name: Temperature
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:
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:
entity_id:
description: Enter the entity_id for the device reuired to set the boost mode.
example: "water_heater.hot_water"
name: Entity ID
description: Select entity_id to boost.
required: true
example: water_heater.hot_water
selector:
entity:
integration: hive
domain: water_heater
time_period:
name: Time Period
description: Set the time period for the boost.
example: "01:30:00"
required: true
example: 01:30:00
selector:
time:
on_off:
name: Mode
description: Set the boost function on or off.
required: true
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 . 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
SCAN_INTERVAL = timedelta(seconds=15)
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up the Hive Switch."""
if discovery_info is None:
return
async def async_setup_entry(hass, entry, async_add_entities):
"""Set up Hive thermostat based on a config entry."""
hive = hass.data[DOMAIN].get(DATA_HIVE)
devices = hive.devices.get("switch")
hive = hass.data[DOMAIN][entry.entry_id]
devices = hive.session.deviceList.get("switch")
entities = []
if devices:
for dev in devices:
@ -34,7 +33,15 @@ class HiveDevicePlug(HiveEntity, SwitchEntity):
@property
def device_info(self):
"""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
def name(self):
@ -50,7 +57,6 @@ class HiveDevicePlug(HiveEntity, SwitchEntity):
def extra_state_attributes(self):
"""Show Device Attributes."""
return {
ATTR_AVAILABLE: self.attributes.get(ATTR_AVAILABLE),
ATTR_MODE: self.attributes.get(ATTR_MODE),
}
@ -67,16 +73,14 @@ class HiveDevicePlug(HiveEntity, SwitchEntity):
@refresh_system
async def async_turn_on(self, **kwargs):
"""Turn the switch on."""
if self.device["hiveType"] == "activeplug":
await self.hive.switch.turn_on(self.device)
await self.hive.switch.turnOn(self.device)
@refresh_system
async def async_turn_off(self, **kwargs):
"""Turn the device off."""
if self.device["hiveType"] == "activeplug":
await self.hive.switch.turn_off(self.device)
await self.hive.switch.turnOff(self.device)
async def async_update(self):
"""Update all Node data from Hive."""
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
import voluptuous as vol
from homeassistant.components.water_heater import (
STATE_ECO,
STATE_OFF,
@ -10,8 +12,16 @@ from homeassistant.components.water_heater import (
WaterHeaterEntity,
)
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
HOTWATER_NAME = "Hot Water"
@ -32,19 +42,32 @@ HASS_TO_HIVE_STATE = {
SUPPORT_WATER_HEATER = [STATE_ECO, STATE_ON, STATE_OFF]
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up the Hive Hotwater."""
if discovery_info is None:
return
async def async_setup_entry(hass, entry, async_add_entities):
"""Set up Hive thermostat based on a config entry."""
hive = hass.data[DOMAIN].get(DATA_HIVE)
devices = hive.devices.get("water_heater")
hive = hass.data[DOMAIN][entry.entry_id]
devices = hive.session.deviceList.get("water_heater")
entities = []
if devices:
for dev in devices:
entities.append(HiveWaterHeater(hive, dev))
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):
"""Hive Water Heater Device."""
@ -57,7 +80,14 @@ class HiveWaterHeater(HiveEntity, WaterHeaterEntity):
@property
def device_info(self):
"""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
def supported_features(self):
@ -92,20 +122,28 @@ class HiveWaterHeater(HiveEntity, WaterHeaterEntity):
@refresh_system
async def async_turn_on(self, **kwargs):
"""Turn on hotwater."""
await self.hive.hotwater.set_mode(self.device, "MANUAL")
await self.hive.hotwater.setMode(self.device, "MANUAL")
@refresh_system
async def async_turn_off(self, **kwargs):
"""Turn on hotwater."""
await self.hive.hotwater.set_mode(self.device, "OFF")
await self.hive.hotwater.setMode(self.device, "OFF")
@refresh_system
async def async_set_operation_mode(self, operation_mode):
"""Set 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):
"""Update all Node data from Hive."""
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",
"heos",
"hisense_aehw4a1",
"hive",
"hlk_sw16",
"home_connect",
"homekit",

View File

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

View File

@ -747,6 +747,9 @@ pyhaversion==3.4.2
# homeassistant.components.heos
pyheos==0.7.2
# homeassistant.components.hive
pyhiveapi==0.3.9
# homeassistant.components.homematic
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"}