1
mirror of https://github.com/home-assistant/core synced 2024-09-25 00:41:32 +02:00

Add Switcher button platform (#81245)

This commit is contained in:
Shay Levy 2022-11-28 10:06:14 +02:00 committed by GitHub
parent ec823582eb
commit f97ac9fdcd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 330 additions and 7 deletions

View File

@ -29,7 +29,13 @@ from .const import (
)
from .utils import async_start_bridge, async_stop_bridge
PLATFORMS = [Platform.CLIMATE, Platform.COVER, Platform.SENSOR, Platform.SWITCH]
PLATFORMS = [
Platform.BUTTON,
Platform.CLIMATE,
Platform.COVER,
Platform.SENSOR,
Platform.SWITCH,
]
_LOGGER = logging.getLogger(__name__)

View File

@ -0,0 +1,159 @@
"""Switcher integration Button platform."""
from __future__ import annotations
import asyncio
from collections.abc import Callable
from dataclasses import dataclass
from aioswitcher.api import (
DeviceState,
SwitcherBaseResponse,
SwitcherType2Api,
ThermostatSwing,
)
from aioswitcher.api.remotes import SwitcherBreezeRemote
from aioswitcher.device import DeviceCategory
from homeassistant.components.button import ButtonEntity, ButtonEntityDescription
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import device_registry
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import DeviceInfo, EntityCategory
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import SwitcherDataUpdateCoordinator
from .const import SIGNAL_DEVICE_ADD
from .utils import get_breeze_remote_manager
@dataclass
class SwitcherThermostatButtonDescriptionMixin:
"""Mixin to describe a Switcher Thermostat Button entity."""
press_fn: Callable[[SwitcherType2Api, SwitcherBreezeRemote], SwitcherBaseResponse]
supported: Callable[[SwitcherBreezeRemote], bool]
@dataclass
class SwitcherThermostatButtonEntityDescription(
ButtonEntityDescription, SwitcherThermostatButtonDescriptionMixin
):
"""Class to describe a Switcher Thermostat Button entity."""
THERMOSTAT_BUTTONS = [
SwitcherThermostatButtonEntityDescription(
key="assume_on",
name="Assume on",
icon="mdi:fan",
entity_category=EntityCategory.CONFIG,
press_fn=lambda api, remote: api.control_breeze_device(
remote, state=DeviceState.ON, update_state=True
),
supported=lambda remote: bool(remote.on_off_type),
),
SwitcherThermostatButtonEntityDescription(
key="assume_off",
name="Assume off",
icon="mdi:fan-off",
entity_category=EntityCategory.CONFIG,
press_fn=lambda api, remote: api.control_breeze_device(
remote, state=DeviceState.OFF, update_state=True
),
supported=lambda remote: bool(remote.on_off_type),
),
SwitcherThermostatButtonEntityDescription(
key="vertical_swing_on",
name="Vertical swing on",
icon="mdi:autorenew",
press_fn=lambda api, remote: api.control_breeze_device(
remote, swing=ThermostatSwing.ON
),
supported=lambda remote: bool(remote.separated_swing_command),
),
SwitcherThermostatButtonEntityDescription(
key="vertical_swing_off",
name="Vertical swing off",
icon="mdi:autorenew-off",
press_fn=lambda api, remote: api.control_breeze_device(
remote, swing=ThermostatSwing.OFF
),
supported=lambda remote: bool(remote.separated_swing_command),
),
]
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up Switcher button from config entry."""
@callback
async def async_add_buttons(coordinator: SwitcherDataUpdateCoordinator) -> None:
"""Get remote and add button from Switcher device."""
if coordinator.data.device_type.category == DeviceCategory.THERMOSTAT:
remote: SwitcherBreezeRemote = await hass.async_add_executor_job(
get_breeze_remote_manager(hass).get_remote, coordinator.data.remote_id
)
async_add_entities(
SwitcherThermostatButtonEntity(coordinator, description, remote)
for description in THERMOSTAT_BUTTONS
if description.supported(remote)
)
config_entry.async_on_unload(
async_dispatcher_connect(hass, SIGNAL_DEVICE_ADD, async_add_buttons)
)
class SwitcherThermostatButtonEntity(
CoordinatorEntity[SwitcherDataUpdateCoordinator], ButtonEntity
):
"""Representation of a Switcher climate entity."""
entity_description: SwitcherThermostatButtonEntityDescription
def __init__(
self,
coordinator: SwitcherDataUpdateCoordinator,
description: SwitcherThermostatButtonEntityDescription,
remote: SwitcherBreezeRemote,
) -> None:
"""Initialize the entity."""
super().__init__(coordinator)
self.entity_description = description
self._remote = remote
self._attr_name = f"{coordinator.name} {description.name}"
self._attr_unique_id = f"{coordinator.mac_address}-{description.key}"
self._attr_device_info = DeviceInfo(
connections={
(device_registry.CONNECTION_NETWORK_MAC, coordinator.mac_address)
}
)
async def async_press(self) -> None:
"""Press the button."""
response: SwitcherBaseResponse = None
error = None
try:
async with SwitcherType2Api(
self.coordinator.data.ip_address, self.coordinator.data.device_id
) as swapi:
response = await self.entity_description.press_fn(swapi, self._remote)
except (asyncio.TimeoutError, OSError, RuntimeError) as err:
error = repr(err)
if error or not response or not response.successful:
self.coordinator.last_update_success = False
self.async_write_ha_state()
raise HomeAssistantError(
f"Call api for {self.name} failed, "
f"response/error: {response or error}"
)

View File

@ -5,7 +5,7 @@ import asyncio
from typing import Any, cast
from aioswitcher.api import SwitcherBaseResponse, SwitcherType2Api
from aioswitcher.api.remotes import SwitcherBreezeRemote, SwitcherBreezeRemoteManager
from aioswitcher.api.remotes import SwitcherBreezeRemote
from aioswitcher.device import (
DeviceCategory,
DeviceState,
@ -37,6 +37,7 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import SwitcherDataUpdateCoordinator
from .const import SIGNAL_DEVICE_ADD
from .utils import get_breeze_remote_manager
DEVICE_MODE_TO_HA = {
ThermostatMode.COOL: HVACMode.COOL,
@ -64,13 +65,12 @@ async def async_setup_entry(
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up Switcher climate from config entry."""
remote_manager = SwitcherBreezeRemoteManager()
async def async_add_climate(coordinator: SwitcherDataUpdateCoordinator) -> None:
"""Get remote and add climate from Switcher device."""
if coordinator.data.device_type.category == DeviceCategory.THERMOSTAT:
remote: SwitcherBreezeRemote = await hass.async_add_executor_job(
remote_manager.get_remote, coordinator.data.remote_id
get_breeze_remote_manager(hass).get_remote, coordinator.data.remote_id
)
async_add_entities([SwitcherClimateEntity(coordinator, remote)])

View File

@ -3,7 +3,7 @@
"name": "Switcher",
"documentation": "https://www.home-assistant.io/integrations/switcher_kis/",
"codeowners": ["@tomerfi", "@thecode"],
"requirements": ["aioswitcher==3.1.0"],
"requirements": ["aioswitcher==3.2.0"],
"quality_scale": "platinum",
"iot_class": "local_push",
"config_flow": true,

View File

@ -6,9 +6,11 @@ from collections.abc import Callable
import logging
from typing import Any
from aioswitcher.api.remotes import SwitcherBreezeRemoteManager
from aioswitcher.bridge import SwitcherBase, SwitcherBridge
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import singleton
from .const import DATA_BRIDGE, DISCOVERY_TIME_SEC, DOMAIN
@ -53,3 +55,9 @@ async def async_discover_devices() -> dict[str, SwitcherBase]:
_LOGGER.debug("Finished discovery, discovered devices: %s", len(discovered_devices))
return discovered_devices
@singleton.singleton("switcher_breeze_remote_manager")
def get_breeze_remote_manager(hass: HomeAssistant) -> SwitcherBreezeRemoteManager:
"""Get Switcher Breeze remote manager."""
return SwitcherBreezeRemoteManager()

View File

@ -273,7 +273,7 @@ aioslimproto==2.1.1
aiosteamist==0.3.2
# homeassistant.components.switcher_kis
aioswitcher==3.1.0
aioswitcher==3.2.0
# homeassistant.components.syncthing
aiosyncthing==0.5.1

View File

@ -248,7 +248,7 @@ aioslimproto==2.1.1
aiosteamist==0.3.2
# homeassistant.components.switcher_kis
aioswitcher==3.1.0
aioswitcher==3.2.0
# homeassistant.components.syncthing
aiosyncthing==0.5.1

View File

@ -0,0 +1,150 @@
"""Tests for Switcher button platform."""
from unittest.mock import ANY, patch
from aioswitcher.api import DeviceState, SwitcherBaseResponse, ThermostatSwing
import pytest
from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS
from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.util import slugify
from . import init_integration
from .consts import DUMMY_THERMOSTAT_DEVICE as DEVICE
BASE_ENTITY_ID = f"{BUTTON_DOMAIN}.{slugify(DEVICE.name)}"
ASSUME_ON_EID = BASE_ENTITY_ID + "_assume_on"
ASSUME_OFF_EID = BASE_ENTITY_ID + "_assume_off"
SWING_ON_EID = BASE_ENTITY_ID + "_vertical_swing_on"
SWING_OFF_EID = BASE_ENTITY_ID + "_vertical_swing_off"
@pytest.mark.parametrize("mock_bridge", [[DEVICE]], indirect=True)
async def test_assume_button(hass: HomeAssistant, mock_bridge, mock_api):
"""Test assume on/off button."""
await init_integration(hass)
assert mock_bridge
assert hass.states.get(ASSUME_ON_EID) is not None
assert hass.states.get(ASSUME_OFF_EID) is not None
assert hass.states.get(SWING_ON_EID) is None
assert hass.states.get(SWING_OFF_EID) is None
with patch(
"homeassistant.components.switcher_kis.climate.SwitcherType2Api.control_breeze_device",
) as mock_control_device:
await hass.services.async_call(
BUTTON_DOMAIN,
SERVICE_PRESS,
{ATTR_ENTITY_ID: ASSUME_ON_EID},
blocking=True,
)
assert mock_api.call_count == 2
mock_control_device.assert_called_once_with(
ANY, state=DeviceState.ON, update_state=True
)
mock_control_device.reset_mock()
await hass.services.async_call(
BUTTON_DOMAIN,
SERVICE_PRESS,
{ATTR_ENTITY_ID: ASSUME_OFF_EID},
blocking=True,
)
assert mock_api.call_count == 4
mock_control_device.assert_called_once_with(
ANY, state=DeviceState.OFF, update_state=True
)
@pytest.mark.parametrize("mock_bridge", [[DEVICE]], indirect=True)
async def test_swing_button(hass: HomeAssistant, mock_bridge, mock_api, monkeypatch):
"""Test vertical swing on/off button."""
monkeypatch.setattr(DEVICE, "remote_id", "ELEC7022")
await init_integration(hass)
assert mock_bridge
assert hass.states.get(ASSUME_ON_EID) is None
assert hass.states.get(ASSUME_OFF_EID) is None
assert hass.states.get(SWING_ON_EID) is not None
assert hass.states.get(SWING_OFF_EID) is not None
with patch(
"homeassistant.components.switcher_kis.climate.SwitcherType2Api.control_breeze_device",
) as mock_control_device:
await hass.services.async_call(
BUTTON_DOMAIN,
SERVICE_PRESS,
{ATTR_ENTITY_ID: SWING_ON_EID},
blocking=True,
)
assert mock_api.call_count == 2
mock_control_device.assert_called_once_with(ANY, swing=ThermostatSwing.ON)
mock_control_device.reset_mock()
await hass.services.async_call(
BUTTON_DOMAIN,
SERVICE_PRESS,
{ATTR_ENTITY_ID: SWING_OFF_EID},
blocking=True,
)
assert mock_api.call_count == 4
mock_control_device.assert_called_once_with(ANY, swing=ThermostatSwing.OFF)
@pytest.mark.parametrize("mock_bridge", [[DEVICE]], indirect=True)
async def test_control_device_fail(hass, mock_bridge, mock_api, monkeypatch):
"""Test control device fail."""
await init_integration(hass)
assert mock_bridge
assert hass.states.get(ASSUME_ON_EID) is not None
# Test exception during set hvac mode
with patch(
"homeassistant.components.switcher_kis.climate.SwitcherType2Api.control_breeze_device",
side_effect=RuntimeError("fake error"),
) as mock_control_device:
with pytest.raises(HomeAssistantError):
await hass.services.async_call(
BUTTON_DOMAIN,
SERVICE_PRESS,
{ATTR_ENTITY_ID: ASSUME_ON_EID},
blocking=True,
)
assert mock_api.call_count == 2
mock_control_device.assert_called_once_with(
ANY, state=DeviceState.ON, update_state=True
)
state = hass.states.get(ASSUME_ON_EID)
assert state.state == STATE_UNAVAILABLE
# Make device available again
mock_bridge.mock_callbacks([DEVICE])
await hass.async_block_till_done()
assert hass.states.get(ASSUME_ON_EID) is not None
# Test error response during turn on
with patch(
"homeassistant.components.switcher_kis.climate.SwitcherType2Api.control_breeze_device",
return_value=SwitcherBaseResponse(None),
) as mock_control_device:
with pytest.raises(HomeAssistantError):
await hass.services.async_call(
BUTTON_DOMAIN,
SERVICE_PRESS,
{ATTR_ENTITY_ID: ASSUME_ON_EID},
blocking=True,
)
assert mock_api.call_count == 4
mock_control_device.assert_called_once_with(
ANY, state=DeviceState.ON, update_state=True
)
state = hass.states.get(ASSUME_ON_EID)
assert state.state == STATE_UNAVAILABLE