mirror of
https://github.com/home-assistant/core.git
synced 2025-07-13 08:17:08 +00:00
Handle errors retrieving Ondilo data and bump ondilo to 0.5.0 (#115926)
* Bump ondilo to 0.5.0 and handle errors retrieving data * Bump ondilo to 0.5.0 and handle errors retrieving data * Updated ruff recommendation * Refactor * Refactor * Added exception log and updated call to update data * Updated test cases to test through state machine * Updated test cases * Updated test cases after comments * REnamed file --------- Co-authored-by: Joostlek <joostlek@outlook.com>
This commit is contained in:
parent
4b8b9ce92d
commit
c9930d912e
@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
from asyncio import run_coroutine_threadsafe
|
from asyncio import run_coroutine_threadsafe
|
||||||
import logging
|
import logging
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from ondilo import Ondilo
|
from ondilo import Ondilo
|
||||||
|
|
||||||
@ -36,17 +35,3 @@ class OndiloClient(Ondilo):
|
|||||||
).result()
|
).result()
|
||||||
|
|
||||||
return self.session.token
|
return self.session.token
|
||||||
|
|
||||||
def get_all_pools_data(self) -> list[dict[str, Any]]:
|
|
||||||
"""Fetch pools and add pool details and last measures to pool data."""
|
|
||||||
|
|
||||||
pools = self.get_pools()
|
|
||||||
for pool in pools:
|
|
||||||
_LOGGER.debug(
|
|
||||||
"Retrieving data for pool/spa: %s, id: %d", pool["name"], pool["id"]
|
|
||||||
)
|
|
||||||
pool["ICO"] = self.get_ICO_details(pool["id"])
|
|
||||||
pool["sensors"] = self.get_last_pool_measures(pool["id"])
|
|
||||||
_LOGGER.debug("Retrieved the following sensors data: %s", pool["sensors"])
|
|
||||||
|
|
||||||
return pools
|
|
||||||
|
@ -31,7 +31,36 @@ class OndiloIcoCoordinator(DataUpdateCoordinator[list[dict[str, Any]]]):
|
|||||||
async def _async_update_data(self) -> list[dict[str, Any]]:
|
async def _async_update_data(self) -> list[dict[str, Any]]:
|
||||||
"""Fetch data from API endpoint."""
|
"""Fetch data from API endpoint."""
|
||||||
try:
|
try:
|
||||||
return await self.hass.async_add_executor_job(self.api.get_all_pools_data)
|
return await self.hass.async_add_executor_job(self._update_data)
|
||||||
|
|
||||||
except OndiloError as err:
|
except OndiloError as err:
|
||||||
|
_LOGGER.exception("Error getting pools")
|
||||||
raise UpdateFailed(f"Error communicating with API: {err}") from err
|
raise UpdateFailed(f"Error communicating with API: {err}") from err
|
||||||
|
|
||||||
|
def _update_data(self) -> list[dict[str, Any]]:
|
||||||
|
"""Fetch data from API endpoint."""
|
||||||
|
res = []
|
||||||
|
pools = self.api.get_pools()
|
||||||
|
_LOGGER.debug("Pools: %s", pools)
|
||||||
|
for pool in pools:
|
||||||
|
try:
|
||||||
|
ico = self.api.get_ICO_details(pool["id"])
|
||||||
|
if not ico:
|
||||||
|
_LOGGER.debug(
|
||||||
|
"The pool id %s does not have any ICO attached", pool["id"]
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
sensors = self.api.get_last_pool_measures(pool["id"])
|
||||||
|
except OndiloError:
|
||||||
|
_LOGGER.exception("Error communicating with API for %s", pool["id"])
|
||||||
|
continue
|
||||||
|
res.append(
|
||||||
|
{
|
||||||
|
**pool,
|
||||||
|
"ICO": ico,
|
||||||
|
"sensors": sensors,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if not res:
|
||||||
|
raise UpdateFailed("No data available")
|
||||||
|
return res
|
||||||
|
@ -5,7 +5,8 @@
|
|||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"dependencies": ["auth"],
|
"dependencies": ["auth"],
|
||||||
"documentation": "https://www.home-assistant.io/integrations/ondilo_ico",
|
"documentation": "https://www.home-assistant.io/integrations/ondilo_ico",
|
||||||
|
"integration_type": "hub",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["ondilo"],
|
"loggers": ["ondilo"],
|
||||||
"requirements": ["ondilo==0.4.0"]
|
"requirements": ["ondilo==0.5.0"]
|
||||||
}
|
}
|
||||||
|
@ -1450,7 +1450,7 @@ ollama-hass==0.1.7
|
|||||||
omnilogic==0.4.5
|
omnilogic==0.4.5
|
||||||
|
|
||||||
# homeassistant.components.ondilo_ico
|
# homeassistant.components.ondilo_ico
|
||||||
ondilo==0.4.0
|
ondilo==0.5.0
|
||||||
|
|
||||||
# homeassistant.components.onkyo
|
# homeassistant.components.onkyo
|
||||||
onkyo-eiscp==1.2.7
|
onkyo-eiscp==1.2.7
|
||||||
|
@ -1165,7 +1165,7 @@ ollama-hass==0.1.7
|
|||||||
omnilogic==0.4.5
|
omnilogic==0.4.5
|
||||||
|
|
||||||
# homeassistant.components.ondilo_ico
|
# homeassistant.components.ondilo_ico
|
||||||
ondilo==0.4.0
|
ondilo==0.5.0
|
||||||
|
|
||||||
# homeassistant.components.onvif
|
# homeassistant.components.onvif
|
||||||
onvif-zeep-async==3.1.12
|
onvif-zeep-async==3.1.12
|
||||||
|
@ -1 +1,17 @@
|
|||||||
"""Tests for the Ondilo ICO integration."""
|
"""Tests for the Ondilo ICO integration."""
|
||||||
|
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
|
|
||||||
|
async def setup_integration(
|
||||||
|
hass: HomeAssistant, config_entry: MockConfigEntry, mock_ondilo_client: MagicMock
|
||||||
|
) -> None:
|
||||||
|
"""Fixture for setting up the component."""
|
||||||
|
config_entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
84
tests/components/ondilo_ico/conftest.py
Normal file
84
tests/components/ondilo_ico/conftest.py
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
"""Provide basic Ondilo fixture."""
|
||||||
|
|
||||||
|
from collections.abc import Generator
|
||||||
|
from typing import Any
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from homeassistant.components.ondilo_ico.const import DOMAIN
|
||||||
|
|
||||||
|
from tests.common import (
|
||||||
|
MockConfigEntry,
|
||||||
|
load_json_array_fixture,
|
||||||
|
load_json_object_fixture,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(name="config_entry")
|
||||||
|
def mock_config_entry() -> MockConfigEntry:
|
||||||
|
"""Mock a config entry."""
|
||||||
|
return MockConfigEntry(
|
||||||
|
domain=DOMAIN,
|
||||||
|
title="Ondilo ICO",
|
||||||
|
data={"auth_implementation": DOMAIN, "token": {"access_token": "fake_token"}},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_ondilo_client(
|
||||||
|
two_pools: list[dict[str, Any]],
|
||||||
|
ico_details1: dict[str, Any],
|
||||||
|
ico_details2: dict[str, Any],
|
||||||
|
last_measures: list[dict[str, Any]],
|
||||||
|
) -> Generator[MagicMock, None, None]:
|
||||||
|
"""Mock a Homeassistant Ondilo client."""
|
||||||
|
with (
|
||||||
|
patch(
|
||||||
|
"homeassistant.components.ondilo_ico.api.OndiloClient",
|
||||||
|
autospec=True,
|
||||||
|
) as mock_ondilo,
|
||||||
|
):
|
||||||
|
client = mock_ondilo.return_value
|
||||||
|
client.get_pools.return_value = two_pools
|
||||||
|
client.get_ICO_details.side_effect = [ico_details1, ico_details2]
|
||||||
|
client.get_last_pool_measures.return_value = last_measures
|
||||||
|
yield client
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="session")
|
||||||
|
def pool1() -> list[dict[str, Any]]:
|
||||||
|
"""First pool description."""
|
||||||
|
return [load_json_object_fixture("pool1.json", DOMAIN)]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="session")
|
||||||
|
def pool2() -> list[dict[str, Any]]:
|
||||||
|
"""Second pool description."""
|
||||||
|
return [load_json_object_fixture("pool2.json", DOMAIN)]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="session")
|
||||||
|
def ico_details1() -> dict[str, Any]:
|
||||||
|
"""ICO details of first pool."""
|
||||||
|
return load_json_object_fixture("ico_details1.json", DOMAIN)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="session")
|
||||||
|
def ico_details2() -> dict[str, Any]:
|
||||||
|
"""ICO details of second pool."""
|
||||||
|
return load_json_object_fixture("ico_details2.json", DOMAIN)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="session")
|
||||||
|
def last_measures() -> list[dict[str, Any]]:
|
||||||
|
"""Pool measurements."""
|
||||||
|
return load_json_array_fixture("last_measures.json", DOMAIN)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="session")
|
||||||
|
def two_pools(
|
||||||
|
pool1: list[dict[str, Any]], pool2: list[dict[str, Any]]
|
||||||
|
) -> list[dict[str, Any]]:
|
||||||
|
"""Two pools description."""
|
||||||
|
return [*pool1, *pool2]
|
5
tests/components/ondilo_ico/fixtures/ico_details1.json
Normal file
5
tests/components/ondilo_ico/fixtures/ico_details1.json
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"uuid": "111112222233333444445555",
|
||||||
|
"serial_number": "W1122333044455",
|
||||||
|
"sw_version": "1.7.1-stable"
|
||||||
|
}
|
5
tests/components/ondilo_ico/fixtures/ico_details2.json
Normal file
5
tests/components/ondilo_ico/fixtures/ico_details2.json
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"uuid": "222223333344444555566666",
|
||||||
|
"serial_number": "W2233304445566",
|
||||||
|
"sw_version": "1.7.1-stable"
|
||||||
|
}
|
51
tests/components/ondilo_ico/fixtures/last_measures.json
Normal file
51
tests/components/ondilo_ico/fixtures/last_measures.json
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"data_type": "temperature",
|
||||||
|
"value": 19,
|
||||||
|
"value_time": "2024-01-01 01:00:00",
|
||||||
|
"is_valid": true,
|
||||||
|
"exclusion_reason": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"data_type": "ph",
|
||||||
|
"value": 9.29,
|
||||||
|
"value_time": "2024-01-01 01:00:00",
|
||||||
|
"is_valid": true,
|
||||||
|
"exclusion_reason": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"data_type": "orp",
|
||||||
|
"value": 647,
|
||||||
|
"value_time": "2024-01-01 01:00:00",
|
||||||
|
"is_valid": true,
|
||||||
|
"exclusion_reason": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"data_type": "salt",
|
||||||
|
"value": null,
|
||||||
|
"value_time": "2024-01-01 01:00:00",
|
||||||
|
"is_valid": true,
|
||||||
|
"exclusion_reason": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"data_type": "battery",
|
||||||
|
"value": 50,
|
||||||
|
"value_time": "2024-01-01 01:00:00",
|
||||||
|
"is_valid": true,
|
||||||
|
"exclusion_reason": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"data_type": "tds",
|
||||||
|
"value": 845,
|
||||||
|
"value_time": "2024-01-01 01:00:00",
|
||||||
|
"is_valid": true,
|
||||||
|
"exclusion_reason": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"data_type": "rssi",
|
||||||
|
"value": 60,
|
||||||
|
"value_time": "2024-01-01 01:00:00",
|
||||||
|
"is_valid": true,
|
||||||
|
"exclusion_reason": null
|
||||||
|
}
|
||||||
|
]
|
19
tests/components/ondilo_ico/fixtures/pool1.json
Normal file
19
tests/components/ondilo_ico/fixtures/pool1.json
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "Pool 1",
|
||||||
|
"type": "outdoor_inground_pool",
|
||||||
|
"volume": 100,
|
||||||
|
"disinfection": {
|
||||||
|
"primary": "chlorine",
|
||||||
|
"secondary": { "uv_sanitizer": false, "ozonator": false }
|
||||||
|
},
|
||||||
|
"address": {
|
||||||
|
"street": "1 Rue de Paris",
|
||||||
|
"zipcode": "75000",
|
||||||
|
"city": "Paris",
|
||||||
|
"country": "France",
|
||||||
|
"latitude": 48.861783,
|
||||||
|
"longitude": 2.337421
|
||||||
|
},
|
||||||
|
"updated_at": "2024-01-01T01:00:00+0000"
|
||||||
|
}
|
19
tests/components/ondilo_ico/fixtures/pool2.json
Normal file
19
tests/components/ondilo_ico/fixtures/pool2.json
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"name": "Pool 2",
|
||||||
|
"type": "outdoor_inground_pool",
|
||||||
|
"volume": 120,
|
||||||
|
"disinfection": {
|
||||||
|
"primary": "chlorine",
|
||||||
|
"secondary": { "uv_sanitizer": false, "ozonator": false }
|
||||||
|
},
|
||||||
|
"address": {
|
||||||
|
"street": "1 Rue de Paris",
|
||||||
|
"zipcode": "75000",
|
||||||
|
"city": "Paris",
|
||||||
|
"country": "France",
|
||||||
|
"latitude": 48.861783,
|
||||||
|
"longitude": 2.337421
|
||||||
|
},
|
||||||
|
"updated_at": "2024-01-01T01:00:00+0000"
|
||||||
|
}
|
31
tests/components/ondilo_ico/test_init.py
Normal file
31
tests/components/ondilo_ico/test_init.py
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
"""Test Ondilo ICO initialization."""
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigEntryState
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
|
from . import setup_integration
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
|
|
||||||
|
async def test_init_with_no_ico_attached(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_ondilo_client: MagicMock,
|
||||||
|
config_entry: MockConfigEntry,
|
||||||
|
pool1: dict[str, Any],
|
||||||
|
) -> None:
|
||||||
|
"""Test if an ICO is not attached to a pool, then no sensor is created."""
|
||||||
|
# Only one pool, but no ICO attached
|
||||||
|
mock_ondilo_client.get_pools.return_value = pool1
|
||||||
|
mock_ondilo_client.get_ICO_details.side_effect = None
|
||||||
|
mock_ondilo_client.get_ICO_details.return_value = None
|
||||||
|
await setup_integration(hass, config_entry, mock_ondilo_client)
|
||||||
|
|
||||||
|
# No sensor should be created
|
||||||
|
assert len(hass.states.async_all()) == 0
|
||||||
|
# We should not have tried to retrieve pool measures
|
||||||
|
mock_ondilo_client.get_last_pool_measures.assert_not_called()
|
||||||
|
assert config_entry.state is ConfigEntryState.SETUP_RETRY
|
83
tests/components/ondilo_ico/test_sensor.py
Normal file
83
tests/components/ondilo_ico/test_sensor.py
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
"""Test Ondilo ICO integration sensors."""
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
from ondilo import OndiloError
|
||||||
|
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
|
from . import setup_integration
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
|
|
||||||
|
async def test_can_get_pools_when_no_error(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_ondilo_client: MagicMock,
|
||||||
|
config_entry: MockConfigEntry,
|
||||||
|
) -> None:
|
||||||
|
"""Test that I can get all pools data when no error."""
|
||||||
|
await setup_integration(hass, config_entry, mock_ondilo_client)
|
||||||
|
|
||||||
|
# All sensors were created
|
||||||
|
assert len(hass.states.async_all()) == 14
|
||||||
|
|
||||||
|
# Check 2 of the sensors.
|
||||||
|
assert hass.states.get("sensor.pool_1_temperature").state == "19"
|
||||||
|
assert hass.states.get("sensor.pool_2_rssi").state == "60"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_no_ico_for_one_pool(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_ondilo_client: MagicMock,
|
||||||
|
config_entry: MockConfigEntry,
|
||||||
|
two_pools: list[dict[str, Any]],
|
||||||
|
ico_details2: dict[str, Any],
|
||||||
|
last_measures: list[dict[str, Any]],
|
||||||
|
) -> None:
|
||||||
|
"""Test if an ICO is not attached to a pool, then no sensor for that pool is created."""
|
||||||
|
mock_ondilo_client.get_pools.return_value = two_pools
|
||||||
|
mock_ondilo_client.get_ICO_details.side_effect = [None, ico_details2]
|
||||||
|
|
||||||
|
await setup_integration(hass, config_entry, mock_ondilo_client)
|
||||||
|
# Only the second pool is created
|
||||||
|
assert len(hass.states.async_all()) == 7
|
||||||
|
assert hass.states.get("sensor.pool_1_temperature") is None
|
||||||
|
assert hass.states.get("sensor.pool_2_rssi").state == next(
|
||||||
|
str(item["value"]) for item in last_measures if item["data_type"] == "rssi"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_error_retrieving_ico(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_ondilo_client: MagicMock,
|
||||||
|
config_entry: MockConfigEntry,
|
||||||
|
pool1: dict[str, Any],
|
||||||
|
) -> None:
|
||||||
|
"""Test if there's an error retrieving ICO data, then no sensor is created."""
|
||||||
|
mock_ondilo_client.get_pools.return_value = pool1
|
||||||
|
mock_ondilo_client.get_ICO_details.side_effect = OndiloError(400, "error")
|
||||||
|
|
||||||
|
await setup_integration(hass, config_entry, mock_ondilo_client)
|
||||||
|
|
||||||
|
# No sensor should be created
|
||||||
|
assert len(hass.states.async_all()) == 0
|
||||||
|
|
||||||
|
|
||||||
|
async def test_error_retrieving_measures(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_ondilo_client: MagicMock,
|
||||||
|
config_entry: MockConfigEntry,
|
||||||
|
pool1: dict[str, Any],
|
||||||
|
ico_details1: dict[str, Any],
|
||||||
|
) -> None:
|
||||||
|
"""Test if there's an error retrieving measures of ICO, then no sensor is created."""
|
||||||
|
mock_ondilo_client.get_pools.return_value = pool1
|
||||||
|
mock_ondilo_client.get_ICO_details.return_value = ico_details1
|
||||||
|
mock_ondilo_client.get_last_pool_measures.side_effect = OndiloError(400, "error")
|
||||||
|
|
||||||
|
await setup_integration(hass, config_entry, mock_ondilo_client)
|
||||||
|
|
||||||
|
# No sensor should be created
|
||||||
|
assert len(hass.states.async_all()) == 0
|
Loading…
x
Reference in New Issue
Block a user