Palazzetti integration (#128259)

Co-authored-by: Joostlek <joostlek@outlook.com>
This commit is contained in:
dotvav 2024-10-28 17:19:05 +01:00 committed by GitHub
parent 80202f33cb
commit 8eb68b54d9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 897 additions and 0 deletions

View File

@ -1091,6 +1091,8 @@ build.json @home-assistant/supervisor
/tests/components/ovo_energy/ @timmo001
/homeassistant/components/p1_monitor/ @klaasnicolaas
/tests/components/p1_monitor/ @klaasnicolaas
/homeassistant/components/palazzetti/ @dotvav
/tests/components/palazzetti/ @dotvav
/homeassistant/components/panel_custom/ @home-assistant/frontend
/tests/components/panel_custom/ @home-assistant/frontend
/homeassistant/components/peco/ @IceBotYT

View File

@ -0,0 +1,27 @@
"""The Palazzetti integration."""
from __future__ import annotations
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from .coordinator import PalazzettiConfigEntry, PalazzettiDataUpdateCoordinator
PLATFORMS: list[Platform] = [Platform.CLIMATE]
async def async_setup_entry(hass: HomeAssistant, entry: PalazzettiConfigEntry) -> bool:
"""Set up Palazzetti from a config entry."""
coordinator = PalazzettiDataUpdateCoordinator(hass)
await coordinator.async_config_entry_first_refresh()
entry.runtime_data = coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: PalazzettiConfigEntry) -> bool:
"""Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)

View File

@ -0,0 +1,160 @@
"""Support for Palazzetti climates."""
from typing import Any
from pypalazzetti.exceptions import CommunicationError, ValidationError
from homeassistant.components.climate import (
ClimateEntity,
ClimateEntityFeature,
HVACMode,
)
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import PalazzettiConfigEntry
from .const import DOMAIN, FAN_AUTO, FAN_HIGH, FAN_MODES, FAN_SILENT, PALAZZETTI
from .coordinator import PalazzettiDataUpdateCoordinator
async def async_setup_entry(
hass: HomeAssistant,
entry: PalazzettiConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up Palazzetti climates based on a config entry."""
async_add_entities([PalazzettiClimateEntity(entry.runtime_data)])
class PalazzettiClimateEntity(
CoordinatorEntity[PalazzettiDataUpdateCoordinator], ClimateEntity
):
"""Defines a Palazzetti climate."""
_attr_has_entity_name = True
_attr_name = None
_attr_translation_key = DOMAIN
_attr_target_temperature_step = 1.0
_attr_temperature_unit = UnitOfTemperature.CELSIUS
_attr_supported_features = (
ClimateEntityFeature.TARGET_TEMPERATURE
| ClimateEntityFeature.FAN_MODE
| ClimateEntityFeature.TURN_ON
| ClimateEntityFeature.TURN_OFF
)
def __init__(self, coordinator: PalazzettiDataUpdateCoordinator) -> None:
"""Initialize Palazzetti climate."""
super().__init__(coordinator)
client = coordinator.client
mac = coordinator.config_entry.unique_id
assert mac is not None
self._attr_unique_id = mac
self._attr_device_info = dr.DeviceInfo(
connections={(dr.CONNECTION_NETWORK_MAC, mac)},
name=client.name,
manufacturer=PALAZZETTI,
sw_version=client.sw_version,
hw_version=client.hw_version,
)
self._attr_hvac_modes = [HVACMode.HEAT, HVACMode.OFF]
self._attr_min_temp = client.target_temperature_min
self._attr_max_temp = client.target_temperature_max
self._attr_fan_modes = list(
map(str, range(client.fan_speed_min, client.fan_speed_max + 1))
)
if client.has_fan_silent:
self._attr_fan_modes.insert(0, FAN_SILENT)
if client.has_fan_high:
self._attr_fan_modes.append(FAN_HIGH)
if client.has_fan_auto:
self._attr_fan_modes.append(FAN_AUTO)
@property
def available(self) -> bool:
"""Is the entity available."""
return super().available and self.coordinator.client.connected
@property
def hvac_mode(self) -> HVACMode:
"""Return hvac operation ie. heat or off mode."""
is_heating = bool(self.coordinator.client.is_heating)
return HVACMode.HEAT if is_heating else HVACMode.OFF
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
"""Set new target hvac mode."""
try:
await self.coordinator.client.set_on(hvac_mode != HVACMode.OFF)
except CommunicationError as err:
raise HomeAssistantError(
translation_domain=DOMAIN, translation_key="cannot_connect"
) from err
except ValidationError as err:
raise ServiceValidationError(
translation_domain=DOMAIN, translation_key="on_off_not_available"
) from err
await self.coordinator.async_refresh()
@property
def current_temperature(self) -> float | None:
"""Return current temperature."""
return self.coordinator.client.room_temperature
@property
def target_temperature(self) -> int | None:
"""Return the temperature."""
return self.coordinator.client.target_temperature
async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set new temperature."""
temperature = int(kwargs[ATTR_TEMPERATURE])
try:
await self.coordinator.client.set_target_temperature(temperature)
except CommunicationError as err:
raise HomeAssistantError(
translation_domain=DOMAIN, translation_key="cannot_connect"
) from err
except ValidationError as err:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="invalid_target_temperature",
translation_placeholders={
"value": str(temperature),
},
) from err
await self.coordinator.async_refresh()
@property
def fan_mode(self) -> str | None:
"""Return the fan mode."""
api_state = self.coordinator.client.fan_speed
return FAN_MODES[api_state]
async def async_set_fan_mode(self, fan_mode: str) -> None:
"""Set new fan mode."""
try:
if fan_mode == FAN_SILENT:
await self.coordinator.client.set_fan_silent()
elif fan_mode == FAN_HIGH:
await self.coordinator.client.set_fan_high()
elif fan_mode == FAN_AUTO:
await self.coordinator.client.set_fan_auto()
else:
await self.coordinator.client.set_fan_speed(FAN_MODES.index(fan_mode))
except CommunicationError as err:
raise HomeAssistantError(
translation_domain=DOMAIN, translation_key="cannot_connect"
) from err
except ValidationError as err:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="invalid_fan_mode",
translation_placeholders={
"value": fan_mode,
},
) from err
await self.coordinator.async_refresh()

View File

@ -0,0 +1,50 @@
"""Config flow for Palazzetti."""
from typing import Any
from pypalazzetti.client import PalazzettiClient
from pypalazzetti.exceptions import CommunicationError
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_HOST
from homeassistant.helpers import device_registry as dr
from .const import DOMAIN, LOGGER
class PalazzettiConfigFlow(ConfigFlow, domain=DOMAIN):
"""Palazzetti config flow."""
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""User configuration step."""
errors: dict[str, str] = {}
if user_input is not None:
host = user_input[CONF_HOST]
client = PalazzettiClient(hostname=host)
try:
await client.connect()
except CommunicationError:
LOGGER.exception("Communication error")
errors["base"] = "cannot_connect"
else:
formatted_mac = dr.format_mac(client.mac)
# Assign a unique ID to the flow
await self.async_set_unique_id(formatted_mac)
# Abort the flow if a config entry with the same unique ID exists
self._abort_if_unique_id_configured()
return self.async_create_entry(
title=client.name,
data=user_input,
)
return self.async_show_form(
step_id="user",
data_schema=vol.Schema({vol.Required(CONF_HOST): str}),
errors=errors,
)

