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
This commit is contained in:
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 contextlib import suppress
from ayla_iot_unofficial import new_ayla_api 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.config_entries import ConfigEntry
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import aiohttp_client 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 from .coordinator import FGLairCoordinator
PLATFORMS: list[Platform] = [Platform.CLIMATE] PLATFORMS: list[Platform] = [Platform.CLIMATE]
@ -22,12 +22,13 @@ type FGLairConfigEntry = ConfigEntry[FGLairCoordinator]
async def async_setup_entry(hass: HomeAssistant, entry: FGLairConfigEntry) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: FGLairConfigEntry) -> bool:
"""Set up Fujitsu HVAC (based on Ayla IOT) from a config entry.""" """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( api = new_ayla_api(
entry.data[CONF_USERNAME], entry.data[CONF_USERNAME],
entry.data[CONF_PASSWORD], entry.data[CONF_PASSWORD],
FGLAIR_APP_ID, app_id,
FGLAIR_APP_SECRET, app_secret,
europe=entry.data[CONF_EUROPE], europe=entry.data[CONF_REGION] == REGION_EU,
websession=aiohttp_client.async_get_clientsession(hass), websession=aiohttp_client.async_get_clientsession(hass),
timeout=API_TIMEOUT, 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() await entry.runtime_data.api.async_sign_out()
return unload_ok 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 typing import Any
from ayla_iot_unofficial import AylaAuthError, new_ayla_api 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 import voluptuous as vol
from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.helpers import aiohttp_client 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__) _LOGGER = logging.getLogger(__name__)
@ -21,7 +22,12 @@ STEP_USER_DATA_SCHEMA = vol.Schema(
{ {
vol.Required(CONF_USERNAME): str, vol.Required(CONF_USERNAME): str,
vol.Required(CONF_PASSWORD): 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( STEP_REAUTH_DATA_SCHEMA = vol.Schema(
@ -34,18 +40,20 @@ STEP_REAUTH_DATA_SCHEMA = vol.Schema(
class FGLairConfigFlow(ConfigFlow, domain=DOMAIN): class FGLairConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Fujitsu HVAC (based on Ayla IOT).""" """Handle a config flow for Fujitsu HVAC (based on Ayla IOT)."""
MINOR_VERSION = 2
_reauth_entry: ConfigEntry | None = None _reauth_entry: ConfigEntry | None = None
async def _async_validate_credentials( async def _async_validate_credentials(
self, user_input: dict[str, Any] self, user_input: dict[str, Any]
) -> dict[str, str]: ) -> dict[str, str]:
errors: dict[str, str] = {} errors: dict[str, str] = {}
app_id, app_secret = FGLAIR_APP_CREDENTIALS[user_input[CONF_REGION]]
api = new_ayla_api( api = new_ayla_api(
user_input[CONF_USERNAME], user_input[CONF_USERNAME],
user_input[CONF_PASSWORD], user_input[CONF_PASSWORD],
FGLAIR_APP_ID, app_id,
FGLAIR_APP_SECRET, app_secret,
europe=user_input[CONF_EUROPE], europe=user_input[CONF_REGION] == REGION_EU,
websession=aiohttp_client.async_get_clientsession(self.hass), websession=aiohttp_client.async_get_clientsession(self.hass),
timeout=API_TIMEOUT, timeout=API_TIMEOUT,
) )

View File

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

View File

