Add switch to flexit bacnet integration (#108866)

* Add platform switch to flecit_bacnet integration

* Move testing of the switch to it’s own test

* Assert correct method is called one time

* Test switch on/off error recovery

* Review comment
This commit is contained in:
Jonas Fors Lellky 2024-01-25 20:59:36 +01:00 committed by GitHub
parent 12289f172d
commit 2b799830db
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 303 additions and 1 deletions

View File

@ -8,7 +8,12 @@ from homeassistant.core import HomeAssistant
from .const import DOMAIN
from .coordinator import FlexitCoordinator
PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.CLIMATE, Platform.SENSOR]
PLATFORMS: list[Platform] = [
Platform.BINARY_SENSOR,
Platform.CLIMATE,
Platform.SENSOR,
Platform.SWITCH,
]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:

View File

@ -68,6 +68,11 @@
"heat_exchanger_speed": {
"name": "Heat exchanger speed"
}
},
"switch": {
"electric_heater": {
"name": "Electric heater"
}
}
}
}

View File

@ -0,0 +1,96 @@
"""The Flexit Nordic (BACnet) integration."""
import asyncio.exceptions
from collections.abc import Callable
from dataclasses import dataclass
from typing import Any
from flexit_bacnet import FlexitBACnet
from flexit_bacnet.bacnet import DecodingError
from homeassistant.components.switch import (
SwitchDeviceClass,
SwitchEntity,
SwitchEntityDescription,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import FlexitCoordinator
from .const import DOMAIN
from .entity import FlexitEntity
@dataclass(kw_only=True, frozen=True)
class FlexitSwitchEntityDescription(SwitchEntityDescription):
"""Describes a Flexit switch entity."""
is_on_fn: Callable[[FlexitBACnet], bool]
SWITCHES: tuple[FlexitSwitchEntityDescription, ...] = (
FlexitSwitchEntityDescription(
key="electric_heater",
translation_key="electric_heater",
icon="mdi:radiator",
is_on_fn=lambda data: data.electric_heater,
),
)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up Flexit (bacnet) switch from a config entry."""
coordinator: FlexitCoordinator = hass.data[DOMAIN][config_entry.entry_id]
async_add_entities(
FlexitSwitch(coordinator, description) for description in SWITCHES
)
class FlexitSwitch(FlexitEntity, SwitchEntity):
"""Representation of a Flexit Switch."""
_attr_device_class = SwitchDeviceClass.SWITCH
entity_description: FlexitSwitchEntityDescription
def __init__(
self,
coordinator: FlexitCoordinator,
entity_description: FlexitSwitchEntityDescription,
) -> None:
"""Initialize Flexit (bacnet) switch."""
super().__init__(coordinator)
self.entity_description = entity_description
self._attr_unique_id = (
f"{coordinator.device.serial_number}-{entity_description.key}"
)
@property
def is_on(self) -> bool:
"""Return value of the switch."""
return self.entity_description.is_on_fn(self.coordinator.data)
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn electric heater on."""
try:
await self.device.enable_electric_heater()
except (asyncio.exceptions.TimeoutError, ConnectionError, DecodingError) as exc:
raise HomeAssistantError from exc
finally:
await self.coordinator.async_refresh()
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn electric heater off."""
try:
await self.device.disable_electric_heater()
except (asyncio.exceptions.TimeoutError, ConnectionError, DecodingError) as exc:
raise HomeAssistantError from exc
finally:
await self.coordinator.async_refresh()

View File

@ -60,6 +60,7 @@ def mock_flexit_bacnet() -> Generator[AsyncMock, None, None]:
flexit_bacnet.heat_exchanger_efficiency = 81
flexit_bacnet.heat_exchanger_speed = 100
flexit_bacnet.air_filter_polluted = False
flexit_bacnet.electric_heater = True
yield flexit_bacnet

View File

@ -0,0 +1,60 @@
# serializer version: 1
# name: test_switches[switch.device_name_electric_heater-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'switch',
'entity_category': None,
'entity_id': 'switch.device_name_electric_heater',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'name': None,
'options': dict({
}),
'original_device_class': <SwitchDeviceClass.SWITCH: 'switch'>,
'original_icon': 'mdi:radiator',
'original_name': 'Electric heater',
'platform': 'flexit_bacnet',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'electric_heater',
'unique_id': '0000-0001-electric_heater',
'unit_of_measurement': None,
})
# ---
# name: test_switches[switch.device_name_electric_heater-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'switch',
'friendly_name': 'Device Name Electric heater',
'icon': 'mdi:radiator',
}),
'context': <ANY>,
'entity_id': 'switch.device_name_electric_heater',
'last_changed': <ANY>,
'last_updated': <ANY>,
'state': 'on',
})
# ---
# name: test_switches_implementation[switch.device_name_electric_heater-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'switch',
'friendly_name': 'Device Name Electric heater',
'icon': 'mdi:radiator',
}),
'context': <ANY>,
'entity_id': 'switch.device_name_electric_heater',
'last_changed': <ANY>,
'last_updated': <ANY>,
'state': 'on',
})
# ---

View File

@ -0,0 +1,135 @@
"""Tests for the Flexit Nordic (BACnet) switch entities."""
from unittest.mock import AsyncMock
from flexit_bacnet import DecodingError
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, STATE_OFF, STATE_ON, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_registry as er
from tests.common import MockConfigEntry
from tests.components.flexit_bacnet import setup_with_selected_platforms
ENTITY_ID = "switch.device_name_electric_heater"
async def test_switches(
hass: HomeAssistant,
snapshot: SnapshotAssertion,
entity_registry: er.EntityRegistry,
mock_flexit_bacnet: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test switch states are correctly collected from library."""
await setup_with_selected_platforms(hass, mock_config_entry, [Platform.SWITCH])
entity_entries = er.async_entries_for_config_entry(
entity_registry, mock_config_entry.entry_id
)
assert entity_entries
for entity_entry in entity_entries:
assert entity_entry == snapshot(name=f"{entity_entry.entity_id}-entry")
assert (state := hass.states.get(entity_entry.entity_id))
assert state == snapshot(name=f"{entity_entry.entity_id}-state")
async def test_switches_implementation(
hass: HomeAssistant,
snapshot: SnapshotAssertion,
entity_registry: er.EntityRegistry,
mock_flexit_bacnet: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test that the switch can be turned on and off."""
await setup_with_selected_platforms(hass, mock_config_entry, [Platform.SWITCH])
assert hass.states.get(ENTITY_ID) == snapshot(name=f"{ENTITY_ID}-state")
# Set to off
mock_flexit_bacnet.electric_heater = False
await hass.services.async_call(
SWITCH_DOMAIN,
SERVICE_TURN_OFF,
{ATTR_ENTITY_ID: ENTITY_ID},
blocking=True,
)
mocked_method = getattr(mock_flexit_bacnet, "disable_electric_heater")
assert len(mocked_method.mock_calls) == 1
assert hass.states.get(ENTITY_ID).state == STATE_OFF
# Set to on
mock_flexit_bacnet.electric_heater = True
await hass.services.async_call(
SWITCH_DOMAIN,
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: ENTITY_ID},
blocking=True,
)
mocked_method = getattr(mock_flexit_bacnet, "enable_electric_heater")
assert len(mocked_method.mock_calls) == 1
assert hass.states.get(ENTITY_ID).state == STATE_ON
# Error recovery, when turning off
mock_flexit_bacnet.disable_electric_heater.side_effect = DecodingError
with pytest.raises(HomeAssistantError):
await hass.services.async_call(
SWITCH_DOMAIN,
SERVICE_TURN_OFF,
{ATTR_ENTITY_ID: ENTITY_ID},
blocking=True,
)
mocked_method = getattr(mock_flexit_bacnet, "disable_electric_heater")
assert len(mocked_method.mock_calls) == 2
mock_flexit_bacnet.disable_electric_heater.side_effect = None
mock_flexit_bacnet.electric_heater = False
await hass.services.async_call(
SWITCH_DOMAIN,
SERVICE_TURN_OFF,
{ATTR_ENTITY_ID: ENTITY_ID},
blocking=True,
)
assert hass.states.get(ENTITY_ID).state == STATE_OFF
# Error recovery, when turning on
mock_flexit_bacnet.enable_electric_heater.side_effect = DecodingError
with pytest.raises(HomeAssistantError):
await hass.services.async_call(
SWITCH_DOMAIN,
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: ENTITY_ID},
blocking=True,
)
mocked_method = getattr(mock_flexit_bacnet, "enable_electric_heater")
assert len(mocked_method.mock_calls) == 2
mock_flexit_bacnet.enable_electric_heater.side_effect = None
mock_flexit_bacnet.electric_heater = True
await hass.services.async_call(
SWITCH_DOMAIN,
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: ENTITY_ID},
blocking=True,
)
assert hass.states.get(ENTITY_ID).state == STATE_ON