Compare commits

...

23 Commits

Author SHA1 Message Date
Joostlek
f6f732b6a7 Fix tests 2025-05-22 20:29:17 +02:00
Robin
6e74df7d8c Bump pysmarlaapi version 2025-05-22 16:20:57 +02:00
Robin Lintermann
3ad45d4a30 Implemented full test coverage 2025-05-22 16:20:57 +02:00
Robin Lintermann
00ccaf1ff9 Fix leaking tests 2025-05-22 16:20:57 +02:00
Robin Lintermann
acc32eea3e Refactor tests 2025-05-22 16:20:57 +02:00
Robin Lintermann
f0a3ecd27b Patch both connection objects 2025-05-22 16:20:57 +02:00
Robin Lintermann
6bbd357547 Refactoring 2025-05-22 16:20:57 +02:00
Robin Lintermann
f14874bc3c Use object equality operator when applicable 2025-05-22 16:20:57 +02:00
Robin Lintermann
73cfcc285b Bump pysmarlaapi version 2025-05-22 16:20:57 +02:00
Robin Lintermann
d5e4ed2b9e Major rework of tests and refactoring 2025-05-22 16:20:57 +02:00
Robin Lintermann
8893f97f95 Update quality_scale 2025-05-22 16:20:57 +02:00
Robin Lintermann
2bdcdaf446 Fix tests for smarla config_flow 2025-05-22 16:20:57 +02:00
Robin Lintermann
73d37972df Refactoring and changed access token format 2025-05-22 16:20:57 +02:00
Robin Lintermann
b456af473b Bump pysmarlaapi version 2025-05-22 16:20:57 +02:00
Robin Lintermann
2fa18c6838 Code refactoring and clean up 2025-05-22 16:20:57 +02:00
Robin Lintermann
2a96ea0418 Removed obsolete reload function 2025-05-22 16:20:57 +02:00
Robin Lintermann
7b2641d136 Code refactoring 2025-05-22 16:20:57 +02:00
Robin Lintermann
baeb70af8a Change default name of device 2025-05-22 16:20:57 +02:00
Robin Lintermann
00cd8f70b9 Bump pysmarlaapi version 2025-05-22 16:20:57 +02:00
Robin Lintermann
b6d51504c4 Focus on switch platform 2025-05-22 16:20:57 +02:00
Robin Lintermann
2a2cef349d Bump pysmarlaapi version and reevaluate quality scale 2025-05-22 16:20:57 +02:00
Robin Lintermann
7adc1e7e47 Apply suggested changes 2025-05-22 16:20:57 +02:00
Robin Lintermann
d19aabfcd0 Added smarla integration 2025-05-22 16:20:57 +02:00
21 changed files with 782 additions and 0 deletions

2
CODEOWNERS generated
View File

@@ -1417,6 +1417,8 @@ build.json @home-assistant/supervisor
/tests/components/sma/ @kellerza @rklomp @erwindouna
/homeassistant/components/smappee/ @bsmappee
/tests/components/smappee/ @bsmappee
/homeassistant/components/smarla/ @explicatis @rlint-explicatis
/tests/components/smarla/ @explicatis @rlint-explicatis
/homeassistant/components/smart_meter_texas/ @grahamwetzler
/tests/components/smart_meter_texas/ @grahamwetzler
/homeassistant/components/smartthings/ @joostlek

View File

@@ -0,0 +1,39 @@
"""The Swing2Sleep Smarla integration."""
from pysmarlaapi import Connection, Federwiege
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_ACCESS_TOKEN
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed
from .const import HOST, PLATFORMS
type FederwiegeConfigEntry = ConfigEntry[Federwiege]
async def async_setup_entry(hass: HomeAssistant, entry: FederwiegeConfigEntry) -> bool:
"""Set up this integration using UI."""
connection = Connection(HOST, token_b64=entry.data[CONF_ACCESS_TOKEN])
# Check if token still has access
if not await connection.refresh_token():
raise ConfigEntryAuthFailed("Invalid authentication")
federwiege = Federwiege(hass.loop, connection)
federwiege.register()
federwiege.connect()
entry.runtime_data = federwiege
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: FederwiegeConfigEntry) -> bool:
"""Unload a config entry."""
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
entry.runtime_data.disconnect()
return unload_ok