View File

@ -0,0 +1,19 @@
"""Constants for the Palazzetti integration."""
from datetime import timedelta
import logging
from typing import Final
DOMAIN: Final = "palazzetti"
PALAZZETTI: Final = "Palazzetti"
LOGGER = logging.getLogger(__package__)
SCAN_INTERVAL = timedelta(seconds=30)
ON_OFF_NOT_AVAILABLE = "on_off_not_available"
ERROR_INVALID_FAN_MODE = "invalid_fan_mode"
ERROR_INVALID_TARGET_TEMPERATURE = "invalid_target_temperature"
ERROR_CANNOT_CONNECT = "cannot_connect"
FAN_SILENT: Final = "silent"
FAN_HIGH: Final = "high"
FAN_AUTO: Final = "auto"
FAN_MODES: Final = [FAN_SILENT, "1", "2", "3", "4", "5", FAN_HIGH, FAN_AUTO]

View File

@ -0,0 +1,47 @@
"""Helpers to help coordinate updates."""
from pypalazzetti.client import PalazzettiClient
from pypalazzetti.exceptions import CommunicationError, ValidationError
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DOMAIN, LOGGER, SCAN_INTERVAL
type PalazzettiConfigEntry = ConfigEntry[PalazzettiDataUpdateCoordinator]
class PalazzettiDataUpdateCoordinator(DataUpdateCoordinator[None]):
"""Class to manage fetching Palazzetti data from a Palazzetti hub."""
config_entry: PalazzettiConfigEntry
client: PalazzettiClient
def __init__(
self,
hass: HomeAssistant,
) -> None:
"""Initialize global Palazzetti data updater."""
super().__init__(
hass,
LOGGER,
name=DOMAIN,
update_interval=SCAN_INTERVAL,
)
self.client = PalazzettiClient(self.config_entry.data[CONF_HOST])
async def _async_setup(self) -> None:
try:
await self.client.connect()
await self.client.update_state()
except (CommunicationError, ValidationError) as err:
raise UpdateFailed(f"Error communicating with the API: {err}") from err
async def _async_update_data(self) -> None:
"""Fetch data from Palazzetti."""
try:
await self.client.update_state()
except (CommunicationError, ValidationError) as err:
raise UpdateFailed(f"Error communicating with the API: {err}") from err

