Skip to content
Merged
5 changes: 3 additions & 2 deletions homeassistant/components/asuswrt/bridge.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
from aiohttp import ClientSession
from asusrouter import AsusRouter, AsusRouterError
from asusrouter.config import ARConfigKey
from asusrouter.modules.client import AsusClient, ConnectionState
from asusrouter.modules.client import AsusClient
from asusrouter.modules.connection import ConnectionState
from asusrouter.modules.data import AsusData
from asusrouter.modules.homeassistant import convert_to_ha_data, convert_to_ha_sensors
from asusrouter.tools.connection import get_cookie_jar
Expand Down Expand Up @@ -86,7 +87,7 @@ def _handle_errors_and_zip(
"""Run library methods and zip results or manage exceptions."""

@functools.wraps(func)
async def _wrapper(self: _AsusWrtBridgeT) -> dict[str, Any]:
async def _wrapper(self: _AsusWrtBridgeT) -> dict[str, str]:
try:
data = await func(self)
except exceptions as exc:
Expand Down
113 changes: 78 additions & 35 deletions homeassistant/components/cync/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@

from __future__ import annotations

from collections.abc import Mapping
import logging
from typing import Any

from pycync import Auth
from pycync.exceptions import AuthFailedError, CyncError, TwoFactorRequiredError
import voluptuous as vol

from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_EMAIL, CONF_PASSWORD
from homeassistant.helpers.aiohttp_client import async_get_clientsession

Expand Down Expand Up @@ -39,37 +40,22 @@ class CyncConfigFlow(ConfigFlow, domain=DOMAIN):

VERSION = 1

cync_auth: Auth
cync_auth: Auth = None

async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Attempt login with user credentials."""
errors: dict[str, str] = {}

if user_input is None:
return self.async_show_form(
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
)
if user_input:
try:
errors = await self._validate_credentials(user_input)
except TwoFactorRequiredError:
return await self.async_step_two_factor()

self.cync_auth = Auth(
async_get_clientsession(self.hass),
username=user_input[CONF_EMAIL],
password=user_input[CONF_PASSWORD],
)
try:
await self.cync_auth.login()
except AuthFailedError:
errors["base"] = "invalid_auth"
except TwoFactorRequiredError:
return await self.async_step_two_factor()
except CyncError:
errors["base"] = "cannot_connect"
except Exception:
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
return await self._create_config_entry(self.cync_auth.username)
if not errors:
return await self._create_config_entry(self.cync_auth.username)

return self.async_show_form(
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
Expand All @@ -81,38 +67,95 @@ async def async_step_two_factor(
"""Attempt login with the two factor auth code sent to the user."""
errors: dict[str, str] = {}

if user_input is None:
if user_input:
errors = await self._validate_credentials(user_input)

if not errors:
return await self._create_config_entry(self.cync_auth.username)

return self.async_show_form(
step_id="two_factor", data_schema=STEP_TWO_FACTOR_SCHEMA, errors=errors
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
)

return self.async_show_form(
step_id="two_factor", data_schema=STEP_TWO_FACTOR_SCHEMA, errors=errors
)

async def async_step_reauth(
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Perform reauth upon an API authentication error."""
return await self.async_step_reauth_confirm()

async def async_step_reauth_confirm(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Dialog that informs the user that reauth is required and prompts for their Cync credentials."""
errors: dict[str, str] = {}

reauth_entry = self._get_reauth_entry()

if user_input:
try:
errors = await self._validate_credentials(user_input)
except TwoFactorRequiredError:
return await self.async_step_two_factor()

if not errors:
return await self._create_config_entry(self.cync_auth.username)

return self.async_show_form(
step_id="reauth_confirm",
data_schema=STEP_USER_DATA_SCHEMA,
errors=errors,
description_placeholders={CONF_EMAIL: reauth_entry.title},
)

async def _validate_credentials(self, user_input: dict[str, Any]) -> dict[str, str]:
"""Attempt to log in with user email and password, and return the error dict."""
errors: dict[str, str] = {}

if not self.cync_auth:
self.cync_auth = Auth(
async_get_clientsession(self.hass),
username=user_input[CONF_EMAIL],
password=user_input[CONF_PASSWORD],
)

try:
await self.cync_auth.login(user_input[CONF_TWO_FACTOR_CODE])
await self.cync_auth.login(user_input.get(CONF_TWO_FACTOR_CODE))
except TwoFactorRequiredError:
raise
except AuthFailedError:
errors["base"] = "invalid_auth"
except CyncError:
errors["base"] = "cannot_connect"
except Exception:
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
return await self._create_config_entry(self.cync_auth.username)

return self.async_show_form(
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
)
return errors

async def _create_config_entry(self, user_email: str) -> ConfigFlowResult:
"""Create the Cync config entry using input user data."""

cync_user = self.cync_auth.user
await self.async_set_unique_id(str(cync_user.user_id))
self._abort_if_unique_id_configured()

config = {
config_data = {
CONF_USER_ID: cync_user.user_id,
CONF_AUTHORIZE_STRING: cync_user.authorize,
CONF_EXPIRES_AT: cync_user.expires_at,
CONF_ACCESS_TOKEN: cync_user.access_token,
CONF_REFRESH_TOKEN: cync_user.refresh_token,
}
return self.async_create_entry(title=user_email, data=config)

if self.source == SOURCE_REAUTH:
self._abort_if_unique_id_mismatch()
return self.async_update_reload_and_abort(
entry=self._get_reauth_entry(), title=user_email, data=config_data
)

self._abort_if_unique_id_configured()

return self.async_create_entry(title=user_email, data=config_data)
2 changes: 1 addition & 1 deletion homeassistant/components/cync/quality_scale.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ rules:
integration-owner: done
log-when-unavailable: todo
parallel-updates: todo
reauthentication-flow: todo
reauthentication-flow: done
test-coverage: todo

# Gold
Expand Down
16 changes: 15 additions & 1 deletion homeassistant/components/cync/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,18 @@
"data_description": {
"two_factor_code": "The two-factor code sent to your Cync account's email"
}
},
"reauth_confirm": {
"title": "[%key:common::config_flow::title::reauth%]",
"description": "The Cync integration needs to re-authenticate for {email}",
"data": {
"email": "[%key:common::config_flow::data::email%]",
"password": "[%key:common::config_flow::data::password%]"
},
"data_description": {
"email": "[%key:component::cync::config::step::user::data_description::email%]",
"password": "[%key:component::cync::config::step::user::data_description::password%]"
}
}
},
"error": {
Expand All @@ -26,7 +38,9 @@
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]"
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
"unique_id_mismatch": "An incorrect user was provided by Cync for your email address, please consult your Cync app"
}
}
}
2 changes: 1 addition & 1 deletion homeassistant/components/esphome/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"mqtt": ["esphome/discover/#"],
"quality_scale": "platinum",
"requirements": [
"aioesphomeapi==41.16.0",
"aioesphomeapi==42.0.0",
"esphome-dashboard-api==1.3.0",
"bleak-esphome==3.4.0"
],
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/monoprice/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/monoprice",
"iot_class": "local_polling",
"loggers": ["pymonoprice"],
"requirements": ["pymonoprice==0.4"]
"requirements": ["pymonoprice==0.5"]
}
5 changes: 4 additions & 1 deletion homeassistant/components/nuheat/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,10 @@ async def async_step_user(
return self.async_create_entry(title=info["title"], data=user_input)

return self.async_show_form(
step_id="user", data_schema=DATA_SCHEMA, errors=errors
step_id="user",
data_schema=DATA_SCHEMA,
errors=errors,
description_placeholders={"nuheat_url": "https://MyNuHeat.com"},
)


Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/nuheat/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"step": {
"user": {
"title": "Connect to the NuHeat",
"description": "You will need to obtain your thermostat\u2019s numeric serial number or ID by logging into https://MyNuHeat.com and selecting your thermostat(s).",
"description": "You will need to obtain your thermostat\u2019s numeric serial number or ID by logging into {nuheat_url} and selecting your thermostat(s).",
"data": {
"username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]",
Expand Down
8 changes: 7 additions & 1 deletion homeassistant/components/probe_plus/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from __future__ import annotations

from homeassistant.const import Platform
from homeassistant.const import CONF_MODEL, Platform
from homeassistant.core import HomeAssistant

from .coordinator import ProbePlusConfigEntry, ProbePlusDataUpdateCoordinator
Expand All @@ -12,6 +12,12 @@

async def async_setup_entry(hass: HomeAssistant, entry: ProbePlusConfigEntry) -> bool:
"""Set up Probe Plus from a config entry."""
# Perform a migration to ensure the model is added to the config entry schema.
if CONF_MODEL not in entry.data:
# The config entry adds the model number of the device to the start of its title
hass.config_entries.async_update_entry(
entry, data={**entry.data, CONF_MODEL: entry.title.split(" ")[0]}
)
coordinator = ProbePlusDataUpdateCoordinator(hass, entry)
await coordinator.async_config_entry_first_refresh()
entry.runtime_data = coordinator
Expand Down
5 changes: 3 additions & 2 deletions homeassistant/components/probe_plus/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
async_discovered_service_info,
)
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_ADDRESS
from homeassistant.const import CONF_ADDRESS, CONF_MODEL

from .const import DOMAIN

Expand Down Expand Up @@ -73,6 +73,7 @@ async def async_step_bluetooth_confirm(
title=discovery.title,
data={
CONF_ADDRESS: discovery.discovery_info.address,
CONF_MODEL: discovery.discovery_info.name,
},
)
self._set_confirm_only()
Expand All @@ -95,7 +96,7 @@ async def async_step_user(
discovery = self._discovered_devices[address]
return self.async_create_entry(
title=discovery.title,
data=user_input,
data={**user_input, CONF_MODEL: discovery.discovery_info.name},
)

current_addresses = self._async_current_ids()
Expand Down
2 changes: 2 additions & 0 deletions homeassistant/components/probe_plus/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from pyprobeplus import ProbePlusDevice

from homeassistant.const import CONF_MODEL
from homeassistant.helpers.device_registry import (
CONNECTION_BLUETOOTH,
DeviceInfo,
Expand Down Expand Up @@ -40,6 +41,7 @@ def __init__(
name=coordinator.device.name,
manufacturer="Probe Plus",
suggested_area="Kitchen",
model=coordinator.config_entry.data.get(CONF_MODEL),
connections={(CONNECTION_BLUETOOTH, coordinator.device.mac)},
)

Expand Down
4 changes: 2 additions & 2 deletions requirements_all.txt

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions requirements_test_all.txt

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 0 additions & 5 deletions script/hassfest/requirements.py
Original file line number Diff line number Diff line change
Expand Up @@ -229,11 +229,6 @@
# pymochad > pbr > setuptools
"pbr": {"setuptools"}
},
"monoprice": {
# https://github.com/etsinko/pymonoprice/issues/9
# pymonoprice > pyserial-asyncio
"pymonoprice": {"pyserial-asyncio"}
},
"nibe_heatpump": {"nibe": {"async-timeout"}},
"norway_air": {"pymetno": {"async-timeout"}},
"opengarage": {"open-garage": {"async-timeout"}},
Expand Down
7 changes: 7 additions & 0 deletions tests/components/cync/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,11 @@
123456789,
expires_at=(time.time() * 1000) + 3600000,
)
SECOND_MOCKED_USER = pycync.User(
"test_token_2",
"test_refresh_token_2",
"test_authorize_string_2",
987654321,
expires_at=(time.time() * 1000) + 3600000,
)
MOCKED_EMAIL = "[email protected]"
Loading
Loading