View File

@@ -0,0 +1,62 @@
"""Config flow for Swing2Sleep Smarla integration."""
from __future__ import annotations
from typing import Any
from pysmarlaapi import Connection
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_ACCESS_TOKEN
from .const import DOMAIN, HOST
STEP_USER_DATA_SCHEMA = vol.Schema({CONF_ACCESS_TOKEN: str})
class SmarlaConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Swing2Sleep Smarla."""
VERSION = 1
async def _handle_token(self, token: str) -> tuple[dict[str, str], str | None]:
"""Handle the token input."""
errors: dict[str, str] = {}
try:
conn = Connection(url=HOST, token_b64=token)
except ValueError:
errors["base"] = "malformed_token"
return errors, None
if not await conn.refresh_token():
errors["base"] = "invalid_auth"
return errors, None
return errors, conn.token.serialNumber
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the initial step."""
errors: dict[str, str] = {}
if user_input is not None:
raw_token = user_input[CONF_ACCESS_TOKEN]
errors, serial_number = await self._handle_token(token=raw_token)
if not errors and serial_number is not None:
await self.async_set_unique_id(serial_number)
self._abort_if_unique_id_configured()
return self.async_create_entry(
title=serial_number,
data={CONF_ACCESS_TOKEN: raw_token},
)
return self.async_show_form(
step_id="user",
data_schema=STEP_USER_DATA_SCHEMA,
errors=errors,
)

View File

@@ -0,0 +1,12 @@
"""Constants for the Swing2Sleep Smarla integration."""
from homeassistant.const import Platform
DOMAIN = "smarla"
HOST = "https://devices.swing2sleep.de"
PLATFORMS = [Platform.SWITCH]
DEVICE_MODEL_NAME = "Smarla"
MANUFACTURER_NAME = "Swing2Sleep"

View File

@@ -0,0 +1,41 @@
"""Common base for entities."""
from typing import Any
from pysmarlaapi import Federwiege
from pysmarlaapi.federwiege.classes import Property
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity import Entity
from .const import DEVICE_MODEL_NAME, DOMAIN, MANUFACTURER_NAME
class SmarlaBaseEntity(Entity):
"""Common Base Entity class for defining Smarla device."""
_attr_should_poll = False
_attr_has_entity_name = True
def __init__(self, federwiege: Federwiege, prop: Property) -> None:
"""Initialise the entity."""
self._property = prop
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, federwiege.serial_number)},
name=DEVICE_MODEL_NAME,
model=DEVICE_MODEL_NAME,
manufacturer=MANUFACTURER_NAME,
serial_number=federwiege.serial_number,
)
async def on_change(self, value: Any):
"""Notify ha when state changes."""
self.async_write_ha_state()
async def async_added_to_hass(self) -> None:
"""Run when this Entity has been added to HA."""
await self._property.add_listener(self.on_change)
async def async_will_remove_from_hass(self) -> None:
"""Entity being removed from hass."""
await self._property.remove_listener(self.on_change)

View File

@@ -0,0 +1,9 @@
{
"entity": {
"switch": {
"smart_mode": {
"default": "mdi:refresh-auto"
}
}
}
}

View File

