From 60cc0f3175490f0476651a7b8b1bf73cb49add5c Mon Sep 17 00:00:00 2001 From: daxingplay Date: Sun, 3 Dec 2023 15:36:58 +0000 Subject: [PATCH 1/2] :boom: refactor code and test cases --- custom_components/vaillant_plus/__init__.py | 37 +- .../vaillant_plus/binary_sensor.py | 30 +- custom_components/vaillant_plus/client.py | 180 ++++------ custom_components/vaillant_plus/climate.py | 20 +- .../vaillant_plus/config_flow.py | 16 +- custom_components/vaillant_plus/const.py | 5 +- custom_components/vaillant_plus/entity.py | 6 +- custom_components/vaillant_plus/manifest.json | 4 +- custom_components/vaillant_plus/sensor.py | 8 +- .../vaillant_plus/water_heater.py | 10 +- tests/conftest.py | 103 ++---- tests/const.py | 152 +++++--- tests/test_binary_sensor.py | 20 +- tests/test_cliamte.py | 6 +- tests/test_client.py | 336 +++++++++--------- tests/test_init.py | 35 +- tests/test_water_heater.py | 2 +- 17 files changed, 467 insertions(+), 503 deletions(-) diff --git a/custom_components/vaillant_plus/__init__.py b/custom_components/vaillant_plus/__init__.py index 3e60ec5..4bdc22d 100644 --- a/custom_components/vaillant_plus/__init__.py +++ b/custom_components/vaillant_plus/__init__.py @@ -10,15 +10,14 @@ from homeassistant.helpers.typing import ConfigType from vaillant_plus_cn_api import Token -from .client import VaillantApiHub, VaillantDeviceApiClient +from .client import VaillantClient from .const import ( - API_HUB, + API_CLIENT, CONF_DID, CONF_TOKEN, DISPATCHERS, DOMAIN, EVT_TOKEN_UPDATED, - WEBSOCKET_CLIENT, ) # TODO List the platforms that you want to support. @@ -36,7 +35,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: hass.data.setdefault( DOMAIN, - {API_HUB: VaillantApiHub(hass), DISPATCHERS: {}, WEBSOCKET_CLIENT: {}}, + {API_CLIENT: {}, DISPATCHERS: {}}, ) return True @@ -44,20 +43,20 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Vaillant Plus from a config entry.""" - hub: VaillantApiHub = hass.data[DOMAIN][API_HUB] token = Token.deserialize(entry.data.get(CONF_TOKEN)) device_id = entry.data.get(CONF_DID) + client = VaillantClient(hass, token, device_id) + + async def close_client(_): + await client.close() + + hass.data[DOMAIN][API_CLIENT][entry.entry_id] = client @callback def on_token_update(token_new: Token) -> None: hass.config_entries.async_update_entry( entry, data={**entry.data, CONF_TOKEN: token_new.serialize()} ) - # update hub token - hub.update_token(token_new) - # should update VaillantDeviceApiClient - if entry.entry_id in hass.data[DOMAIN][WEBSOCKET_CLIENT]: - hass.loop.create_task(hass.data[DOMAIN][WEBSOCKET_CLIENT][entry.entry_id].update_token(token_new)) unsub = async_dispatcher_connect( hass, @@ -68,20 +67,12 @@ def on_token_update(token_new: Token) -> None: hass.data[DOMAIN][DISPATCHERS].setdefault(device_id, []) hass.data[DOMAIN][DISPATCHERS][device_id].append(unsub) - device = await hub.get_device(token, device_id) - client = VaillantDeviceApiClient(hass, hub, token, device) - - hass.data[DOMAIN][WEBSOCKET_CLIENT][entry.entry_id] = client - - def close_client(_): - return client.close() - unsub_stop = hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, close_client) hass.data[DOMAIN][DISPATCHERS][device_id].append(unsub_stop) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) - hass.loop.create_task(client.connect()) + hass.loop.create_task(client.start()) return True @@ -90,14 +81,14 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): if ( - entry.entry_id in hass.data[DOMAIN][WEBSOCKET_CLIENT] - and hass.data[DOMAIN][WEBSOCKET_CLIENT][entry.entry_id] is not None + entry.entry_id in hass.data[DOMAIN][API_CLIENT] + and hass.data[DOMAIN][API_CLIENT][entry.entry_id] is not None ): try: - await hass.data[DOMAIN][WEBSOCKET_CLIENT][entry.entry_id].close() + await hass.data[DOMAIN][API_CLIENT][entry.entry_id].close() except: pass - hass.data[DOMAIN][WEBSOCKET_CLIENT].pop(entry.entry_id) + hass.data[DOMAIN][API_CLIENT].pop(entry.entry_id) device_id = entry.data.get(CONF_DID) dispatchers = hass.data[DOMAIN][DISPATCHERS].pop(device_id) diff --git a/custom_components/vaillant_plus/binary_sensor.py b/custom_components/vaillant_plus/binary_sensor.py index 9858508..0c889c7 100644 --- a/custom_components/vaillant_plus/binary_sensor.py +++ b/custom_components/vaillant_plus/binary_sensor.py @@ -15,8 +15,8 @@ from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .client import VaillantDeviceApiClient -from .const import CONF_DID, DISPATCHERS, DOMAIN, EVT_DEVICE_CONNECTED, WEBSOCKET_CLIENT +from .client import VaillantClient +from .const import CONF_DID, DISPATCHERS, DOMAIN, EVT_DEVICE_CONNECTED, API_CLIENT from .entity import VaillantEntity _LOGGER = logging.getLogger(__name__) @@ -42,56 +42,56 @@ class VaillantBinarySensorDescription( name="Circulation", device_class=BinarySensorDeviceClass.RUNNING, entity_category=EntityCategory.DIAGNOSTIC, - on_state=True, + on_state=1, ), VaillantBinarySensorDescription( key="Heating_Enable", name="Heating", device_class=BinarySensorDeviceClass.RUNNING, entity_category=EntityCategory.DIAGNOSTIC, - on_state=True, + on_state=1, ), VaillantBinarySensorDescription( key="WarmStar_Tank_Loading_Enable", name="WarmStar tank loading", device_class=BinarySensorDeviceClass.RUNNING, entity_category=EntityCategory.DIAGNOSTIC, - on_state=True, + on_state=1, ), VaillantBinarySensorDescription( key="Enabled_Heating", name="Heating boiler", device_class=BinarySensorDeviceClass.RUNNING, entity_category=EntityCategory.DIAGNOSTIC, - on_state=True, + on_state=1, ), VaillantBinarySensorDescription( key="Enabled_DHW", name="Domestic hot water", device_class=BinarySensorDeviceClass.RUNNING, entity_category=EntityCategory.DIAGNOSTIC, - on_state=True, + on_state=1, ), VaillantBinarySensorDescription( key="BMU_Platform", name="BMU platform", # device_class=BinarySensorDeviceClass.RUNNING, entity_category=EntityCategory.DIAGNOSTIC, - on_state=True, + on_state=1, ), VaillantBinarySensorDescription( key="Weather_compensation", name="Weather compensation", device_class=BinarySensorDeviceClass.RUNNING, entity_category=EntityCategory.DIAGNOSTIC, - on_state=True, + on_state=1, ), VaillantBinarySensorDescription( key="RF_Status", name="EBus status", device_class=BinarySensorDeviceClass.CONNECTIVITY, entity_category=EntityCategory.DIAGNOSTIC, - on_state=True, + on_state=3, ), VaillantBinarySensorDescription( key="Boiler_info3_bit0", @@ -115,7 +115,7 @@ async def async_setup_entry( ) -> bool: """Set up Vaillant binary sensors.""" device_id = entry.data.get(CONF_DID) - client: VaillantDeviceApiClient = hass.data[DOMAIN][WEBSOCKET_CLIENT][ + client: VaillantClient = hass.data[DOMAIN][API_CLIENT][ entry.entry_id ] @@ -154,7 +154,7 @@ class VaillantBinarySensorEntity(VaillantEntity, BinarySensorEntity): def __init__( self, - client: VaillantDeviceApiClient, + client: VaillantClient, description: VaillantBinarySensorDescription, ): super().__init__(client) @@ -174,8 +174,10 @@ def update_from_latest_data(self, data: dict[str, Any]) -> None: if self.entity_description.key == "RF_Status": self._attr_is_on = value == 3 elif self.entity_description.key == "Boiler_info3_bit0": - self._attr_is_on = value[0] == 1 + self._attr_is_on = value.startswith("1") elif self.entity_description.key == "Boiler_info5_bit4": - self._attr_is_on = value[0] == 1 + self._attr_is_on = value.startswith("1") + elif self.entity_description.on_state is not None: + self._attr_is_on = value == self.entity_description.on_state else: self._attr_is_on = value is True diff --git a/custom_components/vaillant_plus/client.py b/custom_components/vaillant_plus/client.py index 118fbd2..6774e36 100644 --- a/custom_components/vaillant_plus/client.py +++ b/custom_components/vaillant_plus/client.py @@ -22,92 +22,29 @@ _LOGGER = logging.getLogger(__name__) - -class VaillantApiHub: - """API hub for authentication to the cloud.""" - - def __init__(self, hass: HomeAssistant, *, retry_interval: int = 3) -> None: - """Initialize.""" - self._hass = hass - self._api_client = VaillantApiClient( - session=aiohttp_client.async_get_clientsession(self._hass) - ) - self._retry_interval = retry_interval - - def update_token(self, token: Token) -> None: - """Update token.""" - self._api_client.update_token(token) - - async def login(self, username: str, password: str) -> Token: - """Login to get uid and token.""" - return await self._api_client.login(username=username, password=password) - - async def get_device_list(self) -> list[Device]: - """Get device list.""" - return await self._api_client.get_device_list() - - async def control_device(self, device_id, attr, value) -> None: - """Send command to control device.""" - return await self._api_client.control_device(device_id, attr, value) - - async def get_device(self, token: Token, device_id: str) -> Device: - device_list: list[Device] = [] - device: Device | None = None - succeed = False - retry_times = 0 - - token_used = token - self.update_token(token_used) - - while not succeed: - if retry_times > 3: - raise ShouldUpdateConfigEntry - - try: - device_list = await self.get_device_list() - for item in device_list: - if item.id == device_id: - device = item - break - - if device is None: - raise ShouldUpdateConfigEntry - - succeed = True - return device - except InvalidAuthError: - token_new = await self.login(token_used.username, token_used.password) - self.update_token(token_new) - token_used = token_new - async_dispatcher_send( - self._hass, EVT_TOKEN_UPDATED.format(token.username), token_new - ) - succeed = False - retry_times += 1 - await asyncio.sleep(self._retry_interval) - - -class VaillantDeviceApiClient: - """Client to connect to device's cloud websocket endpoint.""" +class VaillantClient: + """API client for communicating with the cloud.""" def __init__( self, hass: HomeAssistant, - hub: VaillantApiHub, token: Token, - device: Device, + device_id: str, ) -> None: - """Initialize.""" - self._hub = hub self._hass = hass - self._device = device - self._token = token + self._device_id = device_id self._device_attrs: dict[str, Any] = {} - self._client: VaillantWebsocketClient = self._init_client(token) + self._device: Device | None = None + self._token = token - @property - def client(self) -> VaillantWebsocketClient: - return self._client + session = aiohttp_client.async_get_clientsession(self._hass) + self._api_client = VaillantApiClient(session=session) + + self._websocket_client: VaillantWebsocketClient | None = None + + self._failed_attempts: int = 0 + + self._state = "INITED" @property def device(self) -> Device: @@ -117,69 +54,78 @@ def device(self) -> Device: def device_attrs(self) -> dict[str, Any]: return self._device_attrs - def _init_client(self, token: Token) -> VaillantWebsocketClient: - client = VaillantWebsocketClient( - token, - self._device, - session=aiohttp_client.async_get_clientsession(self._hass), - ) + async def _connect(self) -> None: + device_list = await self._api_client.get_device_list() + filtered_device_list = [device for device in device_list if device.id == self._device_id] + if len(filtered_device_list) == 0: + raise ShouldUpdateConfigEntry + + self._device = filtered_device_list[0] + + if self._websocket_client is not None: + try: + await self._websocket_client.close() + except: + pass @callback def device_connected(device_attrs: dict[str, Any]): self._device_attrs = device_attrs.copy() async_dispatcher_send( - self._hass, EVT_DEVICE_CONNECTED.format(self._device.id), device_attrs + self._hass, EVT_DEVICE_CONNECTED.format(self._device_id), device_attrs.copy() ) @callback def device_update(event: str, data: dict[str, Any]): if event == EVT_DEVICE_ATTR_UPDATE: - device_attrs = data.get("data", {}) + device_attrs: dict[str, Any] = data.get("data", {}) if len(device_attrs) > 0: self._device_attrs = device_attrs.copy() async_dispatcher_send( - self._hass, EVT_DEVICE_UPDATED.format(self._device.id), self._device_attrs + self._hass, EVT_DEVICE_UPDATED.format(self._device.id), device_attrs.copy() ) - client.on_subscribe(device_connected) - client.on_update(device_update) - return client + self._websocket_client = VaillantWebsocketClient( + token=self._token, + device=self._device, + session=aiohttp_client.async_get_clientsession(self._hass), + ) + self._websocket_client.on_subscribe(device_connected) + self._websocket_client.on_update(device_update) - async def update_token(self, token: Token) -> None: - if not Token.equals(self._token, token): - await self.close() - self._token = token - self._client = self._init_client(token) - await asyncio.sleep(5) - await self.connect() - else: - _LOGGER.info("Token is not changed.") - - async def connect(self) -> None: - """Connect to cloud. Try to retrieve a new token if token expires.""" - try: - await self._client.listen() - except InvalidAuthError: - token_new = await self._hub.login( - self._token.username, self._token.password - ) - async_dispatcher_send( - self._hass, EVT_TOKEN_UPDATED.format(token_new.username), token_new - ) + await self._websocket_client.connect() + + async def start(self) -> None: + """Start connection to cloud.""" + while self._state != "CLOSED": + try: + await self._connect() + except InvalidAuthError: + _LOGGER.info("Token expired, retrieve new token...") + token_new = await self._api_client.login(self._token.username, self._token.password) + self._token = token_new + self._api_client.update_token(token_new) + async_dispatcher_send( + self._hass, EVT_TOKEN_UPDATED.format(token_new.username), token_new + ) + except Exception as error: + _LOGGER.warning("Unhandled client exception: %s", error) - async def send_command(self, attr: str, value: Any) -> None: - """Send command about operations for a device to the cloud.""" - if self._hub is not None: - await self._hub.control_device(self._device.id, attr, value) + await asyncio.sleep(5) - async def close(self): + async def close(self) -> None: """Close connection to cloud.""" - if self._client is not None: + if self._websocket_client is not None: try: - await self._client.close() + await self._websocket_client.close() except Exception as error: _LOGGER.exception(error) pass + self._state = "CLOSED" + + async def control_device(self, attr, value) -> None: + """Send command to control device.""" + return await self._api_client.control_device(self._device_id, attr, value) class InvalidAuth(HomeAssistantError): diff --git a/custom_components/vaillant_plus/climate.py b/custom_components/vaillant_plus/climate.py index 13312a6..60a6a58 100644 --- a/custom_components/vaillant_plus/climate.py +++ b/custom_components/vaillant_plus/climate.py @@ -17,8 +17,8 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .client import VaillantDeviceApiClient -from .const import CONF_DID, DISPATCHERS, DOMAIN, EVT_DEVICE_CONNECTED, WEBSOCKET_CLIENT +from .client import VaillantClient +from .const import CONF_DID, DISPATCHERS, DOMAIN, EVT_DEVICE_CONNECTED, API_CLIENT from .entity import VaillantEntity _LOGGER = logging.getLogger(__name__) @@ -41,7 +41,7 @@ async def async_setup_entry( """Set up Vaillant devices from a config entry.""" device_id = entry.data.get(CONF_DID) - client: VaillantDeviceApiClient = hass.data[DOMAIN][WEBSOCKET_CLIENT][ + client: VaillantClient = hass.data[DOMAIN][API_CLIENT][ entry.entry_id ] @@ -127,7 +127,7 @@ def hvac_mode(self) -> HVACMode: """ # TODO whether support HVACMode.AUTO - if self.get_device_attr("Enabled_Heating"): + if self.get_device_attr("Enabled_Heating") == 1: return HVACMode.HEAT return HVACMode.OFF @@ -138,7 +138,7 @@ def hvac_action(self) -> HVACAction: Return the currently running HVAC action. """ - if not self.get_device_attr("Enabled_Heating"): + if self.get_device_attr("Enabled_Heating") == 0: return HVACAction.OFF try: @@ -169,14 +169,14 @@ async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: _LOGGER.debug("Setting HVAC mode to: %s", hvac_mode) if hvac_mode == HVACMode.OFF: - await self._client.send_command( + await self._client.control_device( "Heating_Enable", - False, + 0, ) elif hvac_mode == HVACMode.HEAT: - await self._client.send_command( + await self._client.control_device( "Heating_Enable", - True, + 1, ) async def async_set_preset_mode(self, preset_mode: str) -> None: @@ -195,7 +195,7 @@ async def async_set_temperature(self, **kwargs) -> None: _LOGGER.debug("Setting target temperature to: %s", new_temperature) - await self._client.send_command( + await self._client.control_device( "Room_Temperature_Setpoint_Comfort", new_temperature, ) diff --git a/custom_components/vaillant_plus/config_flow.py b/custom_components/vaillant_plus/config_flow.py index 5656aaa..b2cfec4 100644 --- a/custom_components/vaillant_plus/config_flow.py +++ b/custom_components/vaillant_plus/config_flow.py @@ -7,10 +7,14 @@ from homeassistant import config_entries from homeassistant.data_entry_flow import FlowResult from homeassistant.exceptions import HomeAssistantError -from vaillant_plus_cn_api import Device, Token +from homeassistant.helpers import aiohttp_client +from vaillant_plus_cn_api import ( + Device, + Token, + VaillantApiClient, +) import voluptuous as vol -from .client import VaillantApiHub from .const import ( CONF_DID, CONF_PASSWORD, @@ -53,10 +57,12 @@ async def async_step_user( errors = {} - hub = VaillantApiHub(self.hass) + client = VaillantApiClient( + session=aiohttp_client.async_get_clientsession(self.hass) + ) try: - user_info = await hub.login( + user_info = await client.login( user_input[CONF_USERNAME], user_input[CONF_PASSWORD] ) except Exception: @@ -64,7 +70,7 @@ async def async_step_user( errors["base"] = "invalid_auth" else: self._cloud_token = user_info - device_list = await hub.get_device_list() + device_list = await client.get_device_list() if len(device_list) == 0: errors["base"] = "no_devices" else: diff --git a/custom_components/vaillant_plus/const.py b/custom_components/vaillant_plus/const.py index 92070bc..fd032c4 100644 --- a/custom_components/vaillant_plus/const.py +++ b/custom_components/vaillant_plus/const.py @@ -1,12 +1,9 @@ """Constants for the Vaillant Plus integration.""" DOMAIN = "vaillant_plus" -API_HUB = "hub" -WEBSOCKET_CLIENT = "websockets" +API_CLIENT = "client" DISPATCHERS = "dispatchers" -HOST_API = "https://api.vaillant.com.cn" - CONF_USERNAME = "username" CONF_PASSWORD = "password" diff --git a/custom_components/vaillant_plus/entity.py b/custom_components/vaillant_plus/entity.py index e8d2789..42a0b20 100644 --- a/custom_components/vaillant_plus/entity.py +++ b/custom_components/vaillant_plus/entity.py @@ -8,7 +8,7 @@ from homeassistant.helpers.entity import Entity, DeviceInfo from vaillant_plus_cn_api import Device -from .client import VaillantDeviceApiClient +from .client import VaillantClient from .const import DOMAIN, EVT_DEVICE_UPDATED UPDATE_INTERVAL = timedelta(minutes=1) @@ -21,7 +21,7 @@ class VaillantEntity(Entity): def __init__( self, - client: VaillantDeviceApiClient, + client: VaillantClient, ): """Initialize.""" self._client = client @@ -81,4 +81,4 @@ def update_from_latest_data(self, data: dict[str, Any]) -> None: async def send_command(self, attr: str, value: Any) -> None: """Send operations to cloud.""" - await self._client.send_command(attr, value) + await self._client.control_device(attr, value) diff --git a/custom_components/vaillant_plus/manifest.json b/custom_components/vaillant_plus/manifest.json index 2ef1ba8..be26c66 100644 --- a/custom_components/vaillant_plus/manifest.json +++ b/custom_components/vaillant_plus/manifest.json @@ -14,9 +14,9 @@ "vaillant_plus_cn_api" ], "requirements": [ - "vaillant-plus-cn-api==1.2.7" + "vaillant-plus-cn-api==1.2.8" ], "ssdp": [], - "version": "1.0.0", + "version": "1.1.0", "zeroconf": [] } \ No newline at end of file diff --git a/custom_components/vaillant_plus/sensor.py b/custom_components/vaillant_plus/sensor.py index 3ccb473..5eca3f7 100644 --- a/custom_components/vaillant_plus/sensor.py +++ b/custom_components/vaillant_plus/sensor.py @@ -15,8 +15,8 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .client import VaillantDeviceApiClient -from .const import CONF_DID, DISPATCHERS, DOMAIN, EVT_DEVICE_CONNECTED, WEBSOCKET_CLIENT +from .client import VaillantClient +from .const import CONF_DID, DISPATCHERS, DOMAIN, EVT_DEVICE_CONNECTED, API_CLIENT from .entity import VaillantEntity _LOGGER = logging.getLogger(__name__) @@ -115,7 +115,7 @@ async def async_setup_entry( ) -> bool: """Set up Vaillant sensors.""" device_id = entry.data.get(CONF_DID) - client: VaillantDeviceApiClient = hass.data[DOMAIN][WEBSOCKET_CLIENT][ + client: VaillantClient = hass.data[DOMAIN][API_CLIENT][ entry.entry_id ] @@ -150,7 +150,7 @@ class VaillantSensorEntity(VaillantEntity, SensorEntity): def __init__( self, - client: VaillantDeviceApiClient, + client: VaillantClient, description: SensorEntityDescription, ): super().__init__(client) diff --git a/custom_components/vaillant_plus/water_heater.py b/custom_components/vaillant_plus/water_heater.py index 6070d3a..a001110 100644 --- a/custom_components/vaillant_plus/water_heater.py +++ b/custom_components/vaillant_plus/water_heater.py @@ -14,7 +14,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .client import VaillantDeviceApiClient +from .client import VaillantClient from .const import ( CONF_DID, DISPATCHERS, @@ -22,7 +22,7 @@ EVT_DEVICE_CONNECTED, WATER_HEATER_OFF, WATER_HEATER_ON, - WEBSOCKET_CLIENT, + API_CLIENT, ) from .entity import VaillantEntity @@ -44,7 +44,7 @@ async def async_setup_entry( """Set up Vaillant devices from a config entry.""" device_id = entry.data.get(CONF_DID) - client: VaillantDeviceApiClient = hass.data[DOMAIN][WEBSOCKET_CLIENT][ + client: VaillantClient = hass.data[DOMAIN][API_CLIENT][ entry.entry_id ] @@ -127,9 +127,9 @@ def operation_list(self) -> list[str] | None: @property def current_temperature(self) -> float: - """Return the current dhw temperature. FIXME""" + """Return the current dhw temperature.""" - return self.get_device_attr("Tank_Temperature") + return self.get_device_attr("Flow_temperature") @property def target_temperature(self) -> float: diff --git a/tests/conftest.py b/tests/conftest.py index d7a3f10..e8ff667 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -20,11 +20,7 @@ import pytest from vaillant_plus_cn_api import Device, InvalidAuthError, Token -from custom_components.vaillant_plus import ( - VaillantApiHub, - VaillantDeviceApiClient, -) - +from custom_components.vaillant_plus import VaillantClient from .const import MOCK_PASSWORD, MOCK_USERNAME pytest_plugins = "pytest_homeassistant_custom_component" @@ -55,7 +51,7 @@ def skip_notifications_fixture(): def bypass_get_data_fixture(): """Skip calls to get data from API.""" with patch( - "custom_components.vaillant_plus.IntegrationBlueprintApiClient.async_get_data" + "custom_components.vaillant_plus.VaillantClient.async_get_data" ): yield @@ -64,12 +60,12 @@ def bypass_get_data_fixture(): def bypass_login_fixture(): """Skip calls to get data from API.""" with patch( - "custom_components.vaillant_plus.VaillantApiHub.login", + "vaillant_plus_cn_api.VaillantApiClient.login", return_value=Token( app_id="1", username=MOCK_USERNAME, password=MOCK_PASSWORD, - token="test_token", + access_token="test_token", uid="u1", ), ): @@ -80,21 +76,25 @@ def bypass_login_fixture(): def bypass_get_device_fixture(): """Skip calls to get data from API.""" with patch( - "custom_components.vaillant_plus.VaillantApiHub.get_device_list", + "vaillant_plus_cn_api.VaillantApiClient.get_device_list", return_value=[ Device( id="1", mac="mac2", product_key="pk", + product_id="p1", product_name="pn", - host="127.0.0.1", - ws_port=8080, - wss_port=8081, - wifi_soft_version="wsv1", - wifi_hard_version="whv1", - mcu_soft_version="msv1", - mcu_hard_version="mhv1", + product_verbose_name="pvn", is_online=True, + is_manager=True, + group_id=2, + sno="sno", + create_time="2000-01-01 00:00:00", + last_offline_time="2000-12-31 00:00:00", + model_alias="weijingling", + model="model_name", + serial_number="s1", + services_count=0, ) ], ): @@ -105,45 +105,19 @@ def bypass_get_device_fixture(): def bypass_get_no_device_fixture(): """Skip calls to get data from API.""" with patch( - "custom_components.vaillant_plus.VaillantApiHub.get_device_list", + "vaillant_plus_cn_api.VaillantApiClient.get_device_list", return_value=[], ): yield -@pytest.fixture(name="bypass_get_device_info") -def bypass_get_device_info_fixture(): - """Skip calls to get data from API.""" - with patch( - "custom_components.vaillant_plus.VaillantApiHub.get_device", - return_value=Device( - id="1", - mac="mac2", - product_key="pk", - product_name="pn", - host="127.0.0.1", - ws_port=8080, - wss_port=8081, - wifi_soft_version="wsv1", - wifi_hard_version="whv1", - mcu_soft_version="msv1", - mcu_hard_version="mhv1", - is_online=True, - model="test_model", - sno="test_sno", - serial_number="test_sn", - ), - ): - yield - - # In this fixture, we are forcing calls to login to raise an Exception. This is useful # for exception handling. @pytest.fixture(name="error_on_login") def error_login_fixture(): """Simulate error when retrieving data from API.""" with patch( - "custom_components.vaillant_plus.VaillantApiHub.login", + "custom_components.vaillant_plus.VaillantClient._connect", side_effect=Exception, ): yield @@ -154,7 +128,7 @@ def error_login_fixture(): def error_invaild_auth_when_get_device_list_fixture(): """Simulate error when retrieving data from API.""" with patch( - "custom_components.vaillant_plus.VaillantApiHub.get_device_list", + "custom_components.vaillant_plus.VaillantClient.get_device_list", side_effect=InvalidAuthError, ): yield @@ -163,26 +137,27 @@ def error_invaild_auth_when_get_device_list_fixture(): # Mock VaillantDeviceApiClient @pytest.fixture(name="device_api_client") def device_api_client_fixture(hass): - device_api_client = VaillantDeviceApiClient( + device_api_client = VaillantClient( hass=hass, - hub=VaillantApiHub(hass=hass), token=Token("a1", "u1", "p1"), - device=Device( - id="1", - mac="mac2", - product_key="pk", - product_name="pn", - host="127.0.0.1", - ws_port=8080, - wss_port=8081, - wifi_soft_version="wsv1", - wifi_hard_version="whv1", - mcu_soft_version="msv1", - mcu_hard_version="mhv1", - is_online=True, - model="test_model", - sno="test_sno", - serial_number="test_sn", - ), + device_id="1", + ) + device_api_client._device = Device( + id="1", + mac="mac2", + product_key="pk", + product_id="p1", + product_name="pn", + product_verbose_name="pvn", + is_online=True, + is_manager=True, + group_id=2, + sno="sno", + create_time="2000-01-01 00:00:00", + last_offline_time="2000-12-31 00:00:00", + model_alias="weijingling", + model="model_name", + serial_number="s1", + services_count=0, ) yield device_api_client diff --git a/tests/const.py b/tests/const.py index f4c492c..a71d324 100644 --- a/tests/const.py +++ b/tests/const.py @@ -17,77 +17,121 @@ CONF_TOKEN: "eyJhcHBfaWQiOiAiMSIsICJ1c2VybmFtZSI6ICJ0ZXN0X3VzZXJuYW1lIiwgInBhc3N3b3JkIjogInRlc3RfcGFzc3dvcmQiLCAidG9rZW4iOiAidGVzdF90b2tlbiIsICJ1aWQiOiAidTEifQ==", } MOCK_DEVICE_ATTRS_WHEN_CONNECT = { - "Brand": "vaillant on desk", + "WarmStar_Tank_Loading_Enable": 1, + "Fault_List": "00000000000000000000", + "Lower_Limitation_of_CH_Setpoint": 30, + "Upper_Limitation_of_DHW_Setpoint": 65, + "Circulation_Enable": 0, + "Room_Temperature_Setpoint_ECO": 5, + "Tank_temperature": 127.5, + "status_time": 1231234324, + "Flow_temperature": 33.5, + "DSN": 1500, "Time_slot_type": "CH", - "Heating_System_Setting": "radiator", - "DHW_Function": "none", "Mode_Setting_DHW": "Cruising", + "Maintenance": "00000000000000000000", + "Weather_compensation": 1, + "Outdoor_Temperature": 0, + "Lower_Limitation_of_DHW_Setpoint": 35, + "Flow_Temperature_Setpoint": 0, + "Max_NumBer_Of_Timeslots_CH": 0, + "Brand": "vaillant on desk", "Mode_Setting_CH": "Cruising", - "Weather_compensation": True, - "BMU_Platform": True, - "Enabled_DHW": True, - "Enabled_Heating": False, - "WarmStar_Tank_Loading_Enable": True, - "Heating_Enable": False, - "Circulation_Enable": False, - "Heating_Curve": 1, - "Max_NumBer_Of_Timeslots_DHW": 0, + "Boiler_info3_bit0": "00", + "Heating_System_Setting": "radiator", + "Room_Temperature_Setpoint_Comfort": 5.5, + "reserved_data2": "00", + "BMU_Platform": 1, + "Heating_Enable": 0, + "reserved_data1": "00", + "reserved_data3": "00", "Slot_current_DHW": 0, - "Max_NumBer_Of_Timeslots_CH": 0, - "Slot_current_CH": 0, - "Room_Temperature_Setpoint_Comfort": 1, - "Room_Temperature_Setpoint_ECO": 15, - "Outdoor_Temperature": 1, - "Room_Temperature": 20.5, - "DHW_setpoint": 45, - "Lower_Limitation_of_CH_Setpoint": 30, + "Start_Time_DHW1": "000000000000000000000000", + "Room_Temperature": 18.5, + "Start_Time_DHW3": "000000000000000000000000", + "Start_Time_DHW2": "000000000000000000000000", + "Start_Time_DHW5": "000000000000000000000000", + "Start_Time_DHW4": "000000000000000000000000", + "Start_Time_DHW7": "000000000000000000000000", + "Start_Time_DHW6": "000000000000000000000000", + "Enabled_Heating": 0, + "Enabled_DHW": 1, + "Start_Time_CH1": "000000000000000000000000", "Upper_Limitation_of_CH_Setpoint": 75, - "Lower_Limitation_of_DHW_Setpoint": 35, - "Upper_Limitation_of_DHW_Setpoint": 65, - "Current_DHW_Setpoint": 45, + "Slot_current_CH": 0, + "Start_Time_CH7": "000000000000000000000000", + "Max_NumBer_Of_Timeslots_DHW": 0, + "Start_Time_CH6": "000000000000000000000000", + "Start_Time_CH5": "000000000000000000000000", + "Start_Time_CH4": "000000000000000000000000", "RF_Status": 3, - "Flow_Temperature_Setpoint": 0, - "Flow_temperature": 26, + "Start_Time_CH3": "000000000000000000000000", + "Start_Time_CH2": "000000000000000000000000", "return_temperature": 0, - "DSN": 1500, - "Tank_temperature": 127.5, + "DHW_Function": "none", + "Current_DHW_Setpoint": 45, + "Boiler_info5_bit4": "00", + "DHW_setpoint": 45, + "Heating_Curve": 1 } MOCK_DEVICE_ATTRS_WHEN_UPDATE = { - "Brand": "vaillant on desk", + "WarmStar_Tank_Loading_Enable": 1, + "Fault_List": "00000000000000000000", + "Lower_Limitation_of_CH_Setpoint": 30, + "Upper_Limitation_of_DHW_Setpoint": 65, + "Circulation_Enable": 0, + "Room_Temperature_Setpoint_ECO": 5, + "Tank_temperature": 127.5, + "status_time": 3463532413245, + "Flow_temperature": 55.5, + "DSN": 1500, "Time_slot_type": "CH", - "Heating_System_Setting": "radiator", - "DHW_Function": "none", "Mode_Setting_DHW": "Cruising", + "Maintenance": "00000000000000000000", + "Weather_compensation": 1, + "Outdoor_Temperature": 0, + "Lower_Limitation_of_DHW_Setpoint": 35, + "Flow_Temperature_Setpoint": 0, + "Max_NumBer_Of_Timeslots_CH": 0, + "Brand": "vaillant on desk", "Mode_Setting_CH": "Cruising", - "Weather_compensation": True, - "BMU_Platform": True, - "Enabled_DHW": True, - "Enabled_Heating": True, - "WarmStar_Tank_Loading_Enable": True, - "Heating_Enable": True, - "Circulation_Enable": False, - "Heating_Curve": 1, - "Max_NumBer_Of_Timeslots_DHW": 0, + "Boiler_info3_bit0": "00", + "Heating_System_Setting": "radiator", + "Room_Temperature_Setpoint_Comfort": 22.5, + "reserved_data2": "00", + "BMU_Platform": 1, + "Heating_Enable": 1, + "reserved_data1": "00", + "reserved_data3": "00", "Slot_current_DHW": 0, - "Max_NumBer_Of_Timeslots_CH": 0, - "Slot_current_CH": 0, - "Room_Temperature_Setpoint_Comfort": 18.5, - "Room_Temperature_Setpoint_ECO": 15, - "Outdoor_Temperature": 10, - "Room_Temperature": 11.5, - "DHW_setpoint": 46, - "Lower_Limitation_of_CH_Setpoint": 30, + "Start_Time_DHW1": "000000000000000000000000", + "Room_Temperature": 20.5, + "Start_Time_DHW3": "000000000000000000000000", + "Start_Time_DHW2": "000000000000000000000000", + "Start_Time_DHW5": "000000000000000000000000", + "Start_Time_DHW4": "000000000000000000000000", + "Start_Time_DHW7": "000000000000000000000000", + "Start_Time_DHW6": "000000000000000000000000", + "Enabled_Heating": 1, + "Enabled_DHW": 1, + "Start_Time_CH1": "000000000000000000000000", "Upper_Limitation_of_CH_Setpoint": 75, - "Lower_Limitation_of_DHW_Setpoint": 35, - "Upper_Limitation_of_DHW_Setpoint": 65, - "Current_DHW_Setpoint": 46, + "Slot_current_CH": 0, + "Start_Time_CH7": "000000000000000000000000", + "Max_NumBer_Of_Timeslots_DHW": 0, + "Start_Time_CH6": "000000000000000000000000", + "Start_Time_CH5": "000000000000000000000000", + "Start_Time_CH4": "000000000000000000000000", "RF_Status": 3, - "Flow_Temperature_Setpoint": 0, - "Flow_temperature": 75, + "Start_Time_CH3": "000000000000000000000000", + "Start_Time_CH2": "000000000000000000000000", "return_temperature": 0, - "DSN": 1500, - "Tank_temperature": 127.5, + "DHW_Function": "none", + "Current_DHW_Setpoint": 45, + "Boiler_info5_bit4": "00", + "DHW_setpoint": 60, + "Heating_Curve": 1 } CONF_HOST = "https://appapi.vaillant.com.cn" diff --git a/tests/test_binary_sensor.py b/tests/test_binary_sensor.py index cbef2e8..c407f71 100644 --- a/tests/test_binary_sensor.py +++ b/tests/test_binary_sensor.py @@ -18,16 +18,16 @@ async def test_binary_sensor_heating_enabled(device_api_client): name="Heating", device_class=BinarySensorDeviceClass.RUNNING, entity_category=EntityCategory.DIAGNOSTIC, - on_state=True, + on_state=1, ), ) assert binary_sensor.unique_id == "1_Heating_Enable" - binary_sensor.update_from_latest_data({"Heating_Enable": True}) + binary_sensor.update_from_latest_data({"Heating_Enable": 1}) assert binary_sensor.is_on is True - binary_sensor.update_from_latest_data({"Heating_Enable": False}) + binary_sensor.update_from_latest_data({"Heating_Enable": 0}) assert binary_sensor.is_on is False @@ -40,7 +40,7 @@ async def test_binary_sensor_rf_status(device_api_client): name="EBus Status", device_class=BinarySensorDeviceClass.CONNECTIVITY, entity_category=EntityCategory.DIAGNOSTIC, - on_state=True, + on_state=3, ), ) @@ -71,13 +71,13 @@ async def test_binary_sensor_boiler_info3_bit0(device_api_client): assert binary_sensor.unique_id == "1_Boiler_info3_bit0" - binary_sensor.update_from_latest_data({"Boiler_info3_bit0": [1]}) + binary_sensor.update_from_latest_data({"Boiler_info3_bit0": "10"}) assert binary_sensor.is_on is True - binary_sensor.update_from_latest_data({"Boiler_info3_bit0": [2]}) + binary_sensor.update_from_latest_data({"Boiler_info3_bit0": "00"}) assert binary_sensor.is_on is False - binary_sensor.update_from_latest_data({"Boiler_info3_bit0": [0, 2, 3]}) + binary_sensor.update_from_latest_data({"Boiler_info3_bit0": "000"}) assert binary_sensor.is_on is False @@ -96,11 +96,11 @@ async def test_binary_sensor_boiler_info5_bit4(device_api_client): assert binary_sensor.unique_id == "1_Boiler_info5_bit4" - binary_sensor.update_from_latest_data({"Boiler_info5_bit4": [1]}) + binary_sensor.update_from_latest_data({"Boiler_info5_bit4": "10"}) assert binary_sensor.is_on is True - binary_sensor.update_from_latest_data({"Boiler_info5_bit4": [2]}) + binary_sensor.update_from_latest_data({"Boiler_info5_bit4": "00"}) assert binary_sensor.is_on is False - binary_sensor.update_from_latest_data({"Boiler_info5_bit4": [0, 2, 3]}) + binary_sensor.update_from_latest_data({"Boiler_info5_bit4": "000"}) assert binary_sensor.is_on is False diff --git a/tests/test_cliamte.py b/tests/test_cliamte.py index dfbde45..3d0d7c9 100644 --- a/tests/test_cliamte.py +++ b/tests/test_cliamte.py @@ -17,7 +17,7 @@ async def test_climate_actions(hass, device_api_client): assert climate.name is None with patch( - "custom_components.vaillant_plus.VaillantDeviceApiClient.send_command" + "custom_components.vaillant_plus.VaillantClient.control_device" ) as send_command_func: await climate.async_set_temperature() send_command_func.assert_not_called() @@ -44,11 +44,11 @@ async def test_climate_actions(hass, device_api_client): await climate.async_set_hvac_mode(HVACMode.OFF) send_command_func.assert_awaited_with( "Heating_Enable", - False, + 0, ) await climate.async_set_hvac_mode(HVACMode.HEAT) send_command_func.assert_awaited_with( "Heating_Enable", - True, + 1, ) diff --git a/tests/test_client.py b/tests/test_client.py index 62bd5cb..295f75d 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -1,171 +1,171 @@ -"""Tests for vaillant-plus client.""" -import pytest -from vaillant_plus_cn_api import Token - -from custom_components.vaillant_plus.client import ( - ShouldUpdateConfigEntry, - VaillantApiHub, -) - -from .const import CONF_HOST, CONF_HOST_API, MOCK_PASSWORD, MOCK_USERNAME - - -@pytest.mark.asyncio -async def test_client(hass, aioclient_mock, caplog): - """Test API calls.""" - - # To test the api submodule, we first create an instance of our API client - client = VaillantApiHub(hass=hass) - - # Use aioclient_mock which is provided by `pytest_homeassistant_custom_components` - # to mock responses to aiohttp requests. - aioclient_mock.post( - f"{CONF_HOST}/app/user/login", - json={"code": "200", "data": {"token": "1", "uid": "2"}}, - ) - token = await client.login(MOCK_USERNAME, MOCK_PASSWORD) - assert token.username == MOCK_USERNAME - assert token.password == MOCK_PASSWORD - assert token.token == "1" - assert token.uid == "2" - - # We do the same for `async_set_title`. Note the difference in the mock call - # between the previous step and this one. We use `patch` here instead of `get` - # because we know that `async_set_title` calls `api_wrapper` with `patch` as the - # first parameter - aioclient_mock.get( - f"{CONF_HOST_API}/app/bindings", - json={ - "devices": [ - { - "remark": "", - "protoc": 3, - "wss_port": 8, - "ws_port": 9, - "did": "1", - "port_s": 10, - "is_disabled": False, - "wifi_soft_version": "wsv1", - "product_key": "abcdefg", - "port": 11, - "mac": "12345678abcd", - "role": "owner", - "dev_alias": "", - "is_sandbox": True, - "is_online": True, - "host": "test_host", - "type": "normal", - "product_name": "vSmartPro", - } - ] - }, - ) - device_list = await client.get_device_list("test") - assert device_list is not None - assert len(device_list) == 1 - - aioclient_mock.get( - f"{CONF_HOST}/app/device/sn/status", - json={ - "code": "200", - "data": { - "gizDid": "1", - "mac": "12345678abcd", - "model": "model_test", - "serialNumber": "2", - "sno": "3", - "status": 1, - }, - "display": None, - "message": "本次请求成功!", - }, - ) - device_info = await client.get_device_info("test", "12345678abcd") - assert device_info == { - "sno": "3", - "mac": "12345678abcd", - "device_id": "1", - "serial_number": "2", - "model": "model_test", - "status_code": 1, - } - - device = await client.get_device(token, "1") - assert device.id == "1" - - with pytest.raises(ShouldUpdateConfigEntry): - await client.get_device(token, "2") - - -@pytest.mark.asyncio -async def test_client_invalid_auth( - hass, aioclient_mock, invalid_auth_on_device_list, caplog -): - """Test API calls.""" - - client = VaillantApiHub(hass=hass, retry_interval=0) - - aioclient_mock.post( - f"{CONF_HOST}/app/user/login", - json={"code": "200", "data": {"token": "1", "uid": "2"}}, - ) - - with pytest.raises(ShouldUpdateConfigEntry): - await client.get_device(Token("a1", "u1", "p1"), "1") - - -# In order to get 100% coverage, we need to test `api_wrapper` to test the code -# that isn't already called by `async_get_data` and `async_set_title`. Because the -# only logic that lives inside `api_wrapper` that is not being handled by a third -# party library (aiohttp) is the exception handling, we also want to simulate -# raising the exceptions to ensure that the function handles them as expected. -# The caplog fixture allows access to log messages in tests. This is particularly -# useful during exception handling testing since often the only action as part of -# exception handling is a logging statement -# caplog.clear() -# aioclient_mock.put( -# "https://jsonplaceholder.typicode.com/posts/1", exc=asyncio.TimeoutError -# ) -# assert ( -# await api.api_wrapper("put", "https://jsonplaceholder.typicode.com/posts/1") -# is None -# ) -# assert ( -# len(caplog.record_tuples) == 1 -# and "Timeout error fetching information from" in caplog.record_tuples[0][2] -# ) - -# caplog.clear() -# aioclient_mock.post( -# "https://jsonplaceholder.typicode.com/posts/1", exc=aiohttp.ClientError -# ) -# assert ( -# await api.api_wrapper("post", "https://jsonplaceholder.typicode.com/posts/1") -# is None -# ) -# assert ( -# len(caplog.record_tuples) == 1 -# and "Error fetching information from" in caplog.record_tuples[0][2] -# ) +# """Tests for vaillant-plus client.""" +# import pytest +# from vaillant_plus_cn_api import Token -# caplog.clear() -# aioclient_mock.post("https://jsonplaceholder.typicode.com/posts/2", exc=Exception) -# assert ( -# await api.api_wrapper("post", "https://jsonplaceholder.typicode.com/posts/2") -# is None -# ) -# assert ( -# len(caplog.record_tuples) == 1 -# and "Something really wrong happened!" in caplog.record_tuples[0][2] +# from custom_components.vaillant_plus.client import ( +# ShouldUpdateConfigEntry, +# VaillantClient, # ) -# caplog.clear() -# aioclient_mock.post("https://jsonplaceholder.typicode.com/posts/3", exc=TypeError) -# assert ( -# await api.api_wrapper("post", "https://jsonplaceholder.typicode.com/posts/3") -# is None -# ) -# assert ( -# len(caplog.record_tuples) == 1 -# and "Error parsing information from" in caplog.record_tuples[0][2] -# ) +# from .const import CONF_HOST, CONF_HOST_API, MOCK_PASSWORD, MOCK_USERNAME + + +# @pytest.mark.asyncio +# async def test_client(hass, aioclient_mock, caplog): +# """Test API calls.""" + +# # To test the api submodule, we first create an instance of our API client +# client = VaillantApiHub(hass=hass) + +# # Use aioclient_mock which is provided by `pytest_homeassistant_custom_components` +# # to mock responses to aiohttp requests. +# aioclient_mock.post( +# f"{CONF_HOST}/app/user/login", +# json={"code": "200", "data": {"token": "1", "uid": "2"}}, +# ) +# token = await client.login(MOCK_USERNAME, MOCK_PASSWORD) +# assert token.username == MOCK_USERNAME +# assert token.password == MOCK_PASSWORD +# assert token.token == "1" +# assert token.uid == "2" + +# # We do the same for `async_set_title`. Note the difference in the mock call +# # between the previous step and this one. We use `patch` here instead of `get` +# # because we know that `async_set_title` calls `api_wrapper` with `patch` as the +# # first parameter +# aioclient_mock.get( +# f"{CONF_HOST_API}/app/bindings", +# json={ +# "devices": [ +# { +# "remark": "", +# "protoc": 3, +# "wss_port": 8, +# "ws_port": 9, +# "did": "1", +# "port_s": 10, +# "is_disabled": False, +# "wifi_soft_version": "wsv1", +# "product_key": "abcdefg", +# "port": 11, +# "mac": "12345678abcd", +# "role": "owner", +# "dev_alias": "", +# "is_sandbox": True, +# "is_online": True, +# "host": "test_host", +# "type": "normal", +# "product_name": "vSmartPro", +# } +# ] +# }, +# ) +# device_list = await client.get_device_list("test") +# assert device_list is not None +# assert len(device_list) == 1 + +# aioclient_mock.get( +# f"{CONF_HOST}/app/device/sn/status", +# json={ +# "code": "200", +# "data": { +# "gizDid": "1", +# "mac": "12345678abcd", +# "model": "model_test", +# "serialNumber": "2", +# "sno": "3", +# "status": 1, +# }, +# "display": None, +# "message": "本次请求成功!", +# }, +# ) +# device_info = await client.get_device_info("test", "12345678abcd") +# assert device_info == { +# "sno": "3", +# "mac": "12345678abcd", +# "device_id": "1", +# "serial_number": "2", +# "model": "model_test", +# "status_code": 1, +# } + +# device = await client.get_device(token, "1") +# assert device.id == "1" + +# with pytest.raises(ShouldUpdateConfigEntry): +# await client.get_device(token, "2") + + +# @pytest.mark.asyncio +# async def test_client_invalid_auth( +# hass, aioclient_mock, invalid_auth_on_device_list, caplog +# ): +# """Test API calls.""" + +# client = VaillantApiHub(hass=hass, retry_interval=0) + +# aioclient_mock.post( +# f"{CONF_HOST}/app/user/login", +# json={"code": "200", "data": {"token": "1", "uid": "2"}}, +# ) + +# with pytest.raises(ShouldUpdateConfigEntry): +# await client.get_device(Token("a1", "u1", "p1"), "1") + + +# # In order to get 100% coverage, we need to test `api_wrapper` to test the code +# # that isn't already called by `async_get_data` and `async_set_title`. Because the +# # only logic that lives inside `api_wrapper` that is not being handled by a third +# # party library (aiohttp) is the exception handling, we also want to simulate +# # raising the exceptions to ensure that the function handles them as expected. +# # The caplog fixture allows access to log messages in tests. This is particularly +# # useful during exception handling testing since often the only action as part of +# # exception handling is a logging statement +# # caplog.clear() +# # aioclient_mock.put( +# # "https://jsonplaceholder.typicode.com/posts/1", exc=asyncio.TimeoutError +# # ) +# # assert ( +# # await api.api_wrapper("put", "https://jsonplaceholder.typicode.com/posts/1") +# # is None +# # ) +# # assert ( +# # len(caplog.record_tuples) == 1 +# # and "Timeout error fetching information from" in caplog.record_tuples[0][2] +# # ) + +# # caplog.clear() +# # aioclient_mock.post( +# # "https://jsonplaceholder.typicode.com/posts/1", exc=aiohttp.ClientError +# # ) +# # assert ( +# # await api.api_wrapper("post", "https://jsonplaceholder.typicode.com/posts/1") +# # is None +# # ) +# # assert ( +# # len(caplog.record_tuples) == 1 +# # and "Error fetching information from" in caplog.record_tuples[0][2] +# # ) + +# # caplog.clear() +# # aioclient_mock.post("https://jsonplaceholder.typicode.com/posts/2", exc=Exception) +# # assert ( +# # await api.api_wrapper("post", "https://jsonplaceholder.typicode.com/posts/2") +# # is None +# # ) +# # assert ( +# # len(caplog.record_tuples) == 1 +# # and "Something really wrong happened!" in caplog.record_tuples[0][2] +# # ) + +# # caplog.clear() +# # aioclient_mock.post("https://jsonplaceholder.typicode.com/posts/3", exc=TypeError) +# # assert ( +# # await api.api_wrapper("post", "https://jsonplaceholder.typicode.com/posts/3") +# # is None +# # ) +# # assert ( +# # len(caplog.record_tuples) == 1 +# # and "Error parsing information from" in caplog.record_tuples[0][2] +# # ) diff --git a/tests/test_init.py b/tests/test_init.py index 10f61d4..b2c579c 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -13,12 +13,12 @@ from vaillant_plus_cn_api import EVT_DEVICE_ATTR_UPDATE from custom_components.vaillant_plus import ( - VaillantDeviceApiClient, + VaillantClient, async_setup, async_setup_entry, async_unload_entry, ) -from custom_components.vaillant_plus.const import DISPATCHERS, DOMAIN, WEBSOCKET_CLIENT +from custom_components.vaillant_plus.const import DISPATCHERS, DOMAIN, API_CLIENT from .const import ( MOCK_CONFIG_ENTRY_DATA, @@ -28,7 +28,7 @@ ) -async def test_init_setup_and_unload_entry(hass: HomeAssistant, bypass_get_device_info): +async def test_init_setup_and_unload_entry(hass: HomeAssistant, bypass_login, bypass_get_device): """Test switch services.""" # Create a mock entry so we don't have to go through config flow config_entry = MockConfigEntry( @@ -38,22 +38,19 @@ async def test_init_setup_and_unload_entry(hass: HomeAssistant, bypass_get_devic # Functions/objects can be patched directly in test code as well and can be used to test # additional things, like whether a function was called or what arguments it was called with with patch( - "custom_components.vaillant_plus.VaillantDeviceApiClient.connect" + "vaillant_plus_cn_api.VaillantWebsocketClient.connect" ) as connect_func: assert await async_setup(hass, {}) assert await async_setup_entry(hass, config_entry) assert DOMAIN in hass.data assert MOCK_DID in hass.data[DOMAIN][DISPATCHERS] - assert config_entry.entry_id in hass.data[DOMAIN][WEBSOCKET_CLIENT] + assert config_entry.entry_id in hass.data[DOMAIN][API_CLIENT] assert isinstance( - hass.data[DOMAIN][WEBSOCKET_CLIENT][config_entry.entry_id], - VaillantDeviceApiClient, + hass.data[DOMAIN][API_CLIENT][config_entry.entry_id], + VaillantClient, ) - client = hass.data[DOMAIN][WEBSOCKET_CLIENT][config_entry.entry_id].client - client._on_subscribe_handler(MOCK_DEVICE_ATTRS_WHEN_CONNECT) - # async_dispatcher_send( # hass, # EVT_DEVICE_UPDATED.format(MOCK_DID), @@ -61,9 +58,15 @@ async def test_init_setup_and_unload_entry(hass: HomeAssistant, bypass_get_devic # ) await hass.async_block_till_done() + assert connect_func.called assert len(hass.data[DOMAIN][DISPATCHERS][MOCK_DID]) >= 2 + client = hass.data[DOMAIN][API_CLIENT][config_entry.entry_id]._websocket_client + client._on_subscribe_handler(MOCK_DEVICE_ATTRS_WHEN_CONNECT) + + await hass.async_block_till_done() + # Test whether the states of those entities are correct state_binary_sensor_heating = hass.states.get("binary_sensor.heating") assert ( @@ -75,7 +78,7 @@ async def test_init_setup_and_unload_entry(hass: HomeAssistant, bypass_get_devic "water_heater.vaillant_plus_1_water_heater" ) assert state_water_heater.state == STATE_ON - assert state_water_heater.attributes.get(ATTR_TEMPERATURE) == 45.0 + assert state_water_heater.attributes.get(ATTR_TEMPERATURE) == 45 assert state_water_heater.attributes.get("min_temp") == 35.0 assert state_water_heater.attributes.get("max_temp") == 65.0 assert state_water_heater.attributes.get("target_temp_low") == 35.0 @@ -84,7 +87,7 @@ async def test_init_setup_and_unload_entry(hass: HomeAssistant, bypass_get_devic state_climate = hass.states.get("climate.vaillant_plus_1_climate") assert state_climate.state == STATE_OFF assert state_climate.attributes.get("hvac_action") == STATE_OFF - assert state_climate.attributes.get("current_temperature") == 20.5 + assert state_climate.attributes.get("current_temperature") == 18.5 # Test whether entities handle correctly when connect event triggered again client._on_subscribe_handler(MOCK_DEVICE_ATTRS_WHEN_CONNECT) @@ -96,7 +99,7 @@ async def test_init_setup_and_unload_entry(hass: HomeAssistant, bypass_get_devic state_water_heater = hass.states.get( "water_heater.vaillant_plus_1_water_heater" ) - assert state_water_heater.attributes.get(ATTR_TEMPERATURE) == 46.0 + assert state_water_heater.attributes.get(ATTR_TEMPERATURE) == 60 assert state_water_heater.attributes.get("min_temp") == 35.0 assert state_water_heater.attributes.get("max_temp") == 65.0 assert state_water_heater.attributes.get("target_temp_low") == 35.0 @@ -105,12 +108,12 @@ async def test_init_setup_and_unload_entry(hass: HomeAssistant, bypass_get_devic state_climate = hass.states.get("climate.vaillant_plus_1_climate") assert state_climate.state == "heat" assert state_climate.attributes.get("hvac_action") == HVACAction.HEATING - assert state_climate.attributes.get("current_temperature") == 11.5 + assert state_climate.attributes.get("current_temperature") == 20.5 with patch( - "custom_components.vaillant_plus.VaillantDeviceApiClient.close" + "custom_components.vaillant_plus.VaillantClient.close" ) as close_func: assert await async_unload_entry(hass, config_entry) await hass.async_block_till_done() assert close_func.called - assert config_entry.entry_id not in hass.data[DOMAIN][WEBSOCKET_CLIENT] + assert config_entry.entry_id not in hass.data[DOMAIN][API_CLIENT] diff --git a/tests/test_water_heater.py b/tests/test_water_heater.py index 9ada310..41970c2 100644 --- a/tests/test_water_heater.py +++ b/tests/test_water_heater.py @@ -16,7 +16,7 @@ async def test_water_heater_actions(hass, device_api_client): assert water_heater.name is None with patch( - "custom_components.vaillant_plus.VaillantDeviceApiClient.send_command" + "custom_components.vaillant_plus.VaillantClient.control_device" ) as send_command_func: await water_heater.async_set_temperature() send_command_func.assert_not_called() From 957faf384c82a88ea198933521e6c0b8312faee0 Mon Sep 17 00:00:00 2001 From: daxingplay Date: Sun, 3 Dec 2023 15:37:47 +0000 Subject: [PATCH 2/2] :package: v1.2.0 --- custom_components/vaillant_plus/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/vaillant_plus/manifest.json b/custom_components/vaillant_plus/manifest.json index be26c66..9088234 100644 --- a/custom_components/vaillant_plus/manifest.json +++ b/custom_components/vaillant_plus/manifest.json @@ -17,6 +17,6 @@ "vaillant-plus-cn-api==1.2.8" ], "ssdp": [], - "version": "1.1.0", + "version": "1.2.0", "zeroconf": [] } \ No newline at end of file