1
0
mirror of https://github.com/home-assistant/core.git synced 2026-07-15 02:23:47 +01:00

Add light platform to systemnexa2 (#163710)

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
This commit is contained in:
konsulten
2026-02-24 21:29:46 +01:00
committed by GitHub
parent adfe4f2b62
commit 9ba28150e9
10 changed files with 573 additions and 114 deletions
@@ -6,4 +6,4 @@ from homeassistant.const import Platform
DOMAIN = "systemnexa2"
MANUFACTURER = "NEXA"
PLATFORMS: Final = [Platform.SWITCH]
PLATFORMS: Final = [Platform.LIGHT, Platform.SWITCH]
@@ -156,6 +156,12 @@ class SystemNexa2DataUpdateCoordinator(DataUpdateCoordinator[SystemNexa2Data]):
"""Toggle the device."""
await self._async_sn2_call_with_error_handling(self.device.toggle())
async def async_set_brightness(self, value: float) -> None:
"""Set the brightness of the device (0.0 to 1.0)."""
await self._async_sn2_call_with_error_handling(
self.device.set_brightness(value)
)
async def async_setting_enable(self, setting: OnOffSetting) -> None:
"""Enable a device setting."""
await self._async_sn2_call_with_error_handling(setting.enable(self.device))
@@ -0,0 +1,74 @@
"""Light entity for the SystemNexa2 integration."""
from typing import Any
from homeassistant.components.light import ATTR_BRIGHTNESS, ColorMode, LightEntity
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import SystemNexa2ConfigEntry, SystemNexa2DataUpdateCoordinator
from .entity import SystemNexa2Entity
PARALLEL_UPDATES = 0
async def async_setup_entry(
hass: HomeAssistant,
entry: SystemNexa2ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up lights based on a config entry."""
coordinator = entry.runtime_data
# Only add light entity for dimmable devices
if coordinator.data.info_data.dimmable:
async_add_entities([SystemNexa2Light(coordinator)])
class SystemNexa2Light(SystemNexa2Entity, LightEntity):
"""Representation of a dimmable SystemNexa2 light."""
_attr_translation_key = "light"
_attr_color_mode = ColorMode.BRIGHTNESS
_attr_supported_color_modes = {ColorMode.BRIGHTNESS}
def __init__(
self,
coordinator: SystemNexa2DataUpdateCoordinator,
) -> None:
"""Initialize the light."""
super().__init__(
coordinator=coordinator,
key="light",
)
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn on the light."""
# Check if we're setting brightness
if ATTR_BRIGHTNESS in kwargs:
brightness = kwargs[ATTR_BRIGHTNESS]
# Convert HomeAssistant brightness (0-255) to device brightness (0-1.0)
value = brightness / 255
await self.coordinator.async_set_brightness(value)
else:
await self.coordinator.async_turn_on()
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn off the light."""
await self.coordinator.async_turn_off()
@property
def is_on(self) -> bool | None:
"""Return true if the light is on."""
if self.coordinator.data.state is None:
return None
# Consider the light on if brightness is greater than 0
return self.coordinator.data.state > 0
@property
def brightness(self) -> int | None:
"""Return the brightness of the light (0-255)."""
if self.coordinator.data.state is None:
return None
# Convert device brightness (0-1.0) to HomeAssistant brightness (0-255)
return max(0, min(255, round(self.coordinator.data.state * 255)))
@@ -31,6 +31,11 @@
}
},
"entity": {
"light": {
"light": {
"name": "Light"
}
},
"switch": {
"433mhz": {
"name": "433 MHz"
@@ -45,7 +50,7 @@
"name": "Physical button"
},
"relay_1": {
"name": "Relay 1"
"name": "Relay"
}
}
},
+49 -27
View File
@@ -2,6 +2,7 @@
from collections.abc import Generator
from ipaddress import ip_address
from typing import Any
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
@@ -14,6 +15,39 @@ from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
from tests.common import MockConfigEntry
@pytest.fixture(params=[False])
def dimmable(request: pytest.FixtureRequest) -> bool:
"""Return whether device is dimmable."""
return request.param
@pytest.fixture
def device_info(dimmable: bool) -> dict[str, Any]:
"""Return device configuration based on type."""
# Create mock settings (same for all devices)
mock_setting_433mhz = MagicMock(spec=OnOffSetting)
mock_setting_433mhz.name = "433Mhz"
mock_setting_433mhz.enable = AsyncMock()
mock_setting_433mhz.disable = AsyncMock()
mock_setting_433mhz.is_enabled = MagicMock(return_value=True)
mock_setting_cloud = MagicMock(spec=OnOffSetting)
mock_setting_cloud.name = "Cloud Access"
mock_setting_cloud.enable = AsyncMock()
mock_setting_cloud.disable = AsyncMock()
mock_setting_cloud.is_enabled = MagicMock(return_value=False)
return {
"name": "In-Wall Dimmer" if dimmable else "Outdoor Smart Plug",
"model": "WBD-01" if dimmable else "WPO-01",
"unique_id": "aabbccddee01" if dimmable else "aabbccddee02",
"host": "10.0.0.101" if dimmable else "10.0.0.100",
"initial_state": 0.5 if dimmable else 1.0,
"settings": [mock_setting_433mhz, mock_setting_cloud],
"dimmable": dimmable,
}
@pytest.fixture
def mock_setup_entry() -> Generator[AsyncMock]:
"""Override async_setup_entry."""
@@ -24,7 +58,7 @@ def mock_setup_entry() -> Generator[AsyncMock]:
@pytest.fixture
def mock_system_nexa_2_device() -> Generator[MagicMock]:
def mock_system_nexa_2_device(device_info: dict[str, Any]) -> Generator[MagicMock]:
"""Mock the System Nexa 2 API."""
with (
patch(
@@ -36,30 +70,17 @@ def mock_system_nexa_2_device() -> Generator[MagicMock]:
):
device = mock_device.return_value
device.info_data = InformationData(
name="Test Device",
model="Test Model",
unique_id="test_device_id",
name=device_info["name"],
model=device_info["model"],
unique_id=device_info["unique_id"],
sw_version="Test Model Version",
hw_version="Test HW Version",
wifi_dbm=-50,
wifi_ssid="Test WiFi SSID",
dimmable=False,
dimmable=device_info["dimmable"],
)
# Create mock OnOffSettings
mock_setting_433mhz = MagicMock(spec=OnOffSetting)
mock_setting_433mhz.name = "433Mhz"
mock_setting_433mhz.enable = AsyncMock()
mock_setting_433mhz.disable = AsyncMock()
mock_setting_433mhz.is_enabled = MagicMock(return_value=True)
mock_setting_cloud = MagicMock(spec=OnOffSetting)
mock_setting_cloud.name = "Cloud Access"
mock_setting_cloud.enable = AsyncMock()
mock_setting_cloud.disable = AsyncMock()
mock_setting_cloud.is_enabled = MagicMock(return_value=False)
device.settings = [mock_setting_433mhz, mock_setting_cloud]
device.settings = device_info["settings"]
device.get_info = AsyncMock()
device.get_info.return_value = InformationUpdate(information=device.info_data)
@@ -72,13 +93,14 @@ def mock_system_nexa_2_device() -> Generator[MagicMock]:
"on_update"
)
if on_update:
await on_update(StateChange(state=1.0))
await on_update(StateChange(state=device_info["initial_state"]))
device.connect = AsyncMock(side_effect=mock_connect)
device.disconnect = AsyncMock()
device.turn_on = AsyncMock()
device.turn_off = AsyncMock()
device.toggle = AsyncMock()
device.set_brightness = AsyncMock()
mock_device.is_device_supported = MagicMock(return_value=(True, ""))
mock_device.initiate_device = AsyncMock(return_value=device)
@@ -86,16 +108,16 @@ def mock_system_nexa_2_device() -> Generator[MagicMock]:
@pytest.fixture
def mock_config_entry() -> MockConfigEntry:
def mock_config_entry(device_info: dict[str, Any]) -> MockConfigEntry:
"""Return a mock config entry."""
return MockConfigEntry(
domain=DOMAIN,
unique_id="test_device_id",
unique_id=device_info["unique_id"],
data={
CONF_HOST: "10.0.0.100",
CONF_NAME: "Test Device",
CONF_DEVICE_ID: "test_device_id",
CONF_MODEL: "Test Model",
CONF_HOST: device_info["host"],
CONF_NAME: device_info["name"],
CONF_DEVICE_ID: device_info["unique_id"],
CONF_MODEL: device_info["model"],
},
)
@@ -121,5 +143,5 @@ def mock_zeroconf_discovery_info():
name="systemnexa2_test._systemnexa2._tcp.local.",
port=80,
type="_systemnexa2._tcp.local.",
properties={"id": "test_device_id", "model": "Test Model", "version": "1.0.0"},
properties={"id": "aabbccddee02", "model": "WPO-01", "version": "1.0.0"},
)
@@ -0,0 +1,60 @@
# serializer version: 1
# name: test_light_entities[True][light.in_wall_dimmer_light-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'supported_color_modes': list([
<ColorMode.BRIGHTNESS: 'brightness'>,
]),
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'light',
'entity_category': None,
'entity_id': 'light.in_wall_dimmer_light',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Light',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Light',
'platform': 'systemnexa2',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'light',
'unique_id': 'aabbccddee01-light',
'unit_of_measurement': None,
})
# ---
# name: test_light_entities[True][light.in_wall_dimmer_light-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'brightness': 128,
'color_mode': <ColorMode.BRIGHTNESS: 'brightness'>,
'friendly_name': 'In-Wall Dimmer Light',
'supported_color_modes': list([
<ColorMode.BRIGHTNESS: 'brightness'>,
]),
'supported_features': <LightEntityFeature: 0>,
}),
'context': <ANY>,
'entity_id': 'light.in_wall_dimmer_light',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'on',
})
# ---
@@ -1,5 +1,5 @@
# serializer version: 1
# name: test_switch_entities[switch.test_device_433_mhz-entry]
# name: test_switch_entities[False][switch.outdoor_smart_plug_433_mhz-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
@@ -12,7 +12,7 @@
'disabled_by': None,
'domain': 'switch',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'switch.test_device_433_mhz',
'entity_id': 'switch.outdoor_smart_plug_433_mhz',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
@@ -31,25 +31,25 @@
'suggested_object_id': None,
'supported_features': 0,
'translation_key': '433mhz',
'unique_id': 'test_device_id-433Mhz',
'unique_id': 'aabbccddee02-433Mhz',
'unit_of_measurement': None,
})
# ---
# name: test_switch_entities[switch.test_device_433_mhz-state]
# name: test_switch_entities[False][switch.outdoor_smart_plug_433_mhz-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'switch',
'friendly_name': 'Test Device 433 MHz',
'friendly_name': 'Outdoor Smart Plug 433 MHz',
}),
'context': <ANY>,
'entity_id': 'switch.test_device_433_mhz',
'entity_id': 'switch.outdoor_smart_plug_433_mhz',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'on',
})
# ---
# name: test_switch_entities[switch.test_device_cloud_access-entry]
# name: test_switch_entities[False][switch.outdoor_smart_plug_cloud_access-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
@@ -62,7 +62,7 @@
'disabled_by': None,
'domain': 'switch',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'switch.test_device_cloud_access',
'entity_id': 'switch.outdoor_smart_plug_cloud_access',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
@@ -81,25 +81,25 @@
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'cloud_access',
'unique_id': 'test_device_id-Cloud Access',
'unique_id': 'aabbccddee02-Cloud Access',
'unit_of_measurement': None,
})
# ---
# name: test_switch_entities[switch.test_device_cloud_access-state]
# name: test_switch_entities[False][switch.outdoor_smart_plug_cloud_access-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'switch',
'friendly_name': 'Test Device Cloud access',
'friendly_name': 'Outdoor Smart Plug Cloud access',
}),
'context': <ANY>,
'entity_id': 'switch.test_device_cloud_access',
'entity_id': 'switch.outdoor_smart_plug_cloud_access',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_switch_entities[switch.test_device_relay_1-entry]
# name: test_switch_entities[False][switch.outdoor_smart_plug_relay-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
@@ -112,7 +112,7 @@
'disabled_by': None,
'domain': 'switch',
'entity_category': None,
'entity_id': 'switch.test_device_relay_1',
'entity_id': 'switch.outdoor_smart_plug_relay',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
@@ -120,28 +120,28 @@
'labels': set({
}),
'name': None,
'object_id_base': 'Relay 1',
'object_id_base': 'Relay',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Relay 1',
'original_name': 'Relay',
'platform': 'systemnexa2',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'relay_1',
'unique_id': 'test_device_id-relay_1',
'unique_id': 'aabbccddee02-relay_1',
'unit_of_measurement': None,
})
# ---
# name: test_switch_entities[switch.test_device_relay_1-state]
# name: test_switch_entities[False][switch.outdoor_smart_plug_relay-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Test Device Relay 1',
'friendly_name': 'Outdoor Smart Plug Relay',
}),
'context': <ANY>,
'entity_id': 'switch.test_device_relay_1',
'entity_id': 'switch.outdoor_smart_plug_relay',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
@@ -30,14 +30,14 @@ async def test_full_flow(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> No
result["flow_id"], {CONF_HOST: "10.0.0.131"}
)
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["title"] == "Test Device (Test Model)"
assert result["title"] == "Outdoor Smart Plug (WPO-01)"
assert result["data"] == {
CONF_HOST: "10.0.0.131",
CONF_NAME: "Test Device",
CONF_DEVICE_ID: "test_device_id",
CONF_MODEL: "Test Model",
CONF_NAME: "Outdoor Smart Plug",
CONF_DEVICE_ID: "aabbccddee02",
CONF_MODEL: "WPO-01",
}
assert result["result"].unique_id == "test_device_id"
assert result["result"].unique_id == "aabbccddee02"
assert len(mock_setup_entry.mock_calls) == 1
@@ -98,7 +98,7 @@ async def test_connection_error_and_recovery(
result["flow_id"], {CONF_HOST: "10.0.0.131"}
)
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["title"] == "Test Device (Test Model)"
assert result["title"] == "Outdoor Smart Plug (WPO-01)"
assert len(mock_setup_entry.mock_calls) == 1
@@ -164,12 +164,12 @@ async def test_valid_hostname(hass: HomeAssistant, mock_setup_entry: AsyncMock)
result["flow_id"], {CONF_HOST: "valid-hostname.local"}
)
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["title"] == "Test Device (Test Model)"
assert result["title"] == "Outdoor Smart Plug (WPO-01)"
assert result["data"] == {
CONF_HOST: "valid-hostname.local",
CONF_NAME: "Test Device",
CONF_DEVICE_ID: "test_device_id",
CONF_MODEL: "Test Model",
CONF_NAME: "Outdoor Smart Plug",
CONF_DEVICE_ID: "aabbccddee02",
CONF_MODEL: "WPO-01",
}
assert len(mock_setup_entry.mock_calls) == 1
@@ -192,7 +192,7 @@ async def test_unsupported_device(
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "unsupported_model"
assert result["description_placeholders"] == {
"model": "Test Model",
"model": "WPO-01",
"sw_version": "Test Model Version",
}
@@ -215,12 +215,12 @@ async def test_zeroconf_discovery(
result["flow_id"], user_input={}
)
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["title"] == "systemnexa2_test (Test Model)"
assert result["title"] == "systemnexa2_test (WPO-01)"
assert result["data"] == {
CONF_HOST: "10.0.0.131",
CONF_NAME: "systemnexa2_test",
CONF_DEVICE_ID: "test_device_id",
CONF_MODEL: "Test Model",
CONF_DEVICE_ID: "aabbccddee02",
CONF_MODEL: "WPO-01",
}
assert len(mock_setup_entry.mock_calls) == 1
@@ -249,8 +249,8 @@ async def test_device_with_none_values(
device = mock_system_nexa_2_device.return_value
# Create new InformationData with None unique_id
device.info_data = InformationData(
name="Test Device",
model="Test Model",
name="Outdoor Smart Plug",
model="WPO-01",
unique_id=None,
sw_version="Test Model Version",
hw_version="Test HW Version",
@@ -281,7 +281,7 @@ async def test_zeroconf_discovery_none_values(hass: HomeAssistant) -> None:
type="_systemnexa2._tcp.local.",
properties={
"id": None,
"model": "Test Model",
"model": "WPO-01",
"version": "1.0.0",
},
)
+302
View File
@@ -0,0 +1,302 @@
"""Test the System Nexa 2 light platform."""
from unittest.mock import MagicMock, patch
import pytest
from sn2 import ConnectionStatus, StateChange
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.light import ATTR_BRIGHTNESS, DOMAIN as LIGHT_DOMAIN
from homeassistant.const import (
ATTR_ENTITY_ID,
SERVICE_TURN_OFF,
SERVICE_TURN_ON,
STATE_OFF,
STATE_ON,
STATE_UNAVAILABLE,
Platform,
)
from homeassistant.core import HomeAssistant
import homeassistant.helpers.entity_registry as er
from . import find_update_callback
from tests.common import MockConfigEntry, snapshot_platform
@pytest.mark.parametrize("dimmable", [True], indirect=True)
async def test_light_entities(
hass: HomeAssistant,
snapshot: SnapshotAssertion,
entity_registry: er.EntityRegistry,
mock_config_entry: MockConfigEntry,
mock_system_nexa_2_device: MagicMock,
) -> None:
"""Test the light entities."""
mock_config_entry.add_to_hass(hass)
# Only load the light platform for snapshot testing
with patch(
"homeassistant.components.systemnexa2.PLATFORMS",
[Platform.LIGHT],
):
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
await snapshot_platform(
hass, entity_registry, snapshot, mock_config_entry.entry_id
)
async def test_light_only_for_dimmable_devices(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_system_nexa_2_device: MagicMock,
) -> None:
"""Test that light entity is only created for dimmable devices."""
# The mock_system_nexa_2_device has dimmable=False
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
# Light entity should NOT exist for non-dimmable device
state = hass.states.get("light.outdoor_smart_plug")
assert state is None
@pytest.mark.parametrize("dimmable", [True], indirect=True)
async def test_light_control_operations(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_system_nexa_2_device: MagicMock,
) -> None:
"""Test all light control operations (on/off/toggle/dim)."""
device = mock_system_nexa_2_device.return_value
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
entity_id = "light.in_wall_dimmer_light"
# Verify initial state (should be on with 50% brightness from fixture)
state = hass.states.get(entity_id)
assert state is not None
assert state.state == STATE_ON
# Test turn on without brightness
await hass.services.async_call(
LIGHT_DOMAIN,
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: entity_id},
blocking=True,
)
device.turn_on.assert_called_once()
device.set_brightness.assert_not_called()
device.turn_on.reset_mock()
# Test turn on with brightness=128 (50% in HA scale 0-255)
await hass.services.async_call(
LIGHT_DOMAIN,
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 128},
blocking=True,
)
device.set_brightness.assert_called_once_with(128 / 255)
device.turn_on.assert_not_called()
device.set_brightness.reset_mock()
# Test turn on with brightness=255 (100% in HA scale)
await hass.services.async_call(
LIGHT_DOMAIN,
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 255},
blocking=True,
)
device.set_brightness.assert_called_once_with(255 / 255)
device.set_brightness.reset_mock()
# Test turn on with brightness=1 (minimum non-zero in HA scale)
await hass.services.async_call(
LIGHT_DOMAIN,
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 1},
blocking=True,
)
device.set_brightness.assert_called_once_with(1 / 255)
device.set_brightness.reset_mock()
# Test turn off
await hass.services.async_call(
LIGHT_DOMAIN,
SERVICE_TURN_OFF,
{ATTR_ENTITY_ID: entity_id},
blocking=True,
)
device.turn_off.assert_called_once()
device.turn_off.reset_mock()
# No reason to test toggle service as its an internal function using turn_on/off
@pytest.mark.parametrize("dimmable", [True], indirect=True)
async def test_light_brightness_property(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_system_nexa_2_device: MagicMock,
) -> None:
"""Test light brightness property conversion."""
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
# Find the callback that was registered with the device
update_callback = find_update_callback(mock_system_nexa_2_device)
# Test with state = 0.5 (50% in device scale, should be 128 in HA scale)
await update_callback(StateChange(state=0.5))
await hass.async_block_till_done()
state = hass.states.get("light.in_wall_dimmer_light")
assert state is not None
assert state.state == STATE_ON
assert state.attributes.get(ATTR_BRIGHTNESS) == 128
# Test with state = 1.0 (100% in device scale, should be 255 in HA scale)
await update_callback(StateChange(state=1.0))
await hass.async_block_till_done()
state = hass.states.get("light.in_wall_dimmer_light")
assert state is not None
assert state.state == STATE_ON
assert state.attributes.get(ATTR_BRIGHTNESS) == 255
# Test with state = 0.0 (0% - light should be off)
await update_callback(StateChange(state=0.0))
await hass.async_block_till_done()
state = hass.states.get("light.in_wall_dimmer_light")
assert state is not None
assert state.state == STATE_OFF
# Test with state = 0.1 (10% in device scale, should be 26 in HA scale)
await update_callback(StateChange(state=0.1))
await hass.async_block_till_done()
state = hass.states.get("light.in_wall_dimmer_light")
assert state is not None
assert state.state == STATE_ON
assert state.attributes.get(ATTR_BRIGHTNESS) == 26
@pytest.mark.parametrize("dimmable", [True], indirect=True)
async def test_light_is_on_property(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_system_nexa_2_device: MagicMock,
) -> None:
"""Test light is_on property."""
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
# Find the callback that was registered with the device
update_callback = find_update_callback(mock_system_nexa_2_device)
# Test with state > 0 (light is on)
await update_callback(StateChange(state=0.5))
await hass.async_block_till_done()
state = hass.states.get("light.in_wall_dimmer_light")
assert state is not None
assert state.state == STATE_ON
# Test with state = 0 (light is off)
await update_callback(StateChange(state=0.0))
await hass.async_block_till_done()
state = hass.states.get("light.in_wall_dimmer_light")
assert state is not None
assert state.state == STATE_OFF
@pytest.mark.parametrize("dimmable", [True], indirect=True)
async def test_coordinator_connection_status(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_system_nexa_2_device: MagicMock,
) -> None:
"""Test coordinator handles connection status updates for light."""
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
# Find the callback that was registered with the device
update_callback = find_update_callback(mock_system_nexa_2_device)
# Initially, the light should be on (state=0.5 from fixture)
state = hass.states.get("light.in_wall_dimmer_light")
assert state is not None
assert state.state == STATE_ON
# Simulate device disconnection
await update_callback(ConnectionStatus(connected=False))
await hass.async_block_till_done()
state = hass.states.get("light.in_wall_dimmer_light")
assert state is not None
assert state.state == STATE_UNAVAILABLE
# Simulate reconnection and state update
await update_callback(ConnectionStatus(connected=True))
await update_callback(StateChange(state=0.75))
await hass.async_block_till_done()
state = hass.states.get("light.in_wall_dimmer_light")
assert state is not None
assert state.state == STATE_ON
assert state.attributes.get(ATTR_BRIGHTNESS) == 191 # 0.75 * 255 ≈ 191
@pytest.mark.parametrize("dimmable", [True], indirect=True)
async def test_coordinator_state_change(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_system_nexa_2_device: MagicMock,
) -> None:
"""Test coordinator handles state change updates for light."""
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
# Find the callback that was registered with the device
update_callback = find_update_callback(mock_system_nexa_2_device)
# Change state to off (0.0)
await update_callback(StateChange(state=0.0))
await hass.async_block_till_done()
state = hass.states.get("light.in_wall_dimmer_light")
assert state is not None
assert state.state == STATE_OFF
# Change state to 25% (0.25)
await update_callback(StateChange(state=0.25))
await hass.async_block_till_done()
state = hass.states.get("light.in_wall_dimmer_light")
assert state is not None
assert state.state == STATE_ON
assert state.attributes.get(ATTR_BRIGHTNESS) == 64 # 0.25 * 255 ≈ 64
# Change state to full brightness (1.0)
await update_callback(StateChange(state=1.0))
await hass.async_block_till_done()
state = hass.states.get("light.in_wall_dimmer_light")
assert state is not None
assert state.state == STATE_ON
assert state.attributes.get(ATTR_BRIGHTNESS) == 255
+38 -48
View File
@@ -1,9 +1,9 @@
"""Test the System Nexa 2 switch platform."""
from unittest.mock import AsyncMock, MagicMock
from unittest.mock import MagicMock
import pytest
from sn2 import ConnectionStatus, OnOffSetting, SettingsUpdate, StateChange
from sn2 import ConnectionStatus, SettingsUpdate, StateChange
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
@@ -52,18 +52,16 @@ async def test_switch_turn_on_off_toggle(
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
# Get the coordinator and update it with state
entry = hass.config_entries.async_get_entry(mock_config_entry.entry_id)
assert entry
coordinator = entry.runtime_data
await coordinator._async_handle_update(StateChange(state=0.0))
# Find the callback that was registered with the device
update_callback = find_update_callback(mock_system_nexa_2_device)
await update_callback(StateChange(state=0.0))
await hass.async_block_till_done()
# Test turn on
await hass.services.async_call(
SWITCH_DOMAIN,
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: "switch.test_device_relay_1"},
{ATTR_ENTITY_ID: "switch.outdoor_smart_plug_relay"},
blocking=True,
)
device.turn_on.assert_called_once()
@@ -72,7 +70,7 @@ async def test_switch_turn_on_off_toggle(
await hass.services.async_call(
SWITCH_DOMAIN,
SERVICE_TURN_OFF,
{ATTR_ENTITY_ID: "switch.test_device_relay_1"},
{ATTR_ENTITY_ID: "switch.outdoor_smart_plug_relay"},
blocking=True,
)
device.turn_off.assert_called_once()
@@ -81,7 +79,7 @@ async def test_switch_turn_on_off_toggle(
await hass.services.async_call(
SWITCH_DOMAIN,
SERVICE_TOGGLE,
{ATTR_ENTITY_ID: "switch.test_device_relay_1"},
{ATTR_ENTITY_ID: "switch.outdoor_smart_plug_relay"},
blocking=True,
)
device.toggle.assert_called_once()
@@ -98,22 +96,23 @@ async def test_switch_is_on_property(
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
# Get the coordinator and update it with state
entry = hass.config_entries.async_get_entry(mock_config_entry.entry_id)
coordinator = entry.runtime_data
# Find the callback that was registered with the device
update_callback = find_update_callback(mock_system_nexa_2_device)
# Test with state = 1.0 (on)
await coordinator._async_handle_update(StateChange(state=1.0))
await update_callback(StateChange(state=1.0))
await hass.async_block_till_done()
state = hass.states.get("switch.test_device_relay_1")
state = hass.states.get("switch.outdoor_smart_plug_relay")
assert state is not None
assert state.state == "on"
# Test with state = 0.0 (off)
await coordinator._async_handle_update(StateChange(state=0.0))
await update_callback(StateChange(state=0.0))
await hass.async_block_till_done()
state = hass.states.get("switch.test_device_relay_1")
state = hass.states.get("switch.outdoor_smart_plug_relay")
assert state is not None
assert state.state == "off"
@@ -125,39 +124,26 @@ async def test_configuration_switches(
"""Test configuration switch entities."""
device = mock_system_nexa_2_device.return_value
# Create mock OnOffSettings with keys matching SWITCH_TYPES
mock_setting_433mhz = MagicMock(spec=OnOffSetting)
mock_setting_433mhz.name = "433Mhz" # Must match key in SWITCH_TYPES
mock_setting_433mhz.enable = AsyncMock()
mock_setting_433mhz.disable = AsyncMock()
mock_setting_433mhz.is_enabled = MagicMock(return_value=True)
mock_setting_cloud = MagicMock(spec=OnOffSetting)
mock_setting_cloud.name = "Cloud Access" # Must match key in SWITCH_TYPES
mock_setting_cloud.enable = AsyncMock()
mock_setting_cloud.disable = AsyncMock()
mock_setting_cloud.is_enabled = MagicMock(return_value=False)
# Set settings before setup
device.settings = [mock_setting_433mhz, mock_setting_cloud]
# Settings are already configured in the fixture
mock_setting_433mhz = device.settings[0] # 433Mhz
mock_setting_cloud = device.settings[1] # Cloud Access
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
# Make coordinator data available
entry = hass.config_entries.async_get_entry(mock_config_entry.entry_id)
coordinator = entry.runtime_data
await coordinator._async_handle_update(StateChange(state=1.0))
# Find the callback that was registered with the device
update_callback = find_update_callback(mock_system_nexa_2_device)
await update_callback(StateChange(state=1.0))
await hass.async_block_till_done()
# Check 433mhz switch state (should be on)
state = hass.states.get("switch.test_device_433_mhz")
state = hass.states.get("switch.outdoor_smart_plug_433_mhz")
assert state is not None
assert state.state == "on"
# Check cloud_access switch state (should be off)
state = hass.states.get("switch.test_device_cloud_access")
state = hass.states.get("switch.outdoor_smart_plug_cloud_access")
assert state is not None
assert state.state == "off"
@@ -165,7 +151,7 @@ async def test_configuration_switches(
await hass.services.async_call(
SWITCH_DOMAIN,
SERVICE_TURN_OFF,
{ATTR_ENTITY_ID: "switch.test_device_433_mhz"},
{ATTR_ENTITY_ID: "switch.outdoor_smart_plug_433_mhz"},
blocking=True,
)
mock_setting_433mhz.disable.assert_called_once_with(device)
@@ -174,7 +160,7 @@ async def test_configuration_switches(
await hass.services.async_call(
SWITCH_DOMAIN,
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: "switch.test_device_cloud_access"},
{ATTR_ENTITY_ID: "switch.outdoor_smart_plug_cloud_access"},
blocking=True,
)
mock_setting_cloud.enable.assert_called_once_with(device)
@@ -193,8 +179,8 @@ async def test_coordinator_connection_status(
# Find the callback that was registered with the device
update_callback = find_update_callback(mock_system_nexa_2_device)
# Initially, the relay switch should be off (state=1.0 from fixture)
state = hass.states.get("switch.test_device_relay_1")
# Initially, the relay switch should be on (state=1.0 from fixture)
state = hass.states.get("switch.outdoor_smart_plug_relay")
assert state is not None
assert state.state == STATE_ON
@@ -202,7 +188,8 @@ async def test_coordinator_connection_status(
await update_callback(ConnectionStatus(connected=False))
await hass.async_block_till_done()
state = hass.states.get("switch.test_device_relay_1")
state = hass.states.get("switch.outdoor_smart_plug_relay")
assert state is not None
assert state.state == STATE_UNAVAILABLE
# Simulate reconnection and state update
@@ -210,7 +197,8 @@ async def test_coordinator_connection_status(
await update_callback(StateChange(state=1.0))
await hass.async_block_till_done()
state = hass.states.get("switch.test_device_relay_1")
state = hass.states.get("switch.outdoor_smart_plug_relay")
assert state is not None
assert state.state == STATE_ON
@@ -231,7 +219,7 @@ async def test_coordinator_state_change(
await update_callback(StateChange(state=0.0))
await hass.async_block_till_done()
state = hass.states.get("switch.test_device_relay_1")
state = hass.states.get("switch.outdoor_smart_plug_relay")
assert state is not None
assert state.state == STATE_OFF
@@ -239,7 +227,8 @@ async def test_coordinator_state_change(
await update_callback(StateChange(state=1.0))
await hass.async_block_till_done()
state = hass.states.get("switch.test_device_relay_1")
state = hass.states.get("switch.outdoor_smart_plug_relay")
assert state is not None
assert state.state == STATE_ON
@@ -257,7 +246,7 @@ async def test_coordinator_settings_update(
update_callback = find_update_callback(mock_system_nexa_2_device)
# Get initial state of 433Mhz switch (should be on from fixture)
state = hass.states.get("switch.test_device_433_mhz")
state = hass.states.get("switch.outdoor_smart_plug_433_mhz")
assert state is not None
assert state.state == STATE_ON
@@ -271,5 +260,6 @@ async def test_coordinator_settings_update(
await update_callback(StateChange(state=1.0))
await hass.async_block_till_done()
state = hass.states.get("switch.test_device_433_mhz")
state = hass.states.get("switch.outdoor_smart_plug_433_mhz")
assert state is not None
assert state.state == STATE_OFF