@@ -0,0 +1,12 @@
{
"domain": "smarla",
"name": "Swing2Sleep Smarla",
"codeowners": ["@explicatis", "@rlint-explicatis"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/smarla",
"integration_type": "device",
"iot_class": "cloud_push",
"loggers": ["pysmarlaapi", "pysignalr"],
"quality_scale": "bronze",
"requirements": ["pysmarlaapi==0.8.2"]
}

View File

@@ -0,0 +1,60 @@
rules:
# Bronze
action-setup: done
appropriate-polling: done
brands: done
common-modules: done
config-flow-test-coverage: done
config-flow: done
dependency-transparency: done
docs-actions: done
docs-high-level-description: done
docs-installation-instructions: done
docs-removal-instructions: done
entity-event-setup: done
entity-unique-id: done
has-entity-name: done
runtime-data: done
test-before-configure: done
test-before-setup: done
unique-config-entry: done
# Silver
action-exceptions: done
config-entry-unloading: done
docs-configuration-parameters: done
docs-installation-parameters: done
entity-unavailable: todo
integration-owner: done
log-when-unavailable: todo
parallel-updates: todo
reauthentication-flow: todo
test-coverage: done
# Gold
devices: done
diagnostics: todo
discovery-update-info: todo
discovery: todo
docs-data-update: todo
docs-examples: todo
docs-known-limitations: todo
docs-supported-devices: done
docs-supported-functions: done
docs-troubleshooting: todo
docs-use-cases: todo
dynamic-devices: todo
entity-category: todo
entity-device-class: done
entity-disabled-by-default: done
entity-translations: done
exception-translations: done
icon-translations: done
reconfiguration-flow: todo
repair-issues: todo
stale-devices: todo
# Platinum
async-dependency: done
inject-websession: todo
strict-typing: todo

View File

@@ -0,0 +1,28 @@
{
"config": {
"error": {
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"malformed_token": "Malformed access token"
},
"step": {
"user": {
"data": {
"access_token": "[%key:common::config_flow::data::access_token%]"
},
"data_description": {
"access_token": "The access token generated by the Swing2Sleep app."
}
}
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
}
},
"entity": {
"switch": {
"smart_mode": {
"name": "Smart Mode"
}
}
}
}

View File

@@ -0,0 +1,80 @@
"""Support for the Swing2Sleep Smarla switch entities."""
from dataclasses import dataclass
from typing import Any
from pysmarlaapi import Federwiege
from pysmarlaapi.federwiege.classes import Property
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import FederwiegeConfigEntry
from .entity import SmarlaBaseEntity
@dataclass(frozen=True, kw_only=True)
class SmarlaSwitchEntityDescription(SwitchEntityDescription):
"""Class describing Swing2Sleep Smarla switch entity."""
service: str
property: str
SWITCHES: list[SmarlaSwitchEntityDescription] = [
SmarlaSwitchEntityDescription(
key="swing_active",
name=None,
service="babywiege",
property="swing_active",
),
SmarlaSwitchEntityDescription(
key="smart_mode",
translation_key="smart_mode",
service="babywiege",
property="smart_mode",
),
]
async def async_setup_entry(
hass: HomeAssistant,
config_entry: FederwiegeConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the Smarla switches from config entry."""
federwiege = config_entry.runtime_data
async_add_entities(SmarlaSwitch(federwiege, desc) for desc in SWITCHES)
class SmarlaSwitch(SmarlaBaseEntity, SwitchEntity):
"""Representation of Smarla switch."""
entity_description: SmarlaSwitchEntityDescription
_property: Property[bool]
def __init__(
self,
federwiege: Federwiege,
desc: SmarlaSwitchEntityDescription,
) -> None:
"""Initialize a Smarla switch."""
prop = federwiege.get_property(desc.service, desc.property)
super().__init__(federwiege, prop)
self.entity_description = desc
self._attr_unique_id = f"{federwiege.serial_number}-{desc.key}"
@property
def is_on(self) -> bool:
"""Return the entity value to represent the entity state."""
return self._property.get()
def turn_on(self, **kwargs: Any) -> None:
"""Turn the switch on."""
self._property.set(True)
def turn_off(self, **kwargs: Any) -> None:
"""Turn the switch off."""
self._property.set(False)

View File

@@ -577,6 +577,7 @@ FLOWS = {
"slimproto",
"sma",
"smappee",
"smarla",
"smart_meter_texas",
"smartthings",
"smarttub",

View File

@@ -6022,6 +6022,12 @@
"config_flow": true,
"iot_class": "cloud_polling"
},
"smarla": {
"name": "Swing2Sleep Smarla",
"integration_type": "device",
"config_flow": true,
"iot_class": "cloud_push"
},
"smart_blinds": {
"name": "Smartblinds",
"integration_type": "virtual",

3
requirements_all.txt generated
View File

@@ -2334,6 +2334,9 @@ pysma==0.7.5
# homeassistant.components.smappee
pysmappee==0.2.29
# homeassistant.components.smarla
pysmarlaapi==0.8.2
# homeassistant.components.smartthings
pysmartthings==3.2.2

View File

@@ -1907,6 +1907,9 @@ pysma==0.7.5
# homeassistant.components.smappee
pysmappee==0.2.29
# homeassistant.components.smarla
pysmarlaapi==0.8.2
# homeassistant.components.smartthings
pysmartthings==3.2.2

View File

@@ -0,0 +1,22 @@
"""Tests for the Smarla integration."""
from typing import Any
from unittest.mock import AsyncMock
from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry
async def setup_integration(hass: HomeAssistant, config_entry: MockConfigEntry) -> None:
"""Set up the component."""
config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
async def update_property_listeners(mock: AsyncMock, value: Any = None) -> None:
"""Update the property listeners for the mock object."""
for call in mock.add_listener.call_args_list:
await call[0][0](value)

View File

@@ -0,0 +1,77 @@
"""Configuration for smarla tests."""
from __future__ import annotations
from collections.abc import Generator
from unittest.mock import AsyncMock, MagicMock, patch
from pysmarlaapi.classes import AuthToken
from pysmarlaapi.federwiege.classes import Property
import pytest
from homeassistant.components.smarla.const import DOMAIN
from homeassistant.config_entries import SOURCE_USER
from .const import MOCK_ACCESS_TOKEN_JSON, MOCK_SERIAL_NUMBER, MOCK_USER_INPUT
from tests.common import MockConfigEntry
@pytest.fixture
def mock_config_entry() -> MockConfigEntry:
"""Create a mock config entry."""
return MockConfigEntry(
domain=DOMAIN,
unique_id=MOCK_SERIAL_NUMBER,
source=SOURCE_USER,
data=MOCK_USER_INPUT,
)
@pytest.fixture
def mock_setup_entry() -> Generator[AsyncMock]:
"""Override async_setup_entry."""
with patch(
"homeassistant.components.smarla.async_setup_entry", return_value=True
) as mock_setup_entry:
yield mock_setup_entry
@pytest.fixture
def mock_connection() -> Generator[MagicMock]:
"""Patch Connection object."""
with (
patch(
"homeassistant.components.smarla.config_flow.Connection", autospec=True
) as mock_connection,
patch(
"homeassistant.components.smarla.Connection",
mock_connection,
),
):
connection = mock_connection.return_value
connection.token = AuthToken.from_json(MOCK_ACCESS_TOKEN_JSON)
connection.refresh_token.return_value = True
yield connection
@pytest.fixture
def mock_federwiege(
mock_connection: AsyncMock, mock_property: AsyncMock
) -> Generator[AsyncMock]:
"""Mock the Federwiege instance."""
with patch(
"homeassistant.components.smarla.Federwiege", autospec=True
) as mock_federwiege:
federwiege = mock_federwiege.return_value
federwiege.serial_number = MOCK_SERIAL_NUMBER
federwiege.get_property.return_value = mock_property
yield federwiege
@pytest.fixture
def mock_property() -> AsyncMock:
"""Mock the Federwiege instance."""
mock = AsyncMock(spec=Property)
mock.get.return_value = False
return mock

View File

@@ -0,0 +1,20 @@
"""Constants for the Smarla integration tests."""
import base64
import json
from homeassistant.const import CONF_ACCESS_TOKEN
MOCK_ACCESS_TOKEN_JSON = {
"refreshToken": "test",
"appIdentifier": "HA-test",
"serialNumber": "ABCD",
}
MOCK_SERIAL_NUMBER = MOCK_ACCESS_TOKEN_JSON["serialNumber"]
MOCK_ACCESS_TOKEN = base64.b64encode(
json.dumps(MOCK_ACCESS_TOKEN_JSON).encode()
).decode()
MOCK_USER_INPUT = {CONF_ACCESS_TOKEN: MOCK_ACCESS_TOKEN}

View File

@@ -0,0 +1,95 @@
# serializer version: 1
# name: test_entities[switch.smarla-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': None,
'entity_id': 'switch.smarla',
'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': 'smarla',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': 'ABCD-swing_active',
'unit_of_measurement': None,
})
# ---
# name: test_entities[switch.smarla-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Smarla',
}),
'context': <ANY>,
'entity_id': 'switch.smarla',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_entities[switch.smarla_smart_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': None,
'entity_id': 'switch.smarla_smart_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': 'Smart Mode',
'platform': 'smarla',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'smart_mode',
'unique_id': 'ABCD-smart_mode',
'unit_of_measurement': None,
})
# ---
# name: test_entities[switch.smarla_smart_mode-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Smarla Smart Mode',
}),
'context': <ANY>,
'entity_id': 'switch.smarla_smart_mode',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---

View File

@@ -0,0 +1,102 @@
"""Test config flow for Swing2Sleep Smarla integration."""
from unittest.mock import AsyncMock, patch
from homeassistant.components.smarla.const import DOMAIN
from homeassistant.config_entries import SOURCE_USER
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
from .const import MOCK_SERIAL_NUMBER, MOCK_USER_INPUT
from tests.common import MockConfigEntry
async def test_config_flow(
hass: HomeAssistant, mock_setup_entry, mock_connection
) -> None:
"""Test creating a config entry."""
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=MOCK_USER_INPUT,
)
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["title"] == MOCK_SERIAL_NUMBER
assert result["data"] == MOCK_USER_INPUT
assert result["result"].unique_id == MOCK_SERIAL_NUMBER
async def test_malformed_token(
hass: HomeAssistant, mock_setup_entry, mock_connection
) -> None:
"""Test we show user form on malformed token input."""
with patch(
"homeassistant.components.smarla.config_flow.Connection", side_effect=ValueError
):
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_USER},
data=MOCK_USER_INPUT,
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "user"
assert result["errors"] == {"base": "malformed_token"}
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input=MOCK_USER_INPUT,
)
assert result["type"] is FlowResultType.CREATE_ENTRY
async def test_invalid_auth(
hass: HomeAssistant, mock_setup_entry, mock_connection
) -> None:
"""Test we show user form on invalid auth."""
with patch.object(
mock_connection, "refresh_token", new=AsyncMock(return_value=False)
):
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_USER},
data=MOCK_USER_INPUT,
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "user"
assert result["errors"] == {"base": "invalid_auth"}
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input=MOCK_USER_INPUT,
)
assert result["type"] is FlowResultType.CREATE_ENTRY
async def test_device_exists_abort(
hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_connection
) -> None:
"""Test we abort config flow if Smarla device already configured."""
mock_config_entry.add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_USER},
data=MOCK_USER_INPUT,
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "already_configured"
assert len(hass.config_entries.async_entries(DOMAIN)) == 1

View File

@@ -0,0 +1,21 @@
"""Test switch platform for Swing2Sleep Smarla integration."""
from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry
async def test_init_invalid_auth(
hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_connection
) -> None:
"""Test init invalid authentication behavior."""
# Add the mock entry to hass
mock_config_entry.add_to_hass(hass)
mock_connection.refresh_token.return_value = False
# Set up the platform
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR

View File

@@ -0,0 +1,87 @@
"""Test switch platform for Swing2Sleep Smarla integration."""
from unittest.mock import AsyncMock, patch
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
from homeassistant.const import (
ATTR_ENTITY_ID,
SERVICE_TURN_OFF,
SERVICE_TURN_ON,
STATE_OFF,
STATE_ON,
Platform,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from . import setup_integration, update_property_listeners
from tests.common import MockConfigEntry, snapshot_platform
async def test_entities(
hass: HomeAssistant,
mock_federwiege: AsyncMock,
mock_config_entry: MockConfigEntry,
entity_registry: er.EntityRegistry,
snapshot: SnapshotAssertion,
) -> None:
"""Test the Spotify entities."""
with (
patch("homeassistant.components.smarla.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(
("service", "parameter"),
[
(SERVICE_TURN_ON, True),
(SERVICE_TURN_OFF, False),
],
)
async def test_switch_action(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_federwiege: AsyncMock,
mock_property: AsyncMock,
service: str,
parameter: bool,
) -> None:
"""Test Smarla Switch on/off behavior."""
await setup_integration(hass, mock_config_entry)
# Turn on
await hass.services.async_call(
SWITCH_DOMAIN,
service,
{ATTR_ENTITY_ID: "switch.smarla"},
blocking=True,
)
mock_property.set.assert_called_once_with(parameter)
async def test_switch_state_update(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_federwiege: AsyncMock,
mock_property: AsyncMock,
) -> None:
"""Test Smarla Switch on/off behavior."""
await setup_integration(hass, mock_config_entry)
assert hass.states.get("switch.smarla").state == STATE_OFF
mock_property.get.return_value = True
await update_property_listeners(mock_property)
await hass.async_block_till_done()
assert hass.states.get("switch.smarla").state == STATE_ON