@ -5,5 +5,5 @@
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/fujitsu_fglair", "documentation": "https://www.home-assistant.io/integrations/fujitsu_fglair",
"iot_class": "cloud_polling", "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": { "user": {
"title": "Enter your FGLair credentials", "title": "Enter your FGLair credentials",
"data": { "data": {
"is_europe": "Use european servers", "region": "Region",
"username": "[%key:common::config_flow::data::username%]", "username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]" "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": { "reauth_confirm": {
@ -29,5 +26,14 @@
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" "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 axis==62
# homeassistant.components.fujitsu_fglair # homeassistant.components.fujitsu_fglair
ayla-iot-unofficial==1.3.1 ayla-iot-unofficial==1.4.1
# homeassistant.components.azure_event_hub # homeassistant.components.azure_event_hub
azure-eventhub==5.11.1 azure-eventhub==5.11.1

View File

@ -481,7 +481,7 @@ autarco==3.0.0
axis==62 axis==62
# homeassistant.components.fujitsu_fglair # homeassistant.components.fujitsu_fglair
ayla-iot-unofficial==1.3.1 ayla-iot-unofficial==1.4.1
# homeassistant.components.azure_event_hub # homeassistant.components.azure_event_hub
azure-eventhub==5.11.1 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 from ayla_iot_unofficial.fujitsu_hvac import FanSpeed, FujitsuHVAC, OpMode, SwingMode
import pytest 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 homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from tests.common import MockConfigEntry from tests.common import MockConfigEntry
@ -57,15 +61,19 @@ def mock_ayla_api(mock_devices: list[AsyncMock]) -> Generator[AsyncMock]:
@pytest.fixture @pytest.fixture
def mock_config_entry() -> MockConfigEntry: def mock_config_entry(request: pytest.FixtureRequest) -> MockConfigEntry:
"""Return a regular config entry.""" """Return a regular config entry."""
region = REGION_DEFAULT
if hasattr(request, "param"):
region = request.param
return MockConfigEntry( return MockConfigEntry(
domain=DOMAIN, domain=DOMAIN,
unique_id=TEST_USERNAME, unique_id=TEST_USERNAME,
data={ data={
CONF_USERNAME: TEST_USERNAME, CONF_USERNAME: TEST_USERNAME,
CONF_PASSWORD: TEST_PASSWORD, 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 from ayla_iot_unofficial import AylaAuthError
import pytest 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.config_entries import SOURCE_USER
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
@ -28,7 +32,7 @@ async def _initial_step(hass: HomeAssistant) -> FlowResult:
{ {
CONF_USERNAME: TEST_USERNAME, CONF_USERNAME: TEST_USERNAME,
CONF_PASSWORD: TEST_PASSWORD, CONF_PASSWORD: TEST_PASSWORD,
CONF_EUROPE: False, CONF_REGION: REGION_DEFAULT,
}, },
) )
@ -45,7 +49,7 @@ async def test_full_flow(
assert result["data"] == { assert result["data"] == {
CONF_USERNAME: TEST_USERNAME, CONF_USERNAME: TEST_USERNAME,
CONF_PASSWORD: TEST_PASSWORD, 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_USERNAME: TEST_USERNAME,
CONF_PASSWORD: TEST_PASSWORD, CONF_PASSWORD: TEST_PASSWORD,
CONF_EUROPE: False, CONF_REGION: REGION_DEFAULT,
}, },
) )
@ -103,7 +107,7 @@ async def test_form_exceptions(
assert result["data"] == { assert result["data"] == {
CONF_USERNAME: TEST_USERNAME, CONF_USERNAME: TEST_USERNAME,
CONF_PASSWORD: TEST_PASSWORD, CONF_PASSWORD: TEST_PASSWORD,
CONF_EUROPE: False, CONF_REGION: REGION_DEFAULT,
} }

View File

@ -1,17 +1,33 @@
"""Test the initialization of fujitsu_fglair entities.""" """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 import AylaAuthError
from ayla_iot_unofficial.fujitsu_consts import FGLAIR_APP_CREDENTIALS
from freezegun.api import FrozenDateTimeFactory from freezegun.api import FrozenDateTimeFactory
import pytest import pytest
from homeassistant.components.fujitsu_fglair.const import API_REFRESH, DOMAIN from homeassistant.components.fujitsu_fglair.const import (
from homeassistant.const import STATE_UNAVAILABLE, Platform 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.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 . import entity_id, setup_integration
from .conftest import TEST_PASSWORD, TEST_USERNAME
from tests.common import MockConfigEntry, async_fire_time_changed 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 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( async def test_device_auth_failure(
hass: HomeAssistant, hass: HomeAssistant,
freezer: FrozenDateTimeFactory, freezer: FrozenDateTimeFactory,