Skip to content

Commit

Permalink
Add switch platform to opentherm_gw (#125410)
Browse files Browse the repository at this point in the history
* WIP

* * Add switch platform
* Add tests for switches

* Remove unnecessary block_till_done-s

* Test that entities get added in a disabled state separately

* Convert to parametrized test

* Use fixture to add entities enabled.
  • Loading branch information
mvn23 authored Sep 6, 2024
1 parent cd3059a commit b9bd8f6
Show file tree
Hide file tree
Showing 4 changed files with 202 additions and 1 deletion.
8 changes: 7 additions & 1 deletion homeassistant/components/opentherm_gw/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,13 @@
extra=vol.ALLOW_EXTRA,
)

PLATFORMS = [Platform.BINARY_SENSOR, Platform.BUTTON, Platform.CLIMATE, Platform.SENSOR]
PLATFORMS = [
Platform.BINARY_SENSOR,
Platform.BUTTON,
Platform.CLIMATE,
Platform.SENSOR,
Platform.SWITCH,
]


async def options_updated(hass: HomeAssistant, entry: ConfigEntry) -> None:
Expand Down
5 changes: 5 additions & 0 deletions homeassistant/components/opentherm_gw/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,11 @@
"outside_temperature": {
"name": "Outside temperature"
}
},
"switch": {
"central_heating_override_n": {
"name": "Force central heating {circuit_number} on"
}
}
},
"options": {
Expand Down
79 changes: 79 additions & 0 deletions homeassistant/components/opentherm_gw/switch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
"""Support for OpenTherm Gateway switches."""

from collections.abc import Awaitable, Callable
from dataclasses import dataclass
from typing import Any

from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_ID, EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback

from . import OpenThermGatewayHub
from .const import DATA_GATEWAYS, DATA_OPENTHERM_GW, GATEWAY_DEVICE_DESCRIPTION
from .entity import OpenThermEntity, OpenThermEntityDescription


@dataclass(frozen=True, kw_only=True)
class OpenThermSwitchEntityDescription(
OpenThermEntityDescription, SwitchEntityDescription
):
"""Describes opentherm_gw switch entity."""

turn_off_action: Callable[[OpenThermGatewayHub], Awaitable[int | None]]
turn_on_action: Callable[[OpenThermGatewayHub], Awaitable[int | None]]


SWITCH_DESCRIPTIONS: tuple[OpenThermSwitchEntityDescription, ...] = (
OpenThermSwitchEntityDescription(
key="central_heating_1_override",
translation_key="central_heating_override_n",
translation_placeholders={"circuit_number": "1"},
device_description=GATEWAY_DEVICE_DESCRIPTION,
turn_off_action=lambda hub: hub.gateway.set_ch_enable_bit(0),
turn_on_action=lambda hub: hub.gateway.set_ch_enable_bit(1),
),
OpenThermSwitchEntityDescription(
key="central_heating_2_override",
translation_key="central_heating_override_n",
translation_placeholders={"circuit_number": "2"},
device_description=GATEWAY_DEVICE_DESCRIPTION,
turn_off_action=lambda hub: hub.gateway.set_ch2_enable_bit(0),
turn_on_action=lambda hub: hub.gateway.set_ch2_enable_bit(1),
),
)


async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the OpenTherm Gateway switches."""
gw_hub = hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][config_entry.data[CONF_ID]]

async_add_entities(
OpenThermSwitch(gw_hub, description) for description in SWITCH_DESCRIPTIONS
)


class OpenThermSwitch(OpenThermEntity, SwitchEntity):
"""Represent an OpenTherm Gateway switch."""

_attr_assumed_state = True
_attr_entity_category = EntityCategory.CONFIG
_attr_entity_registry_enabled_default = False
entity_description: OpenThermSwitchEntityDescription

async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn switch on."""
value = await self.entity_description.turn_off_action(self._gateway)
self._attr_is_on = bool(value) if value is not None else None
self.async_write_ha_state()

async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn switch on."""
value = await self.entity_description.turn_on_action(self._gateway)
self._attr_is_on = bool(value) if value is not None else None
self.async_write_ha_state()
111 changes: 111 additions & 0 deletions tests/components/opentherm_gw/test_switch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
"""Test opentherm_gw switches."""

from unittest.mock import AsyncMock, MagicMock, call

import pytest

from homeassistant.components.opentherm_gw import DOMAIN as OPENTHERM_DOMAIN
from homeassistant.components.opentherm_gw.const import OpenThermDeviceIdentifier
from homeassistant.components.switch import (
DOMAIN as SWITCH_DOMAIN,
SERVICE_TURN_OFF,
SERVICE_TURN_ON,
)
from homeassistant.const import (
ATTR_ENTITY_ID,
CONF_ID,
STATE_OFF,
STATE_ON,
STATE_UNKNOWN,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er

from tests.common import MockConfigEntry


@pytest.mark.parametrize(
"entity_key", ["central_heating_1_override", "central_heating_2_override"]
)
async def test_switch_added_disabled(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
mock_config_entry: MockConfigEntry,
mock_pyotgw: MagicMock,
entity_key: str,
) -> None:
"""Test switch gets added in disabled state."""

mock_config_entry.add_to_hass(hass)

await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()

assert (
switch_entity_id := entity_registry.async_get_entity_id(
SWITCH_DOMAIN,
OPENTHERM_DOMAIN,
f"{mock_config_entry.data[CONF_ID]}-{OpenThermDeviceIdentifier.GATEWAY}-{entity_key}",
)
) is not None

assert (entity_entry := entity_registry.async_get(switch_entity_id)) is not None
assert entity_entry.disabled_by == er.RegistryEntryDisabler.INTEGRATION


@pytest.mark.usefixtures("entity_registry_enabled_by_default")
@pytest.mark.parametrize(
("entity_key", "target_func"),
[
("central_heating_1_override", "set_ch_enable_bit"),
("central_heating_2_override", "set_ch2_enable_bit"),
],
)
async def test_ch_override_switch(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
mock_config_entry: MockConfigEntry,
mock_pyotgw: MagicMock,
entity_key: str,
target_func: str,
) -> None:
"""Test central heating override switch."""

setattr(mock_pyotgw.return_value, target_func, AsyncMock(side_effect=[0, 1]))
mock_config_entry.add_to_hass(hass)

await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()

assert (
switch_entity_id := entity_registry.async_get_entity_id(
SWITCH_DOMAIN,
OPENTHERM_DOMAIN,
f"{mock_config_entry.data[CONF_ID]}-{OpenThermDeviceIdentifier.GATEWAY}-{entity_key}",
)
) is not None
assert hass.states.get(switch_entity_id).state == STATE_UNKNOWN

await hass.services.async_call(
SWITCH_DOMAIN,
SERVICE_TURN_OFF,
{
ATTR_ENTITY_ID: switch_entity_id,
},
blocking=True,
)
assert hass.states.get(switch_entity_id).state == STATE_OFF

await hass.services.async_call(
SWITCH_DOMAIN,
SERVICE_TURN_ON,
{
ATTR_ENTITY_ID: switch_entity_id,
},
blocking=True,
)
assert hass.states.get(switch_entity_id).state == STATE_ON

mock_func = getattr(mock_pyotgw.return_value, target_func)
assert mock_func.await_count == 2
mock_func.assert_has_awaits([call(0), call(1)])

0 comments on commit b9bd8f6

Please sign in to comment.