View File

@ -0,0 +1,10 @@
{
"domain": "palazzetti",
"name": "Palazzetti",
"codeowners": ["@dotvav"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/palazzetti",
"integration_type": "device",
"iot_class": "local_polling",
"requirements": ["pypalazzetti==0.1.6"]
}

View File

@ -0,0 +1,49 @@
{
"config": {
"step": {
"user": {
"data": {
"host": "[%key:common::config_flow::data::host%]"
},
"data_description": {
"host": "The host name or the IP address of the Palazzetti CBox"
}
}
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
}
},
"exceptions": {
"on_off_not_available": {
"message": "The appliance cannot be turned on or off."
},
"invalid_fan_mode": {
"message": "Fan mode {value} is invalid."
},
"invalid_target_temperatures": {
"message": "Target temperature {value} is invalid."
},
"cannot_connect": {
"message": "Could not connect to the device."
}
},
"entity": {
"climate": {
"palazzetti": {
"state_attributes": {
"fan_mode": {
"state": {
"silent": "Silent",
"auto": "Auto",
"high": "High"
}
}
}
}
}
}
}

View File

@ -439,6 +439,7 @@ FLOWS = {
"ovo_energy",
"owntracks",
"p1_monitor",
"palazzetti",
"panasonic_viera",
"peco",
"pegel_online",

View File

@ -4530,6 +4530,12 @@
"config_flow": true,
"iot_class": "local_polling"
},
"palazzetti": {
"name": "Palazzetti",
"integration_type": "device",
"config_flow": true,
"iot_class": "local_polling"
},
"panasonic": {
"name": "Panasonic",
"integrations": {

View File

@ -2142,6 +2142,9 @@ pyoverkiz==1.14.1
# homeassistant.components.onewire
pyownet==0.10.0.post1
# homeassistant.components.palazzetti
pypalazzetti==0.1.6
# homeassistant.components.elv
pypca==0.0.7

View File

@ -1729,6 +1729,9 @@ pyoverkiz==1.14.1
# homeassistant.components.onewire
pyownet==0.10.0.post1
# homeassistant.components.palazzetti
pypalazzetti==0.1.6
# homeassistant.components.lcn
pypck==0.7.24

View File

@ -0,0 +1,13 @@
"""Tests for the Palazzetti integration."""
from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry
async def setup_integration(hass: HomeAssistant, config_entry: MockConfigEntry) -> None:
"""Fixture for setting up the component."""
config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()

View File

@ -0,0 +1,74 @@
"""Fixtures for Palazzetti integration tests."""
from collections.abc import Generator
from unittest.mock import AsyncMock, patch
import pytest
from homeassistant.components.palazzetti.const import DOMAIN
from homeassistant.const import CONF_HOST
from tests.common import MockConfigEntry
@pytest.fixture
def mock_setup_entry() -> Generator[AsyncMock]:
"""Override async_setup_entry."""
with patch(
"homeassistant.components.palazzetti.async_setup_entry",
return_value=True,
) as mock_setup_entry:
yield mock_setup_entry
@pytest.fixture
def mock_config_entry() -> MockConfigEntry:
"""Return the default mocked config entry."""
return MockConfigEntry(
title="palazzetti",
domain=DOMAIN,
data={CONF_HOST: "127.0.0.1"},
unique_id="11:22:33:44:55:66",
)
@pytest.fixture
def mock_palazzetti_client() -> Generator[AsyncMock]:
"""Return a mocked PalazzettiClient."""
with (
patch(
"homeassistant.components.palazzetti.coordinator.PalazzettiClient",
autospec=True,
) as client,
patch(
"homeassistant.components.palazzetti.config_flow.PalazzettiClient",
new=client,
),
):
mock_client = client.return_value
mock_client.mac = "11:22:33:44:55:66"
mock_client.name = "Stove"
mock_client.sw_version = "0.0.0"
mock_client.hw_version = "1.1.1"
mock_client.fan_speed_min = 1
mock_client.fan_speed_max = 5
mock_client.has_fan_silent = True
mock_client.has_fan_high = True
mock_client.has_fan_auto = True
mock_client.has_on_off_switch = True
mock_client.connected = True
mock_client.is_heating = True
mock_client.room_temperature = 18
mock_client.target_temperature = 21
mock_client.target_temperature_min = 5
mock_client.target_temperature_max = 50
mock_client.fan_speed = 3
mock_client.connect.return_value = True
mock_client.update_state.return_value = True
mock_client.set_on.return_value = True
mock_client.set_target_temperature.return_value = True
mock_client.set_fan_speed.return_value = True
mock_client.set_fan_silent.return_value = True
mock_client.set_fan_high.return_value = True
mock_client.set_fan_auto.return_value = True
yield mock_client

View File

@ -0,0 +1,86 @@
# serializer version: 1
# name: test_all_entities[climate.stove-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'fan_modes': list([
'silent',
'1',
'2',
'3',
'4',
'5',
'high',
'auto',
]),
'hvac_modes': list([
<HVACMode.HEAT: 'heat'>,
<HVACMode.OFF: 'off'>,
]),
'max_temp': 50,
'min_temp': 5,
'target_temp_step': 1.0,
}),
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'climate',
'entity_category': None,
'entity_id': 'climate.stove',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': None,
'platform': 'palazzetti',
'previous_unique_id': None,
'supported_features': <ClimateEntityFeature: 393>,
'translation_key': 'palazzetti',
'unique_id': '11:22:33:44:55:66',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[climate.stove-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'current_temperature': 18,
'fan_mode': '3',
'fan_modes': list([
'silent',
'1',
'2',
'3',
'4',
'5',
'high',
'auto',
]),
'friendly_name': 'Stove',
'hvac_modes': list([
<HVACMode.HEAT: 'heat'>,
<HVACMode.OFF: 'off'>,
]),
'max_temp': 50,
'min_temp': 5,
'supported_features': <ClimateEntityFeature: 393>,
'target_temp_step': 1.0,
'temperature': 21,
}),
'context': <ANY>,
'entity_id': 'climate.stove',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'heat',
})
# ---

