1
0
mirror of https://github.com/home-assistant/core.git synced 2025-12-20 02:48:57 +00:00

Actron Air: Add switch entity platform (#158087)

Co-authored-by: Joostlek <joostlek@outlook.com>
This commit is contained in:
Kurt Chrisford
2025-12-19 04:11:39 +10:00
committed by GitHub
parent 62464b83dc
commit 81be14c8f1
9 changed files with 504 additions and 10 deletions

View File

@@ -18,7 +18,7 @@ from .coordinator import (
ActronAirSystemCoordinator, ActronAirSystemCoordinator,
) )
PLATFORM = [Platform.CLIMATE] PLATFORMS = [Platform.CLIMATE, Platform.SWITCH]
async def async_setup_entry(hass: HomeAssistant, entry: ActronAirConfigEntry) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ActronAirConfigEntry) -> bool:
@@ -50,10 +50,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ActronAirConfigEntry) ->
system_coordinators=system_coordinators, system_coordinators=system_coordinators,
) )
await hass.config_entries.async_forward_entry_setups(entry, PLATFORM) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True return True
async def async_unload_entry(hass: HomeAssistant, entry: ActronAirConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ActronAirConfigEntry) -> bool:
"""Unload a config entry.""" """Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORM) return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)

View File

@@ -0,0 +1,30 @@
{
"entity": {
"switch": {
"away_mode": {
"default": "mdi:home-export-outline",
"state": {
"off": "mdi:home-import-outline"
}
},
"continuous_fan": {
"default": "mdi:fan",
"state": {
"off": "mdi:fan-off"
}
},
"quiet_mode": {
"default": "mdi:volume-low",
"state": {
"off": "mdi:volume-high"
}
},
"turbo_mode": {
"default": "mdi:fan-plus",
"state": {
"off": "mdi:fan"
}
}
}
}
}

View File

@@ -32,6 +32,22 @@
} }
} }
}, },
"entity": {
"switch": {
"away_mode": {
"name": "Away mode"
},
"continuous_fan": {
"name": "Continuous fan"
},
"quiet_mode": {
"name": "Quiet mode"
},
"turbo_mode": {
"name": "Turbo mode"
}
}
},
"exceptions": { "exceptions": {
"auth_error": { "auth_error": {
"message": "Authentication failed, please reauthenticate" "message": "Authentication failed, please reauthenticate"

View File

@@ -0,0 +1,110 @@
"""Switch platform for Actron Air integration."""
from collections.abc import Awaitable, Callable
from dataclasses import dataclass
from typing import Any
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN
from .coordinator import ActronAirConfigEntry, ActronAirSystemCoordinator
PARALLEL_UPDATES = 0
@dataclass(frozen=True, kw_only=True)
class ActronAirSwitchEntityDescription(SwitchEntityDescription):
"""Class describing Actron Air switch entities."""
is_on_fn: Callable[[ActronAirSystemCoordinator], bool]
set_fn: Callable[[ActronAirSystemCoordinator, bool], Awaitable[None]]
is_supported_fn: Callable[[ActronAirSystemCoordinator], bool] = lambda _: True
SWITCHES: tuple[ActronAirSwitchEntityDescription, ...] = (
ActronAirSwitchEntityDescription(
key="away_mode",
translation_key="away_mode",
is_on_fn=lambda coordinator: coordinator.data.user_aircon_settings.away_mode,
set_fn=lambda coordinator,
enabled: coordinator.data.user_aircon_settings.set_away_mode(enabled),
),
ActronAirSwitchEntityDescription(
key="continuous_fan",
translation_key="continuous_fan",
is_on_fn=lambda coordinator: coordinator.data.user_aircon_settings.continuous_fan_enabled,
set_fn=lambda coordinator,
enabled: coordinator.data.user_aircon_settings.set_continuous_mode(enabled),
),
ActronAirSwitchEntityDescription(
key="quiet_mode",
translation_key="quiet_mode",
is_on_fn=lambda coordinator: coordinator.data.user_aircon_settings.quiet_mode_enabled,
set_fn=lambda coordinator,
enabled: coordinator.data.user_aircon_settings.set_quiet_mode(enabled),
),
ActronAirSwitchEntityDescription(
key="turbo_mode",
translation_key="turbo_mode",
is_on_fn=lambda coordinator: coordinator.data.user_aircon_settings.turbo_enabled,
set_fn=lambda coordinator,
enabled: coordinator.data.user_aircon_settings.set_turbo_mode(enabled),
is_supported_fn=lambda coordinator: coordinator.data.user_aircon_settings.turbo_supported,
),
)
async def async_setup_entry(
hass: HomeAssistant,
entry: ActronAirConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Actron Air switch entities."""
system_coordinators = entry.runtime_data.system_coordinators
async_add_entities(
ActronAirSwitch(coordinator, description)
for coordinator in system_coordinators.values()
for description in SWITCHES
if description.is_supported_fn(coordinator)
)
class ActronAirSwitch(CoordinatorEntity[ActronAirSystemCoordinator], SwitchEntity):
"""Actron Air switch."""
_attr_has_entity_name = True
_attr_entity_category = EntityCategory.CONFIG
entity_description: ActronAirSwitchEntityDescription
def __init__(
self,
coordinator: ActronAirSystemCoordinator,
description: ActronAirSwitchEntityDescription,
) -> None:
"""Initialize the switch."""
super().__init__(coordinator)
self.entity_description = description
self._attr_unique_id = f"{coordinator.serial_number}_{description.key}"
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, coordinator.serial_number)},
manufacturer="Actron Air",
name=coordinator.data.ac_system.system_name,
)
@property
def is_on(self) -> bool:
"""Return true if the switch is on."""
return self.entity_description.is_on_fn(self.coordinator)
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the switch on."""
await self.entity_description.set_fn(self.coordinator, True)
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the switch off."""
await self.entity_description.set_fn(self.coordinator, False)

