mirror of
https://github.com/home-assistant/core.git
synced 2026-09-25 17:04:04 -04:00
Improve code quality of hr_energy_qube (#178280)
This commit is contained in:
@@ -1,57 +1,23 @@
|
||||
"""The Qube Heat Pump integration."""
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from python_qube_heatpump import QubeClient
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_HOST, CONF_PORT
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
|
||||
from .const import PLATFORMS
|
||||
from .coordinator import QubeCoordinator
|
||||
|
||||
|
||||
@dataclass
|
||||
class QubeData:
|
||||
"""Runtime data for Qube Heat Pump."""
|
||||
|
||||
coordinator: QubeCoordinator
|
||||
client: QubeClient
|
||||
sw_version: str | None
|
||||
|
||||
|
||||
type QubeConfigEntry = ConfigEntry[QubeData]
|
||||
type QubeConfigEntry = ConfigEntry[QubeCoordinator]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: QubeConfigEntry) -> bool:
|
||||
"""Set up Qube Heat Pump from a config entry."""
|
||||
client = QubeClient(entry.data[CONF_HOST], entry.data[CONF_PORT])
|
||||
|
||||
# Connect and read software version for device info
|
||||
sw_version: str | None = None
|
||||
try:
|
||||
connected = await client.connect()
|
||||
if not connected:
|
||||
await client.close()
|
||||
raise ConfigEntryNotReady(
|
||||
f"Unable to connect to Qube heat pump at {entry.data[CONF_HOST]}"
|
||||
)
|
||||
sw_version = await client.async_get_software_version()
|
||||
except (OSError, TimeoutError) as err:
|
||||
await client.close()
|
||||
raise ConfigEntryNotReady(
|
||||
f"Unable to connect to Qube heat pump at {entry.data[CONF_HOST]}"
|
||||
) from err
|
||||
|
||||
coordinator = QubeCoordinator(hass, client, entry)
|
||||
|
||||
entry.runtime_data = QubeData(
|
||||
coordinator=coordinator,
|
||||
client=client,
|
||||
sw_version=sw_version,
|
||||
)
|
||||
entry.runtime_data = coordinator
|
||||
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
@@ -260,7 +260,7 @@ async def async_setup_entry(
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Qube binary sensors."""
|
||||
coordinator = entry.runtime_data.coordinator
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
async_add_entities(
|
||||
QubeBinarySensor(coordinator, entry, description)
|
||||
|
||||
@@ -38,7 +38,7 @@ class QubeConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
version = await client.async_get_software_version()
|
||||
if version is None:
|
||||
errors["base"] = "not_qube_device"
|
||||
except OSError, TimeoutError:
|
||||
except OSError:
|
||||
errors["base"] = "cannot_connect"
|
||||
finally:
|
||||
await client.close()
|
||||
|
||||
@@ -31,6 +31,8 @@ class QubeData:
|
||||
class QubeCoordinator(DataUpdateCoordinator[QubeData]):
|
||||
"""Qube Heat Pump data coordinator."""
|
||||
|
||||
sw_version: str | None = None
|
||||
|
||||
def __init__(
|
||||
self, hass: HomeAssistant, client: QubeClient, entry: ConfigEntry
|
||||
) -> None:
|
||||
@@ -44,6 +46,23 @@ class QubeCoordinator(DataUpdateCoordinator[QubeData]):
|
||||
config_entry=entry,
|
||||
)
|
||||
|
||||
@override
|
||||
async def _async_setup(self) -> None:
|
||||
"""Connect to the device and read its software version."""
|
||||
try:
|
||||
connected = await self.client.connect()
|
||||
if not connected:
|
||||
await self.client.close()
|
||||
raise UpdateFailed(
|
||||
f"Unable to connect to Qube heat pump at {self.client.host}"
|
||||
)
|
||||
self.sw_version = await self.client.async_get_software_version()
|
||||
except OSError as err:
|
||||
await self.client.close()
|
||||
raise UpdateFailed(
|
||||
f"Unable to connect to Qube heat pump at {self.client.host}"
|
||||
) from err
|
||||
|
||||
@override
|
||||
async def _async_update_data(self) -> QubeData:
|
||||
"""Fetch data from the device."""
|
||||
@@ -51,7 +70,7 @@ class QubeCoordinator(DataUpdateCoordinator[QubeData]):
|
||||
state = await self.client.get_all_data()
|
||||
switches = await self.client.read_all_switches()
|
||||
sg_ready_mode = await self.client.get_sg_ready_mode()
|
||||
except (ConnectionError, TimeoutError, OSError) as exc:
|
||||
except OSError as exc:
|
||||
raise UpdateFailed(
|
||||
f"Error communicating with Qube heat pump: {exc}"
|
||||
) from exc
|
||||
|
||||
@@ -28,5 +28,5 @@ class QubeEntity(CoordinatorEntity[QubeCoordinator]):
|
||||
identifiers={(DOMAIN, entry.entry_id)},
|
||||
manufacturer="Qube",
|
||||
model="Heat Pump",
|
||||
sw_version=entry.runtime_data.sw_version,
|
||||
sw_version=coordinator.sw_version,
|
||||
)
|
||||
|
||||
@@ -42,7 +42,7 @@ rules:
|
||||
docs-installation-parameters: done
|
||||
entity-unavailable: done
|
||||
integration-owner: done
|
||||
log-when-unavailable: todo
|
||||
log-when-unavailable: done
|
||||
parallel-updates: done
|
||||
reauthentication-flow:
|
||||
status: exempt
|
||||
@@ -64,9 +64,9 @@ rules:
|
||||
dynamic-devices:
|
||||
status: exempt
|
||||
comment: Single device per config entry.
|
||||
entity-category: todo
|
||||
entity-category: done
|
||||
entity-device-class: done
|
||||
entity-disabled-by-default: todo
|
||||
entity-disabled-by-default: done
|
||||
entity-translations: done
|
||||
exception-translations: todo
|
||||
icon-translations: todo
|
||||
|
||||
@@ -23,7 +23,7 @@ async def async_setup_entry(
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Qube select entities."""
|
||||
coordinator = entry.runtime_data.coordinator
|
||||
coordinator = entry.runtime_data
|
||||
async_add_entities([QubeSGReadySelect(coordinator, entry)])
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@ class QubeSGReadySelect(QubeEntity, SelectEntity):
|
||||
"""Set the SG Ready mode."""
|
||||
try:
|
||||
success = await self.coordinator.client.set_sg_ready_mode(option)
|
||||
except (ConnectionError, TimeoutError, OSError) as err:
|
||||
except OSError as err:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="switch_command_failed",
|
||||
|
||||
@@ -4,6 +4,8 @@ from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, override
|
||||
|
||||
from python_qube_heatpump import STATUS_CODE_MAP
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
@@ -31,21 +33,18 @@ if TYPE_CHECKING:
|
||||
from . import QubeConfigEntry
|
||||
from .coordinator import QubeCoordinator
|
||||
|
||||
# Status code to state mapping
|
||||
# Status code to state mapping, derived from the library's status code map.
|
||||
STATUS_MAP: dict[int, str] = {
|
||||
1: "standby",
|
||||
2: "alarm",
|
||||
6: "keyboard_off",
|
||||
8: "compressor_startup",
|
||||
9: "compressor_shutdown",
|
||||
14: "standby",
|
||||
15: "cooling",
|
||||
16: "heating",
|
||||
17: "start_fail",
|
||||
18: "standby",
|
||||
22: "heating_dhw",
|
||||
code: status.value for code, status in STATUS_CODE_MAP.items()
|
||||
}
|
||||
|
||||
# Options list for the status sensor: unique status strings from STATUS_MAP,
|
||||
# in first-seen order. StatusCode also defines ANTI_LEGIONELLA and UNKNOWN,
|
||||
# but those are not part of STATUS_CODE_MAP (only surfaced via the library's
|
||||
# resolve_status() helper, which this integration does not use), so they are
|
||||
# excluded here automatically rather than needing an explicit filter.
|
||||
STATUS_OPTIONS: list[str] = list(dict.fromkeys(STATUS_MAP.values()))
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class QubeSensorEntityDescription(SensorEntityDescription):
|
||||
@@ -226,17 +225,7 @@ SENSOR_TYPES: tuple[QubeSensorEntityDescription, ...] = (
|
||||
key="status_heatpump",
|
||||
translation_key="status_heatpump",
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=[
|
||||
"standby",
|
||||
"alarm",
|
||||
"keyboard_off",
|
||||
"compressor_startup",
|
||||
"compressor_shutdown",
|
||||
"cooling",
|
||||
"heating",
|
||||
"start_fail",
|
||||
"heating_dhw",
|
||||
],
|
||||
options=STATUS_OPTIONS,
|
||||
value_fn=_status_value,
|
||||
),
|
||||
)
|
||||
@@ -248,7 +237,7 @@ async def async_setup_entry(
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Qube sensors."""
|
||||
coordinator = entry.runtime_data.coordinator
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
async_add_entities(
|
||||
QubeSensor(coordinator, entry, description) for description in SENSOR_TYPES
|
||||
|
||||
@@ -55,7 +55,7 @@ async def async_setup_entry(
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Qube switches."""
|
||||
coordinator = entry.runtime_data.coordinator
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
async_add_entities(
|
||||
QubeSwitch(coordinator, entry, description) for description in SWITCH_TYPES
|
||||
@@ -98,7 +98,7 @@ class QubeSwitch(QubeEntity, SwitchEntity):
|
||||
register_key = self.entity_description.register_key
|
||||
try:
|
||||
success = await self.coordinator.client.write_switch(register_key, value)
|
||||
except (ConnectionError, TimeoutError, OSError) as err:
|
||||
except OSError as err:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="switch_command_failed",
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from typing import Any, override
|
||||
|
||||
from homeassistant.components.water_heater import (
|
||||
ATTR_TEMPERATURE,
|
||||
STATE_HEAT_PUMP,
|
||||
STATE_PERFORMANCE,
|
||||
WaterHeaterEntity,
|
||||
@@ -34,7 +35,7 @@ async def async_setup_entry(
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Qube water heater."""
|
||||
coordinator = entry.runtime_data.coordinator
|
||||
coordinator = entry.runtime_data
|
||||
async_add_entities([QubeWaterHeater(coordinator, entry)])
|
||||
|
||||
|
||||
@@ -86,14 +87,12 @@ class QubeWaterHeater(QubeEntity, WaterHeaterEntity):
|
||||
@override
|
||||
async def async_set_temperature(self, **kwargs: Any) -> None:
|
||||
"""Set the target DHW temperature."""
|
||||
temperature = kwargs.get("temperature")
|
||||
if temperature is None:
|
||||
return
|
||||
temperature = kwargs[ATTR_TEMPERATURE]
|
||||
try:
|
||||
success = await self.coordinator.client.write_setpoint(
|
||||
DHW_SETPOINT_KEY, temperature
|
||||
)
|
||||
except (ConnectionError, TimeoutError, OSError) as err:
|
||||
except OSError as err:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="set_temperature_failed",
|
||||
@@ -111,7 +110,7 @@ class QubeWaterHeater(QubeEntity, WaterHeaterEntity):
|
||||
boost = operation_mode == STATE_PERFORMANCE
|
||||
try:
|
||||
success = await self.coordinator.client.write_switch(DHW_BOOST_KEY, boost)
|
||||
except (ConnectionError, TimeoutError, OSError) as err:
|
||||
except OSError as err:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="switch_command_failed",
|
||||
|
||||
@@ -1,19 +1,17 @@
|
||||
"""Tests for the Qube Heat Pump binary sensor platform."""
|
||||
|
||||
from datetime import timedelta
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.const import STATE_UNAVAILABLE, Platform
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from . import setup_integration
|
||||
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform
|
||||
from tests.common import MockConfigEntry, snapshot_platform
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||
@@ -32,41 +30,3 @@ async def test_entities(
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("side_effect", "return_value"),
|
||||
[
|
||||
(ConnectionError("Connection lost"), None),
|
||||
(None, None),
|
||||
],
|
||||
)
|
||||
async def test_binary_sensor_unavailable_on_coordinator_error(
|
||||
hass: HomeAssistant,
|
||||
mock_qube_client: MagicMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
side_effect: Exception | None,
|
||||
return_value: None,
|
||||
) -> None:
|
||||
"""Test binary sensors become unavailable when coordinator fails."""
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
# Verify binary sensors are available after setup
|
||||
states = hass.states.async_all("binary_sensor")
|
||||
assert len(states) > 0
|
||||
assert all(s.state != STATE_UNAVAILABLE for s in states)
|
||||
|
||||
# Make the next fetch fail
|
||||
mock_qube_client.get_all_data = AsyncMock(
|
||||
side_effect=side_effect, return_value=return_value
|
||||
)
|
||||
|
||||
# Skip time to trigger coordinator refresh
|
||||
freezer.tick(timedelta(seconds=31))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# All binary sensors should be unavailable
|
||||
states = hass.states.async_all("binary_sensor")
|
||||
assert all(s.state == STATE_UNAVAILABLE for s in states)
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
@@ -25,3 +27,29 @@ async def test_setup_and_unload_entry(
|
||||
|
||||
assert mock_config_entry.state is ConfigEntryState.NOT_LOADED
|
||||
mock_qube_client.close.assert_called_once()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("connect_result", "connect_error"),
|
||||
[
|
||||
(False, None),
|
||||
(None, OSError("Connection refused")),
|
||||
],
|
||||
)
|
||||
async def test_setup_entry_connection_failure(
|
||||
hass: HomeAssistant,
|
||||
mock_qube_client: MagicMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
connect_result: bool | None,
|
||||
connect_error: Exception | None,
|
||||
) -> None:
|
||||
"""Test setup failure when the device cannot be reached."""
|
||||
if connect_error is not None:
|
||||
mock_qube_client.connect.side_effect = connect_error
|
||||
else:
|
||||
mock_qube_client.connect.return_value = connect_result
|
||||
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY
|
||||
mock_qube_client.close.assert_called_once()
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
"""Tests for the Qube Heat Pump select platform."""
|
||||
|
||||
from datetime import timedelta
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
@@ -12,29 +10,16 @@ from homeassistant.components.select import (
|
||||
DOMAIN as SELECT_DOMAIN,
|
||||
SERVICE_SELECT_OPTION,
|
||||
)
|
||||
from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE, Platform
|
||||
from homeassistant.const import ATTR_ENTITY_ID, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from . import setup_integration
|
||||
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform
|
||||
from tests.common import MockConfigEntry, snapshot_platform
|
||||
|
||||
|
||||
async def _setup_select(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> str:
|
||||
"""Set up the select platform and return the entity_id."""
|
||||
with patch("homeassistant.components.hr_energy_qube.PLATFORMS", [Platform.SELECT]):
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
entity_registry = er.async_get(hass)
|
||||
entries = er.async_entries_for_config_entry(
|
||||
entity_registry, mock_config_entry.entry_id
|
||||
)
|
||||
assert len(entries) == 1
|
||||
return entries[0].entity_id
|
||||
ENTITY_ID = "select.qube_heat_pump_smart_grid_ready_mode"
|
||||
|
||||
|
||||
async def test_entities(
|
||||
@@ -59,84 +44,42 @@ async def test_select_option(
|
||||
option: str,
|
||||
) -> None:
|
||||
"""Test selecting an SG Ready mode."""
|
||||
entity_id = await _setup_select(hass, mock_config_entry)
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
await hass.services.async_call(
|
||||
SELECT_DOMAIN,
|
||||
SERVICE_SELECT_OPTION,
|
||||
{ATTR_ENTITY_ID: entity_id, ATTR_OPTION: option},
|
||||
{ATTR_ENTITY_ID: ENTITY_ID, ATTR_OPTION: option},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
mock_qube_client.set_sg_ready_mode.assert_awaited_once_with(option)
|
||||
|
||||
|
||||
async def test_select_option_connection_error(
|
||||
hass: HomeAssistant,
|
||||
mock_qube_client: MagicMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test select raises HomeAssistantError on connection error."""
|
||||
entity_id = await _setup_select(hass, mock_config_entry)
|
||||
|
||||
mock_qube_client.set_sg_ready_mode = AsyncMock(side_effect=ConnectionError)
|
||||
with pytest.raises(HomeAssistantError):
|
||||
await hass.services.async_call(
|
||||
SELECT_DOMAIN,
|
||||
SERVICE_SELECT_OPTION,
|
||||
{ATTR_ENTITY_ID: entity_id, ATTR_OPTION: "plus"},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
|
||||
async def test_select_option_write_failure(
|
||||
hass: HomeAssistant,
|
||||
mock_qube_client: MagicMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test select raises HomeAssistantError when write fails."""
|
||||
entity_id = await _setup_select(hass, mock_config_entry)
|
||||
|
||||
mock_qube_client.set_sg_ready_mode = AsyncMock(return_value=False)
|
||||
with pytest.raises(HomeAssistantError):
|
||||
await hass.services.async_call(
|
||||
SELECT_DOMAIN,
|
||||
SERVICE_SELECT_OPTION,
|
||||
{ATTR_ENTITY_ID: entity_id, ATTR_OPTION: "plus"},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("side_effect", "return_value"),
|
||||
[
|
||||
(ConnectionError("Connection lost"), None),
|
||||
(None, None),
|
||||
(ConnectionError, None),
|
||||
(None, False),
|
||||
],
|
||||
)
|
||||
async def test_select_unavailable_on_coordinator_error(
|
||||
async def test_select_option_error(
|
||||
hass: HomeAssistant,
|
||||
mock_qube_client: MagicMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
side_effect: Exception | None,
|
||||
return_value: None,
|
||||
side_effect: type[Exception] | None,
|
||||
return_value: bool | None,
|
||||
) -> None:
|
||||
"""Test select becomes unavailable when coordinator fails."""
|
||||
entity_id = await _setup_select(hass, mock_config_entry)
|
||||
"""Test select raises HomeAssistantError on connection error or write failure."""
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state is not None
|
||||
assert state.state != STATE_UNAVAILABLE
|
||||
|
||||
mock_qube_client.get_all_data = AsyncMock(
|
||||
mock_qube_client.set_sg_ready_mode = AsyncMock(
|
||||
side_effect=side_effect, return_value=return_value
|
||||
)
|
||||
|
||||
freezer.tick(timedelta(seconds=31))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state is not None
|
||||
assert state.state == STATE_UNAVAILABLE
|
||||
with pytest.raises(HomeAssistantError):
|
||||
await hass.services.async_call(
|
||||
SELECT_DOMAIN,
|
||||
SERVICE_SELECT_OPTION,
|
||||
{ATTR_ENTITY_ID: ENTITY_ID, ATTR_OPTION: "plus"},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
@@ -1,19 +1,15 @@
|
||||
"""Tests for the Qube Heat Pump switch platform."""
|
||||
|
||||
from datetime import timedelta
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.hr_energy_qube.const import DOMAIN
|
||||
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
SERVICE_TURN_OFF,
|
||||
SERVICE_TURN_ON,
|
||||
STATE_UNAVAILABLE,
|
||||
Platform,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -22,19 +18,9 @@ from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from . import setup_integration
|
||||
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform
|
||||
from tests.common import MockConfigEntry, snapshot_platform
|
||||
|
||||
HEATING_DEMAND_KEY = "heating_demand"
|
||||
|
||||
|
||||
def _get_entity_id(
|
||||
entity_registry: er.EntityRegistry, entry: MockConfigEntry, key: str
|
||||
) -> str:
|
||||
"""Look up entity_id by key and config entry."""
|
||||
unique_id = f"{entry.entry_id}-{key}"
|
||||
entity_id = entity_registry.async_get_entity_id(SWITCH_DOMAIN, DOMAIN, unique_id)
|
||||
assert entity_id is not None
|
||||
return entity_id
|
||||
ENTITY_ID = "switch.qube_heat_pump_heating_demand"
|
||||
|
||||
|
||||
async def test_entities(
|
||||
@@ -63,7 +49,6 @@ async def test_entities(
|
||||
)
|
||||
async def test_turn_on_off(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
mock_qube_client: MagicMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
service: str,
|
||||
@@ -72,11 +57,10 @@ async def test_turn_on_off(
|
||||
"""Test turning a switch on and off."""
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
entity_id = _get_entity_id(entity_registry, mock_config_entry, HEATING_DEMAND_KEY)
|
||||
await hass.services.async_call(
|
||||
SWITCH_DOMAIN,
|
||||
service,
|
||||
{ATTR_ENTITY_ID: entity_id},
|
||||
{ATTR_ENTITY_ID: ENTITY_ID},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
@@ -94,7 +78,6 @@ async def test_turn_on_off(
|
||||
)
|
||||
async def test_turn_on_error(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
mock_qube_client: MagicMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
side_effect: type[Exception] | None,
|
||||
@@ -106,49 +89,10 @@ async def test_turn_on_error(
|
||||
mock_qube_client.write_switch = AsyncMock(
|
||||
side_effect=side_effect, return_value=return_value
|
||||
)
|
||||
entity_id = _get_entity_id(entity_registry, mock_config_entry, HEATING_DEMAND_KEY)
|
||||
with pytest.raises(HomeAssistantError):
|
||||
await hass.services.async_call(
|
||||
SWITCH_DOMAIN,
|
||||
SERVICE_TURN_ON,
|
||||
{ATTR_ENTITY_ID: entity_id},
|
||||
{ATTR_ENTITY_ID: ENTITY_ID},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("side_effect", "return_value"),
|
||||
[
|
||||
(ConnectionError("Connection lost"), None),
|
||||
(None, None),
|
||||
],
|
||||
)
|
||||
async def test_switch_unavailable_on_coordinator_error(
|
||||
hass: HomeAssistant,
|
||||
mock_qube_client: MagicMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
side_effect: Exception | None,
|
||||
return_value: None,
|
||||
) -> None:
|
||||
"""Test switches become unavailable when coordinator fails."""
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
# Verify switches are available after setup
|
||||
states = hass.states.async_all("switch")
|
||||
assert len(states) > 0
|
||||
assert all(s.state != STATE_UNAVAILABLE for s in states)
|
||||
|
||||
# Make the next fetch fail
|
||||
mock_qube_client.get_all_data = AsyncMock(
|
||||
side_effect=side_effect, return_value=return_value
|
||||
)
|
||||
|
||||
# Skip time to trigger coordinator refresh
|
||||
freezer.tick(timedelta(seconds=31))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# All switches should be unavailable
|
||||
states = hass.states.async_all("switch")
|
||||
assert all(s.state == STATE_UNAVAILABLE for s in states)
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
"""Tests for the Qube Heat Pump water heater platform."""
|
||||
|
||||
from datetime import timedelta
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
@@ -16,14 +14,14 @@ from homeassistant.components.water_heater import (
|
||||
STATE_HEAT_PUMP,
|
||||
STATE_PERFORMANCE,
|
||||
)
|
||||
from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE, Platform
|
||||
from homeassistant.const import ATTR_ENTITY_ID, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from . import setup_integration
|
||||
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform
|
||||
from tests.common import MockConfigEntry, snapshot_platform
|
||||
|
||||
ENTITY_ID = "water_heater.qube_heat_pump_domestic_hot_water"
|
||||
|
||||
@@ -124,38 +122,3 @@ async def test_current_operation_boost(
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state is not None
|
||||
assert state.attributes["operation_mode"] == STATE_PERFORMANCE
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("side_effect", "return_value"),
|
||||
[
|
||||
(ConnectionError("Connection lost"), None),
|
||||
(None, None),
|
||||
],
|
||||
)
|
||||
async def test_water_heater_unavailable_on_coordinator_error(
|
||||
hass: HomeAssistant,
|
||||
mock_qube_client: MagicMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
side_effect: Exception | None,
|
||||
return_value: None,
|
||||
) -> None:
|
||||
"""Test water heater becomes unavailable when coordinator fails."""
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state is not None
|
||||
assert state.state != STATE_UNAVAILABLE
|
||||
|
||||
mock_qube_client.get_all_data = AsyncMock(
|
||||
side_effect=side_effect, return_value=return_value
|
||||
)
|
||||
|
||||
freezer.tick(timedelta(seconds=31))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state is not None
|
||||
assert state.state == STATE_UNAVAILABLE
|
||||
|
||||
Reference in New Issue
Block a user