mirror of
https://github.com/home-assistant/core.git
synced 2025-04-24 01:08:12 +00:00
Palazzetti integration (#128259)
Co-authored-by: Joostlek <joostlek@outlook.com>
This commit is contained in:
parent
80202f33cb
commit
8eb68b54d9
@ -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
|
||||
|
27
homeassistant/components/palazzetti/__init__.py
Normal file
27
homeassistant/components/palazzetti/__init__.py
Normal 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)
|
160
homeassistant/components/palazzetti/climate.py
Normal file
160
homeassistant/components/palazzetti/climate.py
Normal 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()
|
50
homeassistant/components/palazzetti/config_flow.py
Normal file
50
homeassistant/components/palazzetti/config_flow.py
Normal 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,
|
||||
)
|
19
homeassistant/components/palazzetti/const.py
Normal file
19
homeassistant/components/palazzetti/const.py
Normal 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]
|
47
homeassistant/components/palazzetti/coordinator.py
Normal file
47
homeassistant/components/palazzetti/coordinator.py
Normal 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
|
10
homeassistant/components/palazzetti/manifest.json
Normal file
10
homeassistant/components/palazzetti/manifest.json
Normal 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"]
|
||||
}
|
49
homeassistant/components/palazzetti/strings.json
Normal file
49
homeassistant/components/palazzetti/strings.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -439,6 +439,7 @@ FLOWS = {
|
||||
"ovo_energy",
|
||||
"owntracks",
|
||||
"p1_monitor",
|
||||
"palazzetti",
|
||||
"panasonic_viera",
|
||||
"peco",
|
||||
"pegel_online",
|
||||
|
@ -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": {
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
13
tests/components/palazzetti/__init__.py
Normal file
13
tests/components/palazzetti/__init__.py
Normal 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()
|
74
tests/components/palazzetti/conftest.py
Normal file
74
tests/components/palazzetti/conftest.py
Normal 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
|
86
tests/components/palazzetti/snapshots/test_climate.ambr
Normal file
86
tests/components/palazzetti/snapshots/test_climate.ambr
Normal 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',
|
||||
})
|
||||
# ---
|
33
tests/components/palazzetti/snapshots/test_init.ambr
Normal file
33
tests/components/palazzetti/snapshots/test_init.ambr
Normal 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,
|
||||
})
|
||||
# ---
|
174
tests/components/palazzetti/test_climate.py
Normal file
174
tests/components/palazzetti/test_climate.py
Normal 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,
|
||||
)
|
94
tests/components/palazzetti/test_config_flow.py
Normal file
94
tests/components/palazzetti/test_config_flow.py
Normal 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"
|
46
tests/components/palazzetti/test_init.py
Normal file
46
tests/components/palazzetti/test_init.py
Normal 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
|
Loading…
x
Reference in New Issue
Block a user