View File

@@ -1 +1,13 @@
"""Tests for the Actron Air integration.""" """Tests for the Actron Air 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

@@ -2,7 +2,7 @@
import asyncio import asyncio
from collections.abc import Generator from collections.abc import Generator
from unittest.mock import AsyncMock, patch from unittest.mock import AsyncMock, MagicMock, patch
import pytest import pytest
@@ -53,9 +53,40 @@ def mock_actron_api() -> Generator[AsyncMock]:
# Mock refresh token property # Mock refresh token property
api.refresh_token_value = "test_refresh_token" api.refresh_token_value = "test_refresh_token"
# Mock other API methods that might be used # Mock get_ac_systems
api.get_systems = AsyncMock(return_value=[]) api.get_ac_systems = AsyncMock(
api.get_status = AsyncMock(return_value=None) return_value=[{"serial": "123456", "name": "Test System"}]
)
# Mock state manager
api.state_manager = MagicMock()
status = api.state_manager.get_status.return_value
status.master_info.live_temp_c = 22.0
status.ac_system.system_name = "Test System"
status.ac_system.serial_number = "123456"
status.ac_system.master_wc_model = "Test Model"
status.ac_system.master_wc_firmware_version = "1.0.0"
status.remote_zone_info = []
status.min_temp = 16
status.max_temp = 30
status.aircon_system.mode = "OFF"
status.fan_mode = "LOW"
status.set_point = 24
status.room_temp = 25
status.is_on = False
# Mock user_aircon_settings for the switch platform
settings = status.user_aircon_settings
settings.away_mode = False
settings.continuous_fan_enabled = False
settings.quiet_mode_enabled = False
settings.turbo_enabled = False
settings.turbo_supported = True
settings.set_away_mode = AsyncMock()
settings.set_continuous_mode = AsyncMock()
settings.set_quiet_mode = AsyncMock()
settings.set_turbo_mode = AsyncMock()
yield api yield api
@@ -69,3 +100,12 @@ def mock_config_entry() -> MockConfigEntry:
data={CONF_API_TOKEN: "test_refresh_token"}, data={CONF_API_TOKEN: "test_refresh_token"},
unique_id="test_user_id", unique_id="test_user_id",
) )
@pytest.fixture
def mock_setup_entry() -> Generator[AsyncMock]:
"""Mock async_setup_entry."""
with patch(
"homeassistant.components.actron_air.async_setup_entry", return_value=True
) as mock_setup:
yield mock_setup

View File

@@ -0,0 +1,193 @@
# serializer version: 1
# name: test_switch_entities[switch.test_system_away_mode-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'switch',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'switch.test_system_away_mode',
'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': 'Away mode',
'platform': 'actron_air',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'away_mode',
'unique_id': '123456_away_mode',
'unit_of_measurement': None,
})
# ---
# name: test_switch_entities[switch.test_system_away_mode-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Test System Away mode',
}),
'context': <ANY>,
'entity_id': 'switch.test_system_away_mode',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_switch_entities[switch.test_system_continuous_fan-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'switch',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'switch.test_system_continuous_fan',
'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': 'Continuous fan',
'platform': 'actron_air',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'continuous_fan',
'unique_id': '123456_continuous_fan',
'unit_of_measurement': None,
})
# ---
# name: test_switch_entities[switch.test_system_continuous_fan-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Test System Continuous fan',
}),
'context': <ANY>,
'entity_id': 'switch.test_system_continuous_fan',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_switch_entities[switch.test_system_quiet_mode-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'switch',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'switch.test_system_quiet_mode',
'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': 'Quiet mode',
'platform': 'actron_air',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'quiet_mode',
'unique_id': '123456_quiet_mode',
'unit_of_measurement': None,
})
# ---
# name: test_switch_entities[switch.test_system_quiet_mode-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Test System Quiet mode',
}),
'context': <ANY>,
'entity_id': 'switch.test_system_quiet_mode',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_switch_entities[switch.test_system_turbo_mode-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'switch',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'switch.test_system_turbo_mode',
'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': 'Turbo mode',
'platform': 'actron_air',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'turbo_mode',
'unique_id': '123456_turbo_mode',
'unit_of_measurement': None,
})
# ---
# name: test_switch_entities[switch.test_system_turbo_mode-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Test System Turbo mode',
}),
'context': <ANY>,
'entity_id': 'switch.test_system_turbo_mode',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---

View File

@@ -15,7 +15,7 @@ from tests.common import MockConfigEntry
async def test_user_flow_oauth2_success( async def test_user_flow_oauth2_success(
hass: HomeAssistant, mock_actron_api: AsyncMock hass: HomeAssistant, mock_actron_api: AsyncMock, mock_setup_entry: AsyncMock
) -> None: ) -> None:
"""Test successful OAuth2 device code flow.""" """Test successful OAuth2 device code flow."""
# Start the config flow # Start the config flow
@@ -90,7 +90,7 @@ async def test_user_flow_oauth2_error(hass: HomeAssistant, mock_actron_api) -> N
async def test_user_flow_token_polling_error( async def test_user_flow_token_polling_error(
hass: HomeAssistant, mock_actron_api hass: HomeAssistant, mock_actron_api, mock_setup_entry: AsyncMock
) -> None: ) -> None:
"""Test OAuth2 flow with error during token polling.""" """Test OAuth2 flow with error during token polling."""
# Override the default mock to raise an error during token polling # Override the default mock to raise an error during token polling
@@ -179,7 +179,10 @@ async def test_user_flow_duplicate_account(
async def test_reauth_flow_success( async def test_reauth_flow_success(
hass: HomeAssistant, mock_actron_api: AsyncMock, mock_config_entry: MockConfigEntry hass: HomeAssistant,
mock_actron_api: AsyncMock,
mock_config_entry: MockConfigEntry,
mock_setup_entry: AsyncMock,
) -> None: ) -> None:
"""Test successful reauthentication flow.""" """Test successful reauthentication flow."""
# Create an existing config entry # Create an existing config entry

View File

@@ -0,0 +1,90 @@
"""Tests for the Actron Air switch platform."""
from unittest.mock import MagicMock, patch
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.switch import (
DOMAIN as SWITCH_DOMAIN,
SERVICE_TURN_OFF,
SERVICE_TURN_ON,
)
from homeassistant.const import ATTR_ENTITY_ID, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from . import setup_integration
from tests.common import MockConfigEntry, snapshot_platform
async def test_switch_entities(
hass: HomeAssistant,
mock_actron_api: MagicMock,
entity_registry: er.EntityRegistry,
snapshot: SnapshotAssertion,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test switch entities."""
with patch("homeassistant.components.actron_air.PLATFORMS", [Platform.SWITCH]):
await setup_integration(hass, mock_config_entry)
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
@pytest.mark.parametrize(
("entity_id", "method"),
[
("switch.test_system_away_mode", "set_away_mode"),
("switch.test_system_continuous_fan", "set_continuous_mode"),
("switch.test_system_quiet_mode", "set_quiet_mode"),
("switch.test_system_turbo_mode", "set_turbo_mode"),
],
)
async def test_switch_toggles(
hass: HomeAssistant,
mock_actron_api: MagicMock,
mock_config_entry: MockConfigEntry,
entity_id: str,
method: str,
) -> None:
"""Test switch toggles."""
with patch("homeassistant.components.actron_air.PLATFORMS", [Platform.SWITCH]):
await setup_integration(hass, mock_config_entry)
status = mock_actron_api.state_manager.get_status.return_value
mock_method = getattr(status.user_aircon_settings, method)
await hass.services.async_call(
SWITCH_DOMAIN,
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: [entity_id]},
blocking=True,
)
mock_method.assert_awaited_once_with(True)
mock_method.reset_mock()
await hass.services.async_call(
SWITCH_DOMAIN,
SERVICE_TURN_OFF,
{ATTR_ENTITY_ID: [entity_id]},
blocking=True,
)
mock_method.assert_awaited_once_with(False)
async def test_turbo_mode_not_supported(
hass: HomeAssistant,
mock_actron_api: MagicMock,
mock_config_entry: MockConfigEntry,
entity_registry: er.EntityRegistry,
) -> None:
"""Test turbo mode switch is not created when not supported."""
status = mock_actron_api.state_manager.get_status.return_value
status.user_aircon_settings.turbo_supported = False
await setup_integration(hass, mock_config_entry)
entity_id = "switch.test_system_turbo_mode"
assert not hass.states.get(entity_id)
assert not entity_registry.async_get(entity_id)