View File

@ -0,0 +1,33 @@
# serializer version: 1
# name: test_device
DeviceRegistryEntrySnapshot({
'area_id': None,
'config_entries': <ANY>,
'configuration_url': None,
'connections': set({
tuple(
'mac',
'11:22:33:44:55:66',
),
}),
'disabled_by': None,
'entry_type': None,
'hw_version': '1.1.1',
'id': <ANY>,
'identifiers': set({
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Palazzetti',
'model': None,
'model_id': None,
'name': 'Stove',
'name_by_user': None,
'primary_config_entry': <ANY>,
'serial_number': None,
'suggested_area': None,
'sw_version': '0.0.0',
'via_device_id': None,
})
# ---

View File

@ -0,0 +1,174 @@
"""Tests for the Palazzetti climate platform."""
from unittest.mock import AsyncMock, patch
from pypalazzetti.exceptions import CommunicationError, ValidationError
import pytest
from syrupy import SnapshotAssertion
from homeassistant.components.climate import (
ATTR_FAN_MODE,
ATTR_HVAC_MODE,
DOMAIN as CLIMATE_DOMAIN,
SERVICE_SET_FAN_MODE,
SERVICE_SET_HVAC_MODE,
SERVICE_SET_TEMPERATURE,
HVACMode,
)
from homeassistant.components.palazzetti.const import FAN_AUTO, FAN_HIGH, FAN_SILENT
from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.helpers import entity_registry as er
from . import setup_integration
from tests.common import MockConfigEntry, snapshot_platform
ENTITY_ID = "climate.stove"
async def test_all_entities(
hass: HomeAssistant,
snapshot: SnapshotAssertion,
mock_palazzetti_client: AsyncMock,
mock_config_entry: MockConfigEntry,
entity_registry: er.EntityRegistry,
) -> None:
"""Test all entities."""
with patch("homeassistant.components.palazzetti.PLATFORMS", [Platform.CLIMATE]):
await setup_integration(hass, mock_config_entry)
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
async def test_async_set_data(
hass: HomeAssistant,
mock_palazzetti_client: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test setting climate data via service call."""
await setup_integration(hass, mock_config_entry)
# Set HVAC Mode: Success
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_HVAC_MODE,
{ATTR_ENTITY_ID: ENTITY_ID, ATTR_HVAC_MODE: HVACMode.HEAT},
blocking=True,
)
mock_palazzetti_client.set_on.assert_called_once_with(True)
mock_palazzetti_client.set_on.reset_mock()
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_HVAC_MODE,
{ATTR_ENTITY_ID: ENTITY_ID, ATTR_HVAC_MODE: HVACMode.OFF},
blocking=True,
)
mock_palazzetti_client.set_on.assert_called_once_with(False)
mock_palazzetti_client.set_on.reset_mock()
# Set HVAC Mode: Error
mock_palazzetti_client.set_on.side_effect = CommunicationError()
with pytest.raises(HomeAssistantError):
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_HVAC_MODE,
{ATTR_ENTITY_ID: ENTITY_ID, ATTR_HVAC_MODE: HVACMode.HEAT},
blocking=True,
)
mock_palazzetti_client.set_on.side_effect = ValidationError()
with pytest.raises(ServiceValidationError):
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_HVAC_MODE,
{ATTR_ENTITY_ID: ENTITY_ID, ATTR_HVAC_MODE: HVACMode.HEAT},
blocking=True,
)
# Set Temperature: Success
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_TEMPERATURE,
{ATTR_ENTITY_ID: ENTITY_ID, ATTR_TEMPERATURE: 22},
blocking=True,
)
mock_palazzetti_client.set_target_temperature.assert_called_once_with(22)
mock_palazzetti_client.set_target_temperature.reset_mock()
# Set Temperature: Error
mock_palazzetti_client.set_target_temperature.side_effect = CommunicationError()
with pytest.raises(HomeAssistantError):
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_TEMPERATURE,
{ATTR_ENTITY_ID: ENTITY_ID, ATTR_TEMPERATURE: 22},
blocking=True,
)
mock_palazzetti_client.set_target_temperature.side_effect = ValidationError()
with pytest.raises(ServiceValidationError):
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_TEMPERATURE,
{ATTR_ENTITY_ID: ENTITY_ID, ATTR_TEMPERATURE: 22},
blocking=True,
)
# Set Fan Mode: Success
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_FAN_MODE,
{ATTR_ENTITY_ID: ENTITY_ID, ATTR_FAN_MODE: FAN_SILENT},
blocking=True,
)
mock_palazzetti_client.set_fan_silent.assert_called_once()
mock_palazzetti_client.set_fan_silent.reset_mock()
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_FAN_MODE,
{ATTR_ENTITY_ID: ENTITY_ID, ATTR_FAN_MODE: FAN_HIGH},
blocking=True,
)
mock_palazzetti_client.set_fan_high.assert_called_once()
mock_palazzetti_client.set_fan_high.reset_mock()
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_FAN_MODE,
{ATTR_ENTITY_ID: ENTITY_ID, ATTR_FAN_MODE: FAN_AUTO},
blocking=True,
)
mock_palazzetti_client.set_fan_auto.assert_called_once()
mock_palazzetti_client.set_fan_auto.reset_mock()
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_FAN_MODE,
{ATTR_ENTITY_ID: ENTITY_ID, ATTR_FAN_MODE: "3"},
blocking=True,
)
mock_palazzetti_client.set_fan_speed.assert_called_once_with(3)
mock_palazzetti_client.set_fan_speed.reset_mock()
# Set Fan Mode: Error
mock_palazzetti_client.set_fan_speed.side_effect = CommunicationError()
with pytest.raises(HomeAssistantError):
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_FAN_MODE,
{ATTR_ENTITY_ID: ENTITY_ID, ATTR_FAN_MODE: 3},
blocking=True,
)
mock_palazzetti_client.set_fan_speed.side_effect = ValidationError()
with pytest.raises(ServiceValidationError):
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_FAN_MODE,
{ATTR_ENTITY_ID: ENTITY_ID, ATTR_FAN_MODE: 3},
blocking=True,
)

View File

@ -0,0 +1,94 @@
"""Test the Palazzetti config flow."""
from unittest.mock import AsyncMock
from pypalazzetti.exceptions import CommunicationError
from homeassistant.components.palazzetti.const import DOMAIN
from homeassistant.config_entries import SOURCE_USER
from homeassistant.const import CONF_HOST
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
from tests.common import MockConfigEntry
async def test_full_user_flow(
hass: HomeAssistant, mock_palazzetti_client: AsyncMock, mock_setup_entry: AsyncMock
) -> None:
"""Test the full user configuration flow."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "user"
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={CONF_HOST: "192.168.1.1"},
)
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["title"] == "Stove"
assert result["data"] == {CONF_HOST: "192.168.1.1"}
assert result["result"].unique_id == "11:22:33:44:55:66"
assert len(mock_palazzetti_client.connect.mock_calls) > 0
async def test_invalid_host(
hass: HomeAssistant,
mock_palazzetti_client: AsyncMock,
mock_setup_entry: AsyncMock,
) -> None:
"""Test cannot connect error."""
mock_palazzetti_client.connect.side_effect = CommunicationError()
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "user"
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={CONF_HOST: "192.168.1.1"},
)
assert result["type"] is FlowResultType.FORM
assert result["errors"] == {"base": "cannot_connect"}
mock_palazzetti_client.connect.side_effect = None
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={CONF_HOST: "192.168.1.1"},
)
assert result["type"] is FlowResultType.CREATE_ENTRY
async def test_duplicate(
hass: HomeAssistant,
mock_palazzetti_client: AsyncMock,
mock_setup_entry: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test duplicate flow."""
mock_config_entry.add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_USER},
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "user"
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{CONF_HOST: "192.168.1.1"},
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "already_configured"

View File

@ -0,0 +1,46 @@
"""Tests for the Palazzetti integration."""
from unittest.mock import AsyncMock
from syrupy import SnapshotAssertion
from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
from . import setup_integration
from tests.common import MockConfigEntry
async def test_load_unload_config_entry(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_palazzetti_client: AsyncMock,
) -> None:
"""Test the Palazzetti configuration entry loading/unloading."""
await setup_integration(hass, mock_config_entry)
assert mock_config_entry.state is ConfigEntryState.LOADED
await hass.config_entries.async_unload(mock_config_entry.entry_id)
await hass.async_block_till_done()
assert mock_config_entry.state is ConfigEntryState.NOT_LOADED
async def test_device(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_palazzetti_client: AsyncMock,
snapshot: SnapshotAssertion,
device_registry: dr.DeviceRegistry,
) -> None:
"""Test the device information."""
await setup_integration(hass, mock_config_entry)
device = device_registry.async_get_device(
connections={(dr.CONNECTION_NETWORK_MAC, "11:22:33:44:55:66")}
)
assert device is not None
assert device == snapshot