Fix Fujitsu fglair authentication error and other issues (#125439)

* Use correct app credentials when europe is checked

* Rework to add china as well

* Use our own package since the maintainer of the original package is not responding

* Revert to using rewardone's package

* Import app credentials where needed instead of __init__

* Rework region selector

* Bump config entry minor and add migration

* Address comments
pull/126146/head
Antoine Reversat 2024-09-18 10:23:35 -04:00 committed by GitHub
parent ac93570476
commit e2f1c60981
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 154 additions and 30 deletions

View File

@ -5,14 +5,14 @@ from __future__ import annotations
from contextlib import suppress
from ayla_iot_unofficial import new_ayla_api
from ayla_iot_unofficial.fujitsu_consts import FGLAIR_APP_ID, FGLAIR_APP_SECRET
from ayla_iot_unofficial.fujitsu_consts import FGLAIR_APP_CREDENTIALS
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import aiohttp_client
from .const import API_TIMEOUT, CONF_EUROPE
from .const import API_TIMEOUT, CONF_EUROPE, CONF_REGION, REGION_DEFAULT, REGION_EU
from .coordinator import FGLairCoordinator
PLATFORMS: list[Platform] = [Platform.CLIMATE]
@ -22,12 +22,13 @@ type FGLairConfigEntry = ConfigEntry[FGLairCoordinator]
async def async_setup_entry(hass: HomeAssistant, entry: FGLairConfigEntry) -> bool:
"""Set up Fujitsu HVAC (based on Ayla IOT) from a config entry."""
app_id, app_secret = FGLAIR_APP_CREDENTIALS[entry.data[CONF_REGION]]
api = new_ayla_api(
entry.data[CONF_USERNAME],
entry.data[CONF_PASSWORD],
FGLAIR_APP_ID,
FGLAIR_APP_SECRET,
europe=entry.data[CONF_EUROPE],
app_id,
app_secret,
europe=entry.data[CONF_REGION] == REGION_EU,
websession=aiohttp_client.async_get_clientsession(hass),
timeout=API_TIMEOUT,
)
@ -48,3 +49,24 @@ async def async_unload_entry(hass: HomeAssistant, entry: FGLairConfigEntry) -> b
await entry.runtime_data.api.async_sign_out()
return unload_ok
async def async_migrate_entry(hass: HomeAssistant, entry: FGLairConfigEntry) -> bool:
"""Migrate old entry."""
if entry.version > 1:
return False
if entry.version == 1:
new_data = {**entry.data}
if entry.minor_version < 2:
is_europe = new_data.get(CONF_EUROPE, False)
if is_europe:
new_data[CONF_REGION] = REGION_EU
else:
new_data[CONF_REGION] = REGION_DEFAULT
hass.config_entries.async_update_entry(
entry, data=new_data, minor_version=2, version=1
)
return True

View File

@ -5,14 +5,15 @@ import logging
from typing import Any
from ayla_iot_unofficial import AylaAuthError, new_ayla_api
from ayla_iot_unofficial.fujitsu_consts import FGLAIR_APP_ID, FGLAIR_APP_SECRET
from ayla_iot_unofficial.fujitsu_consts import FGLAIR_APP_CREDENTIALS
import voluptuous as vol
from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.helpers import aiohttp_client
from homeassistant.helpers.selector import SelectSelector, SelectSelectorConfig
from .const import API_TIMEOUT, CONF_EUROPE, DOMAIN
from .const import API_TIMEOUT, CONF_REGION, DOMAIN, REGION_DEFAULT, REGION_EU
_LOGGER = logging.getLogger(__name__)
@ -21,7 +22,12 @@ STEP_USER_DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_USERNAME): str,
vol.Required(CONF_PASSWORD): str,
vol.Required(CONF_EUROPE): bool,
vol.Required(CONF_REGION, default=REGION_DEFAULT): SelectSelector(
SelectSelectorConfig(
options=[region.lower() for region in FGLAIR_APP_CREDENTIALS],
translation_key=CONF_REGION,
)
),
}
)
STEP_REAUTH_DATA_SCHEMA = vol.Schema(
@ -34,18 +40,20 @@ STEP_REAUTH_DATA_SCHEMA = vol.Schema(
class FGLairConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Fujitsu HVAC (based on Ayla IOT)."""
MINOR_VERSION = 2
_reauth_entry: ConfigEntry | None = None
async def _async_validate_credentials(
self, user_input: dict[str, Any]
) -> dict[str, str]:
errors: dict[str, str] = {}
app_id, app_secret = FGLAIR_APP_CREDENTIALS[user_input[CONF_REGION]]
api = new_ayla_api(
user_input[CONF_USERNAME],
user_input[CONF_PASSWORD],
FGLAIR_APP_ID,
FGLAIR_APP_SECRET,
europe=user_input[CONF_EUROPE],
app_id,
app_secret,
europe=user_input[CONF_REGION] == REGION_EU,
websession=aiohttp_client.async_get_clientsession(self.hass),
timeout=API_TIMEOUT,
)

View File

@ -7,4 +7,7 @@ API_REFRESH = timedelta(minutes=5)
DOMAIN = "fujitsu_fglair"
CONF_REGION = "region"
CONF_EUROPE = "is_europe"
REGION_EU = "EU"
REGION_DEFAULT = "default"

View File

@ -5,5 +5,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/fujitsu_fglair",
"iot_class": "cloud_polling",
"requirements": ["ayla-iot-unofficial==1.3.1"]
"requirements": ["ayla-iot-unofficial==1.4.1"]
}

View File

@ -4,12 +4,9 @@
"user": {
"title": "Enter your FGLair credentials",
"data": {
"is_europe": "Use european servers",
"region": "Region",
"username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]"
},
"data_description": {
"is_europe": "Allows the user to choose whether to use european servers or not since the API uses different endoint URLs for european vs non-european users"
}
},
"reauth_confirm": {
@ -29,5 +26,14 @@
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
}
},
"selector": {
"region": {
"options": {
"default": "Other",
"eu": "Europe",
"cn": "China"
}
}
}
}

View File

@ -532,7 +532,7 @@ autarco==3.0.0
axis==62
# homeassistant.components.fujitsu_fglair
ayla-iot-unofficial==1.3.1
ayla-iot-unofficial==1.4.1
# homeassistant.components.azure_event_hub
azure-eventhub==5.11.1

View File

@ -481,7 +481,7 @@ autarco==3.0.0
axis==62
# homeassistant.components.fujitsu_fglair
ayla-iot-unofficial==1.3.1
ayla-iot-unofficial==1.4.1
# homeassistant.components.azure_event_hub
azure-eventhub==5.11.1

View File

@ -7,7 +7,11 @@ from ayla_iot_unofficial import AylaApi
from ayla_iot_unofficial.fujitsu_hvac import FanSpeed, FujitsuHVAC, OpMode, SwingMode
import pytest
from homeassistant.components.fujitsu_fglair.const import CONF_EUROPE, DOMAIN
from homeassistant.components.fujitsu_fglair.const import (
CONF_REGION,
DOMAIN,
REGION_DEFAULT,
)
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from tests.common import MockConfigEntry
@ -57,15 +61,19 @@ def mock_ayla_api(mock_devices: list[AsyncMock]) -> Generator[AsyncMock]:
@pytest.fixture
def mock_config_entry() -> MockConfigEntry:
def mock_config_entry(request: pytest.FixtureRequest) -> MockConfigEntry:
"""Return a regular config entry."""
region = REGION_DEFAULT
if hasattr(request, "param"):
region = request.param
return MockConfigEntry(
domain=DOMAIN,
unique_id=TEST_USERNAME,
data={
CONF_USERNAME: TEST_USERNAME,
CONF_PASSWORD: TEST_PASSWORD,
CONF_EUROPE: False,
CONF_REGION: region,
},
)

View File

@ -5,7 +5,11 @@ from unittest.mock import AsyncMock
from ayla_iot_unofficial import AylaAuthError
import pytest
from homeassistant.components.fujitsu_fglair.const import CONF_EUROPE, DOMAIN
from homeassistant.components.fujitsu_fglair.const import (
CONF_REGION,
DOMAIN,
REGION_DEFAULT,
)
from homeassistant.config_entries import SOURCE_USER
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
@ -28,7 +32,7 @@ async def _initial_step(hass: HomeAssistant) -> FlowResult:
{
CONF_USERNAME: TEST_USERNAME,
CONF_PASSWORD: TEST_PASSWORD,
CONF_EUROPE: False,
CONF_REGION: REGION_DEFAULT,
},
)
@ -45,7 +49,7 @@ async def test_full_flow(
assert result["data"] == {
CONF_USERNAME: TEST_USERNAME,
CONF_PASSWORD: TEST_PASSWORD,
CONF_EUROPE: False,
CONF_REGION: REGION_DEFAULT,
}
@ -94,7 +98,7 @@ async def test_form_exceptions(
{
CONF_USERNAME: TEST_USERNAME,
CONF_PASSWORD: TEST_PASSWORD,
CONF_EUROPE: False,
CONF_REGION: REGION_DEFAULT,
},
)
@ -103,7 +107,7 @@ async def test_form_exceptions(
assert result["data"] == {
CONF_USERNAME: TEST_USERNAME,
CONF_PASSWORD: TEST_PASSWORD,
CONF_EUROPE: False,
CONF_REGION: REGION_DEFAULT,
}

View File

@ -1,17 +1,33 @@
"""Test the initialization of fujitsu_fglair entities."""
from unittest.mock import AsyncMock
from unittest.mock import AsyncMock, patch
from ayla_iot_unofficial import AylaAuthError
from ayla_iot_unofficial.fujitsu_consts import FGLAIR_APP_CREDENTIALS
from freezegun.api import FrozenDateTimeFactory
import pytest
from homeassistant.components.fujitsu_fglair.const import API_REFRESH, DOMAIN
from homeassistant.const import STATE_UNAVAILABLE, Platform
from homeassistant.components.fujitsu_fglair.const import (
API_REFRESH,
API_TIMEOUT,
CONF_EUROPE,
CONF_REGION,
DOMAIN,
REGION_DEFAULT,
REGION_EU,
)
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import (
CONF_PASSWORD,
CONF_USERNAME,
STATE_UNAVAILABLE,
Platform,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers import aiohttp_client, entity_registry as er
from . import entity_id, setup_integration
from .conftest import TEST_PASSWORD, TEST_USERNAME
from tests.common import MockConfigEntry, async_fire_time_changed
@ -35,6 +51,63 @@ async def test_auth_failure(
assert hass.states.get(entity_id(mock_devices[1])).state == STATE_UNAVAILABLE
@pytest.mark.parametrize(
"mock_config_entry", FGLAIR_APP_CREDENTIALS.keys(), indirect=True
)
async def test_auth_regions(
hass: HomeAssistant,
freezer: FrozenDateTimeFactory,
mock_ayla_api: AsyncMock,
mock_config_entry: MockConfigEntry,
mock_devices: list[AsyncMock],
) -> None:
"""Test that we use the correct credentials if europe is selected."""
with patch(
"homeassistant.components.fujitsu_fglair.new_ayla_api", return_value=AsyncMock()
) as new_ayla_api_patch:
await setup_integration(hass, mock_config_entry)
new_ayla_api_patch.assert_called_once_with(
TEST_USERNAME,
TEST_PASSWORD,
FGLAIR_APP_CREDENTIALS[mock_config_entry.data[CONF_REGION]][0],
FGLAIR_APP_CREDENTIALS[mock_config_entry.data[CONF_REGION]][1],
europe=mock_config_entry.data[CONF_REGION] == "EU",
websession=aiohttp_client.async_get_clientsession(hass),
timeout=API_TIMEOUT,
)
@pytest.mark.parametrize("is_europe", [True, False])
async def test_migrate_entry_v11_v12(
hass: HomeAssistant,
freezer: FrozenDateTimeFactory,
mock_ayla_api: AsyncMock,
is_europe: bool,
mock_devices: list[AsyncMock],
) -> None:
"""Test migration from schema 1.1 to 1.2."""
v11_config_entry = MockConfigEntry(
domain=DOMAIN,
unique_id=TEST_USERNAME,
data={
CONF_USERNAME: TEST_USERNAME,
CONF_PASSWORD: TEST_PASSWORD,
CONF_EUROPE: is_europe,
},
)
await setup_integration(hass, v11_config_entry)
updated_entry = hass.config_entries.async_get_entry(v11_config_entry.entry_id)
assert updated_entry.state is ConfigEntryState.LOADED
assert updated_entry.version == 1
assert updated_entry.minor_version == 2
if is_europe:
assert updated_entry.data[CONF_REGION] is REGION_EU
else:
assert updated_entry.data[CONF_REGION] is REGION_DEFAULT
async def test_device_auth_failure(
hass: HomeAssistant,
freezer: FrozenDateTimeFactory,