Fix Tami4 component breaking API changes (#119158)

* fix tami4 api breaking changes

* fix tests
This commit is contained in:
Guy Shefer 2024-06-08 23:52:15 +03:00 committed by GitHub
parent d6097573f5
commit 7e1806229b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 63 additions and 66 deletions

View file

@ -10,7 +10,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryError, ConfigEntryNotReady from homeassistant.exceptions import ConfigEntryError, ConfigEntryNotReady
from .const import API, CONF_REFRESH_TOKEN, COORDINATOR, DOMAIN from .const import API, CONF_REFRESH_TOKEN, COORDINATOR, DOMAIN
from .coordinator import Tami4EdgeWaterQualityCoordinator from .coordinator import Tami4EdgeCoordinator
PLATFORMS: list[Platform] = [Platform.BUTTON, Platform.SENSOR] PLATFORMS: list[Platform] = [Platform.BUTTON, Platform.SENSOR]
@ -26,7 +26,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
except exceptions.TokenRefreshFailedException as ex: except exceptions.TokenRefreshFailedException as ex:
raise ConfigEntryNotReady("Error connecting to API") from ex raise ConfigEntryNotReady("Error connecting to API") from ex
coordinator = Tami4EdgeWaterQualityCoordinator(hass, api) coordinator = Tami4EdgeCoordinator(hass, api)
await coordinator.async_config_entry_first_refresh() await coordinator.async_config_entry_first_refresh()
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = { hass.data.setdefault(DOMAIN, {})[entry.entry_id] = {

View file

@ -83,7 +83,8 @@ class Tami4ConfigFlow(ConfigFlow, domain=DOMAIN):
errors["base"] = "unknown" errors["base"] = "unknown"
else: else:
return self.async_create_entry( return self.async_create_entry(
title=api.device.name, data={CONF_REFRESH_TOKEN: refresh_token} title=api.device_metadata.name,
data={CONF_REFRESH_TOKEN: refresh_token},
) )
return self.async_show_form( return self.async_show_form(

View file

@ -17,27 +17,23 @@ _LOGGER = logging.getLogger(__name__)
class FlattenedWaterQuality: class FlattenedWaterQuality:
"""Flattened WaterQuality dataclass.""" """Flattened WaterQuality dataclass."""
uv_last_replacement: date
uv_upcoming_replacement: date uv_upcoming_replacement: date
uv_status: str uv_installed: bool
filter_last_replacement: date
filter_upcoming_replacement: date filter_upcoming_replacement: date
filter_status: str filter_installed: bool
filter_litters_passed: float filter_litters_passed: float
def __init__(self, water_quality: WaterQuality) -> None: def __init__(self, water_quality: WaterQuality) -> None:
"""Flatten WaterQuality dataclass.""" """Flattened WaterQuality dataclass."""
self.uv_last_replacement = water_quality.uv.last_replacement
self.uv_upcoming_replacement = water_quality.uv.upcoming_replacement self.uv_upcoming_replacement = water_quality.uv.upcoming_replacement
self.uv_status = water_quality.uv.status self.uv_installed = water_quality.uv.installed
self.filter_last_replacement = water_quality.filter.last_replacement
self.filter_upcoming_replacement = water_quality.filter.upcoming_replacement self.filter_upcoming_replacement = water_quality.filter.upcoming_replacement
self.filter_status = water_quality.filter.status self.filter_installed = water_quality.filter.installed
self.filter_litters_passed = water_quality.filter.milli_litters_passed / 1000 self.filter_litters_passed = water_quality.filter.milli_litters_passed / 1000
class Tami4EdgeWaterQualityCoordinator(DataUpdateCoordinator[FlattenedWaterQuality]): class Tami4EdgeCoordinator(DataUpdateCoordinator[FlattenedWaterQuality]):
"""Tami4Edge water quality coordinator.""" """Tami4Edge water quality coordinator."""
def __init__(self, hass: HomeAssistant, api: Tami4EdgeAPI) -> None: def __init__(self, hass: HomeAssistant, api: Tami4EdgeAPI) -> None:
@ -53,10 +49,8 @@ class Tami4EdgeWaterQualityCoordinator(DataUpdateCoordinator[FlattenedWaterQuali
async def _async_update_data(self) -> FlattenedWaterQuality: async def _async_update_data(self) -> FlattenedWaterQuality:
"""Fetch data from the API endpoint.""" """Fetch data from the API endpoint."""
try: try:
water_quality = await self.hass.async_add_executor_job( device = await self.hass.async_add_executor_job(self._api.get_device)
self._api.get_water_quality
)
return FlattenedWaterQuality(water_quality) return FlattenedWaterQuality(device.water_quality)
except exceptions.APIRequestFailedException as ex: except exceptions.APIRequestFailedException as ex:
raise UpdateFailed("Error communicating with API") from ex raise UpdateFailed("Error communicating with API") from ex

View file

@ -21,14 +21,14 @@ class Tami4EdgeBaseEntity(Entity):
"""Initialize the Tami4Edge.""" """Initialize the Tami4Edge."""
self._state = None self._state = None
self._api = api self._api = api
device_id = api.device.psn device_id = api.device_metadata.psn
self.entity_description = entity_description self.entity_description = entity_description
self._attr_unique_id = f"{device_id}_{self.entity_description.key}" self._attr_unique_id = f"{device_id}_{self.entity_description.key}"
self._attr_device_info = DeviceInfo( self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, device_id)}, identifiers={(DOMAIN, device_id)},
manufacturer="Stratuss", manufacturer="Stratuss",
name=api.device.name, name=api.device_metadata.name,
model="Tami4", model="Tami4",
sw_version=api.device.device_firmware, sw_version=api.device_metadata.device_firmware,
suggested_area="Kitchen", suggested_area="Kitchen",
) )

View file

@ -5,5 +5,5 @@
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/tami4", "documentation": "https://www.home-assistant.io/integrations/tami4",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"requirements": ["Tami4EdgeAPI==2.1"] "requirements": ["Tami4EdgeAPI==3.0"]
} }

View file

@ -17,30 +17,20 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import API, COORDINATOR, DOMAIN from .const import API, COORDINATOR, DOMAIN
from .coordinator import Tami4EdgeWaterQualityCoordinator from .coordinator import Tami4EdgeCoordinator
from .entity import Tami4EdgeBaseEntity from .entity import Tami4EdgeBaseEntity
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
ENTITY_DESCRIPTIONS = [ ENTITY_DESCRIPTIONS = [
SensorEntityDescription(
key="uv_last_replacement",
translation_key="uv_last_replacement",
device_class=SensorDeviceClass.DATE,
),
SensorEntityDescription( SensorEntityDescription(
key="uv_upcoming_replacement", key="uv_upcoming_replacement",
translation_key="uv_upcoming_replacement", translation_key="uv_upcoming_replacement",
device_class=SensorDeviceClass.DATE, device_class=SensorDeviceClass.DATE,
), ),
SensorEntityDescription( SensorEntityDescription(
key="uv_status", key="uv_installed",
translation_key="uv_status", translation_key="uv_installed",
),
SensorEntityDescription(
key="filter_last_replacement",
translation_key="filter_last_replacement",
device_class=SensorDeviceClass.DATE,
), ),
SensorEntityDescription( SensorEntityDescription(
key="filter_upcoming_replacement", key="filter_upcoming_replacement",
@ -48,8 +38,8 @@ ENTITY_DESCRIPTIONS = [
device_class=SensorDeviceClass.DATE, device_class=SensorDeviceClass.DATE,
), ),
SensorEntityDescription( SensorEntityDescription(
key="filter_status", key="filter_installed",
translation_key="filter_status", translation_key="filter_installed",
), ),
SensorEntityDescription( SensorEntityDescription(
key="filter_litters_passed", key="filter_litters_passed",
@ -67,7 +57,7 @@ async def async_setup_entry(
"""Perform the setup for Tami4Edge.""" """Perform the setup for Tami4Edge."""
data = hass.data[DOMAIN][entry.entry_id] data = hass.data[DOMAIN][entry.entry_id]
api: Tami4EdgeAPI = data[API] api: Tami4EdgeAPI = data[API]
coordinator: Tami4EdgeWaterQualityCoordinator = data[COORDINATOR] coordinator: Tami4EdgeCoordinator = data[COORDINATOR]
async_add_entities( async_add_entities(
Tami4EdgeSensorEntity( Tami4EdgeSensorEntity(
@ -81,14 +71,14 @@ async def async_setup_entry(
class Tami4EdgeSensorEntity( class Tami4EdgeSensorEntity(
Tami4EdgeBaseEntity, Tami4EdgeBaseEntity,
CoordinatorEntity[Tami4EdgeWaterQualityCoordinator], CoordinatorEntity[Tami4EdgeCoordinator],
SensorEntity, SensorEntity,
): ):
"""Representation of the entity.""" """Representation of the entity."""
def __init__( def __init__(
self, self,
coordinator: Tami4EdgeWaterQualityCoordinator, coordinator: Tami4EdgeCoordinator,
api: Tami4EdgeAPI, api: Tami4EdgeAPI,
entity_description: SensorEntityDescription, entity_description: SensorEntityDescription,
) -> None: ) -> None:

View file

@ -7,8 +7,8 @@
"uv_upcoming_replacement": { "uv_upcoming_replacement": {
"name": "UV upcoming replacement" "name": "UV upcoming replacement"
}, },
"uv_status": { "uv_installed": {
"name": "UV status" "name": "UV installed"
}, },
"filter_last_replacement": { "filter_last_replacement": {
"name": "Filter last replacement" "name": "Filter last replacement"
@ -16,8 +16,8 @@
"filter_upcoming_replacement": { "filter_upcoming_replacement": {
"name": "Filter upcoming replacement" "name": "Filter upcoming replacement"
}, },
"filter_status": { "filter_installed": {
"name": "Filter status" "name": "Filter installed"
}, },
"filter_litters_passed": { "filter_litters_passed": {
"name": "Filter water passed" "name": "Filter water passed"

View file

@ -122,7 +122,7 @@ RtmAPI==0.7.2
SQLAlchemy==2.0.30 SQLAlchemy==2.0.30
# homeassistant.components.tami4 # homeassistant.components.tami4
Tami4EdgeAPI==2.1 Tami4EdgeAPI==3.0
# homeassistant.components.travisci # homeassistant.components.travisci
TravisPy==0.3.5 TravisPy==0.3.5

View file

@ -107,7 +107,7 @@ RtmAPI==0.7.2
SQLAlchemy==2.0.30 SQLAlchemy==2.0.30
# homeassistant.components.tami4 # homeassistant.components.tami4
Tami4EdgeAPI==2.1 Tami4EdgeAPI==3.0
# homeassistant.components.onvif # homeassistant.components.onvif
WSDiscovery==2.0.0 WSDiscovery==2.0.0

View file

@ -5,6 +5,7 @@ from unittest.mock import AsyncMock, MagicMock, patch
import pytest import pytest
from Tami4EdgeAPI.device import Device from Tami4EdgeAPI.device import Device
from Tami4EdgeAPI.device_metadata import DeviceMetadata
from Tami4EdgeAPI.water_quality import UV, Filter, WaterQuality from Tami4EdgeAPI.water_quality import UV, Filter, WaterQuality
from typing_extensions import Generator from typing_extensions import Generator
@ -32,17 +33,17 @@ async def create_config_entry(hass: HomeAssistant) -> MockConfigEntry:
@pytest.fixture @pytest.fixture
def mock_api(mock__get_devices, mock_get_water_quality): def mock_api(mock__get_devices_metadata, mock_get_device):
"""Fixture to mock all API calls.""" """Fixture to mock all API calls."""
@pytest.fixture @pytest.fixture
def mock__get_devices(request: pytest.FixtureRequest) -> Generator[None]: def mock__get_devices_metadata(request: pytest.FixtureRequest) -> Generator[None]:
"""Fixture to mock _get_devices which makes a call to the API.""" """Fixture to mock _get_devices which makes a call to the API."""
side_effect = getattr(request, "param", None) side_effect = getattr(request, "param", None)
device = Device( device_metadata = DeviceMetadata(
id=1, id=1,
name="Drink Water", name="Drink Water",
connected=True, connected=True,
@ -52,38 +53,49 @@ def mock__get_devices(request: pytest.FixtureRequest) -> Generator[None]:
) )
with patch( with patch(
"Tami4EdgeAPI.Tami4EdgeAPI.Tami4EdgeAPI._get_devices", "Tami4EdgeAPI.Tami4EdgeAPI.Tami4EdgeAPI._get_devices_metadata",
return_value=[device], return_value=[device_metadata],
side_effect=side_effect, side_effect=side_effect,
): ):
yield yield
@pytest.fixture @pytest.fixture
def mock_get_water_quality( def mock_get_device(
request: pytest.FixtureRequest, request: pytest.FixtureRequest,
) -> Generator[None]: ) -> Generator[None]:
"""Fixture to mock get_water_quality which makes a call to the API.""" """Fixture to mock get_device which makes a call to the API."""
side_effect = getattr(request, "param", None) side_effect = getattr(request, "param", None)
water_quality = WaterQuality( water_quality = WaterQuality(
uv=UV( uv=UV(
last_replacement=int(datetime.now().timestamp()),
upcoming_replacement=int(datetime.now().timestamp()), upcoming_replacement=int(datetime.now().timestamp()),
status="on", installed=True,
), ),
filter=Filter( filter=Filter(
last_replacement=int(datetime.now().timestamp()),
upcoming_replacement=int(datetime.now().timestamp()), upcoming_replacement=int(datetime.now().timestamp()),
status="on",
milli_litters_passed=1000, milli_litters_passed=1000,
installed=True,
), ),
) )
device_metadata = DeviceMetadata(
id=1,
name="Drink Water",
connected=True,
psn="psn",
type="type",
device_firmware="v1.1",
)
device = Device(
water_quality=water_quality, device_metadata=device_metadata, drinks=[]
)
with patch( with patch(
"Tami4EdgeAPI.Tami4EdgeAPI.Tami4EdgeAPI.get_water_quality", "Tami4EdgeAPI.Tami4EdgeAPI.Tami4EdgeAPI.get_device",
return_value=water_quality, return_value=device,
side_effect=side_effect, side_effect=side_effect,
): ):
yield yield

View file

@ -13,7 +13,7 @@ async def test_step_user_valid_number(
hass: HomeAssistant, hass: HomeAssistant,
mock_setup_entry, mock_setup_entry,
mock_request_otp, mock_request_otp,
mock__get_devices, mock__get_devices_metadata,
) -> None: ) -> None:
"""Test user step with valid phone number.""" """Test user step with valid phone number."""
@ -37,7 +37,7 @@ async def test_step_user_invalid_number(
hass: HomeAssistant, hass: HomeAssistant,
mock_setup_entry, mock_setup_entry,
mock_request_otp, mock_request_otp,
mock__get_devices, mock__get_devices_metadata,
) -> None: ) -> None:
"""Test user step with invalid phone number.""" """Test user step with invalid phone number."""
@ -66,7 +66,7 @@ async def test_step_user_exception(
hass: HomeAssistant, hass: HomeAssistant,
mock_setup_entry, mock_setup_entry,
mock_request_otp, mock_request_otp,
mock__get_devices, mock__get_devices_metadata,
expected_error, expected_error,
) -> None: ) -> None:
"""Test user step with exception.""" """Test user step with exception."""
@ -92,7 +92,7 @@ async def test_step_otp_valid(
mock_setup_entry, mock_setup_entry,
mock_request_otp, mock_request_otp,
mock_submit_otp, mock_submit_otp,
mock__get_devices, mock__get_devices_metadata,
) -> None: ) -> None:
"""Test user step with valid phone number.""" """Test user step with valid phone number."""
@ -134,7 +134,7 @@ async def test_step_otp_exception(
mock_setup_entry, mock_setup_entry,
mock_request_otp, mock_request_otp,
mock_submit_otp, mock_submit_otp,
mock__get_devices, mock__get_devices_metadata,
expected_error, expected_error,
) -> None: ) -> None:
"""Test user step with valid phone number.""" """Test user step with valid phone number."""

View file

@ -17,7 +17,7 @@ async def test_init_success(mock_api, hass: HomeAssistant) -> None:
@pytest.mark.parametrize( @pytest.mark.parametrize(
"mock_get_water_quality", [exceptions.APIRequestFailedException], indirect=True "mock_get_device", [exceptions.APIRequestFailedException], indirect=True
) )
async def test_init_with_api_error(mock_api, hass: HomeAssistant) -> None: async def test_init_with_api_error(mock_api, hass: HomeAssistant) -> None:
"""Test init with api error.""" """Test init with api error."""
@ -27,7 +27,7 @@ async def test_init_with_api_error(mock_api, hass: HomeAssistant) -> None:
@pytest.mark.parametrize( @pytest.mark.parametrize(
("mock__get_devices", "expected_state"), ("mock__get_devices_metadata", "expected_state"),
[ [
( (
exceptions.RefreshTokenExpiredException, exceptions.RefreshTokenExpiredException,
@ -38,7 +38,7 @@ async def test_init_with_api_error(mock_api, hass: HomeAssistant) -> None:
ConfigEntryState.SETUP_RETRY, ConfigEntryState.SETUP_RETRY,
), ),
], ],
indirect=["mock__get_devices"], indirect=["mock__get_devices_metadata"],
) )
async def test_init_error_raised( async def test_init_error_raised(
mock_api, hass: HomeAssistant, expected_state: ConfigEntryState mock_api, hass: HomeAssistant, expected_state: ConfigEntryState