mirror of
https://github.com/home-assistant/core.git
synced 2026-09-24 23:41:48 -05:00
Create one light entity per Modern Forms Gen4 light fixture (#180524)
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
20ea690f96
commit
fb8a2aa00e
@@ -8,6 +8,22 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
from .const import DOMAIN
|
||||
from .coordinator import ModernFormsDataUpdateCoordinator
|
||||
|
||||
_NAME_SEPARATORS = " -_"
|
||||
|
||||
|
||||
def strip_device_name_prefix(device_name: str, name: str) -> str | None:
|
||||
"""Strip a leading device-name prefix so has_entity_name doesn't duplicate it.
|
||||
|
||||
Returns None (rather than a name identical to the device name) when
|
||||
the fixture name adds nothing beyond the device name.
|
||||
"""
|
||||
if not device_name or not name.lower().startswith(device_name.lower()):
|
||||
return name
|
||||
rest = name[len(device_name) :]
|
||||
if rest and rest[0] not in _NAME_SEPARATORS:
|
||||
return name
|
||||
return rest.lstrip(_NAME_SEPARATORS) or None
|
||||
|
||||
|
||||
class ModernFormsDeviceEntity(CoordinatorEntity[ModernFormsDataUpdateCoordinator]):
|
||||
"""Defines a Modern Forms device entity."""
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from typing import Any, override
|
||||
|
||||
from aiomodernforms.const import LIGHT_POWER_OFF, LIGHT_POWER_ON
|
||||
from aiomodernforms.models import Light
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.light import ATTR_BRIGHTNESS, ColorMode, LightEntity
|
||||
@@ -24,7 +25,7 @@ from .const import (
|
||||
SERVICE_SET_LIGHT_SLEEP_TIMER,
|
||||
)
|
||||
from .coordinator import ModernFormsConfigEntry, ModernFormsDataUpdateCoordinator
|
||||
from .entity import ModernFormsDeviceEntity
|
||||
from .entity import ModernFormsDeviceEntity, strip_device_name_prefix
|
||||
|
||||
BRIGHTNESS_RANGE = (1, 255)
|
||||
|
||||
@@ -61,11 +62,12 @@ async def async_setup_entry(
|
||||
)
|
||||
|
||||
async_add_entities(
|
||||
[
|
||||
ModernFormsLightEntity(
|
||||
entry_id=config_entry.entry_id, coordinator=coordinator
|
||||
)
|
||||
]
|
||||
ModernFormsLightEntity(
|
||||
entry_id=config_entry.entry_id,
|
||||
coordinator=coordinator,
|
||||
light_address=light.address,
|
||||
)
|
||||
for light in coordinator.data.state.light_fixtures
|
||||
)
|
||||
|
||||
|
||||
@@ -77,49 +79,79 @@ class ModernFormsLightEntity(ModernFormsDeviceEntity, LightEntity):
|
||||
_attr_translation_key = "light"
|
||||
|
||||
def __init__(
|
||||
self, entry_id: str, coordinator: ModernFormsDataUpdateCoordinator
|
||||
self,
|
||||
entry_id: str,
|
||||
coordinator: ModernFormsDataUpdateCoordinator,
|
||||
light_address: int | None,
|
||||
) -> None:
|
||||
"""Initialize Modern Forms light."""
|
||||
super().__init__(
|
||||
entry_id=entry_id,
|
||||
coordinator=coordinator,
|
||||
)
|
||||
self._attr_unique_id = f"{self.coordinator.data.info.mac_address}"
|
||||
super().__init__(entry_id=entry_id, coordinator=coordinator)
|
||||
self._address = light_address
|
||||
mac_address = self.coordinator.data.info.mac_address
|
||||
|
||||
if light_address is None:
|
||||
self._attr_unique_id = mac_address
|
||||
else:
|
||||
# Real Gen4 fixtures are named by the user, so the device-name
|
||||
# prefix strip below is per-device data rather than static.
|
||||
self._attr_unique_id = f"{mac_address}_{light_address}"
|
||||
fixture_name = next(
|
||||
light.name
|
||||
for light in coordinator.data.state.light_fixtures
|
||||
if light.address == light_address
|
||||
)
|
||||
self._attr_name = strip_device_name_prefix(
|
||||
self.coordinator.data.info.device_name, fixture_name
|
||||
)
|
||||
|
||||
@property
|
||||
def _light(self) -> Light | None:
|
||||
"""Return this entity's current fixture data, if it still exists."""
|
||||
for light in self.coordinator.data.state.light_fixtures:
|
||||
if light.address == self._address:
|
||||
return light
|
||||
return None
|
||||
|
||||
@property
|
||||
@override
|
||||
def available(self) -> bool:
|
||||
"""Return True if the fixture this entity represents still exists."""
|
||||
return super().available and self._light is not None
|
||||
|
||||
@property
|
||||
@override
|
||||
def brightness(self) -> int | None:
|
||||
"""Return the brightness of this light between 1..255."""
|
||||
if self._light is None:
|
||||
return None
|
||||
return round(
|
||||
percentage_to_ranged_value(
|
||||
BRIGHTNESS_RANGE, self.coordinator.data.state.light_brightness
|
||||
)
|
||||
percentage_to_ranged_value(BRIGHTNESS_RANGE, self._light.brightness)
|
||||
)
|
||||
|
||||
@property
|
||||
@override
|
||||
def is_on(self) -> bool:
|
||||
"""Return the state of the light."""
|
||||
return bool(self.coordinator.data.state.light_on)
|
||||
return self._light is not None and bool(self._light.on)
|
||||
|
||||
@modernforms_exception_handler
|
||||
@override
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn off the light."""
|
||||
await self.coordinator.modern_forms.light(on=LIGHT_POWER_OFF)
|
||||
await self._async_control_light(on=LIGHT_POWER_OFF)
|
||||
|
||||
@modernforms_exception_handler
|
||||
@override
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn on the light."""
|
||||
data = {OPT_ON: LIGHT_POWER_ON}
|
||||
data: dict[str, Any] = {OPT_ON: LIGHT_POWER_ON}
|
||||
|
||||
if ATTR_BRIGHTNESS in kwargs:
|
||||
data[OPT_BRIGHTNESS] = ranged_value_to_percentage(
|
||||
BRIGHTNESS_RANGE, kwargs[ATTR_BRIGHTNESS]
|
||||
)
|
||||
|
||||
await self.coordinator.modern_forms.light(**data)
|
||||
await self._async_control_light(**data)
|
||||
|
||||
@modernforms_exception_handler
|
||||
async def async_set_light_sleep_timer(
|
||||
@@ -127,11 +159,18 @@ class ModernFormsLightEntity(ModernFormsDeviceEntity, LightEntity):
|
||||
sleep_time: int,
|
||||
) -> None:
|
||||
"""Set a Modern Forms light sleep timer."""
|
||||
await self.coordinator.modern_forms.light(sleep=sleep_time * 60)
|
||||
await self._async_control_light(sleep=sleep_time * 60)
|
||||
|
||||
@modernforms_exception_handler
|
||||
async def async_clear_light_sleep_timer(
|
||||
self,
|
||||
) -> None:
|
||||
"""Clear a Modern Forms light sleep timer."""
|
||||
await self.coordinator.modern_forms.light(sleep=CLEAR_TIMER)
|
||||
await self._async_control_light(sleep=CLEAR_TIMER)
|
||||
|
||||
async def _async_control_light(self, **kwargs: Any) -> None:
|
||||
"""Send a control command to this entity's fixture."""
|
||||
if self._address is None:
|
||||
await self.coordinator.modern_forms.light(**kwargs)
|
||||
else:
|
||||
await self.coordinator.modern_forms.light_fixture(self._address, **kwargs)
|
||||
|
||||
@@ -6,12 +6,17 @@ import json
|
||||
from typing import Any
|
||||
|
||||
from aiomodernforms.const import COMMAND_QUERY_STATIC_DATA
|
||||
from yarl import URL
|
||||
|
||||
from homeassistant.components.modern_forms.const import DOMAIN
|
||||
from homeassistant.const import CONF_HOST, CONF_MAC, CONTENT_TYPE_JSON
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.common import MockConfigEntry, async_load_fixture
|
||||
from tests.common import (
|
||||
MockConfigEntry,
|
||||
async_load_fixture,
|
||||
async_load_json_object_fixture,
|
||||
)
|
||||
from tests.test_util.aiohttp import AiohttpClientMocker, AiohttpClientMockResponse
|
||||
|
||||
|
||||
@@ -120,3 +125,52 @@ async def init_integration(
|
||||
await hass.async_block_till_done()
|
||||
|
||||
return entry
|
||||
|
||||
|
||||
async def modern_forms_gen4_call_mock(
|
||||
hass: HomeAssistant, method: str, url: URL, data: dict[str, Any]
|
||||
) -> AiohttpClientMockResponse:
|
||||
"""Route Gen4 /device and /fixture requests to their fixtures."""
|
||||
fixture = (
|
||||
"device_gen4.json" if url.path.endswith("/device") else "fixture_gen4.json"
|
||||
)
|
||||
return AiohttpClientMockResponse(
|
||||
method=method,
|
||||
url=url,
|
||||
json=await async_load_json_object_fixture(hass, fixture, DOMAIN),
|
||||
)
|
||||
|
||||
|
||||
async def init_integration_gen4(
|
||||
hass: HomeAssistant,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
skip_setup: bool = False,
|
||||
mock_type: Callable[
|
||||
[HomeAssistant, str, URL, dict[str, Any]],
|
||||
Coroutine[Any, Any, AiohttpClientMockResponse],
|
||||
] = modern_forms_gen4_call_mock,
|
||||
) -> MockConfigEntry:
|
||||
"""Set up the Modern Forms integration against a mock Gen4 device."""
|
||||
aioclient_mock.post("http://192.168.1.123:80/mf", text="", status=404)
|
||||
aioclient_mock.post(
|
||||
"http://192.168.1.123:80/device",
|
||||
side_effect=partial(mock_type, hass),
|
||||
)
|
||||
aioclient_mock.post(
|
||||
"http://192.168.1.123:80/fixture",
|
||||
side_effect=partial(mock_type, hass),
|
||||
)
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data={CONF_HOST: "192.168.1.123", CONF_MAC: "AA:BB:CC:00:11:22"},
|
||||
unique_id="AA:BB:CC:00:11:22",
|
||||
)
|
||||
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
if not skip_setup:
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
return entry
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"systemType": "FAN_G4",
|
||||
"deviceName": "ModernFormsFan",
|
||||
"iotmVer": "01.03.0025",
|
||||
"scmVer": "01.03.3008",
|
||||
"owner": "someone@somewhere.com",
|
||||
"staMac": "AA:BB:CC:00:11:22"
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"fixture": [
|
||||
{
|
||||
"addr": 1,
|
||||
"type": 13,
|
||||
"name": "Fan",
|
||||
"detail": { "model": "2603-56" },
|
||||
"state": {
|
||||
"status": true,
|
||||
"fanSpeed": 3,
|
||||
"fanDirection": false,
|
||||
"wind": false,
|
||||
"windSpeed": 3
|
||||
}
|
||||
},
|
||||
{
|
||||
"addr": 2,
|
||||
"type": 0,
|
||||
"name": "ModernFormsFan Uplight",
|
||||
"detail": { "minColorTemp": 2700, "maxColorTemp": 5000 },
|
||||
"state": { "status": true, "level": 8000, "mixColorTemp": 3000 }
|
||||
},
|
||||
{
|
||||
"addr": 3,
|
||||
"type": 0,
|
||||
"name": "ModernFormsFan Downlight",
|
||||
"detail": { "minColorTemp": 2700, "maxColorTemp": 5000 },
|
||||
"state": { "status": false, "level": 5000, "mixColorTemp": 4000 }
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -77,3 +77,91 @@
|
||||
}),
|
||||
})
|
||||
# ---
|
||||
# name: test_entry_diagnostics_gen4
|
||||
dict({
|
||||
'config_entry': dict({
|
||||
'data': dict({
|
||||
'host': '192.168.1.123',
|
||||
'mac': '**REDACTED**',
|
||||
}),
|
||||
'disabled_by': None,
|
||||
'discovery_keys': dict({
|
||||
}),
|
||||
'domain': 'modern_forms',
|
||||
'minor_version': 1,
|
||||
'options': dict({
|
||||
}),
|
||||
'pref_disable_new_entities': False,
|
||||
'pref_disable_polling': False,
|
||||
'source': 'user',
|
||||
'subentries': list([
|
||||
]),
|
||||
'title': 'Mock Title',
|
||||
'unique_id': 'AA:BB:CC:00:11:22',
|
||||
'version': 1,
|
||||
}),
|
||||
'device': dict({
|
||||
'info': dict({
|
||||
'brand': None,
|
||||
'client_id': '',
|
||||
'date_code': '',
|
||||
'device_name': 'ModernFormsFan',
|
||||
'fan_motor_type': '',
|
||||
'fan_type': '2603-56',
|
||||
'federated_identity': '',
|
||||
'firmware_url': '',
|
||||
'firmware_version': '01.03.0025',
|
||||
'light_type': 'gen4',
|
||||
'mac_address': '**REDACTED**',
|
||||
'main_mcu_firmware_version': '01.03.3008',
|
||||
'owner': '**REDACTED**',
|
||||
'product_sku': '',
|
||||
'production_lot_number': '',
|
||||
}),
|
||||
'status': dict({
|
||||
'adaptive_learning_enabled': False,
|
||||
'away_mode_enabled': False,
|
||||
'decommission': False,
|
||||
'factory_reset': False,
|
||||
'fan_direction': 'forward',
|
||||
'fan_on': True,
|
||||
'fan_sleep_timer': 0,
|
||||
'fan_speed': 3,
|
||||
'fan_timer': None,
|
||||
'light_brightness': 80,
|
||||
'light_color_temp_kelvin': 3000,
|
||||
'light_fixtures': list([
|
||||
dict({
|
||||
'address': 2,
|
||||
'brightness': 80,
|
||||
'color_temp_kelvin': 3000,
|
||||
'fixture_type': 0,
|
||||
'max_color_temp_kelvin': 5000,
|
||||
'min_color_temp_kelvin': 2700,
|
||||
'name': '**REDACTED**',
|
||||
'on': True,
|
||||
}),
|
||||
dict({
|
||||
'address': 3,
|
||||
'brightness': 50,
|
||||
'color_temp_kelvin': 4000,
|
||||
'fixture_type': 0,
|
||||
'max_color_temp_kelvin': 5000,
|
||||
'min_color_temp_kelvin': 2700,
|
||||
'name': '**REDACTED**',
|
||||
'on': False,
|
||||
}),
|
||||
]),
|
||||
'light_on': True,
|
||||
'light_sleep_timer': 0,
|
||||
'light_timer': None,
|
||||
'reset_rf_pair_list': False,
|
||||
'rf_pair_mode_active': False,
|
||||
'schedule': '',
|
||||
'user_data': '',
|
||||
'wind': False,
|
||||
'wind_speed': 3,
|
||||
}),
|
||||
}),
|
||||
})
|
||||
# ---
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Tests for the Modern Forms config flow."""
|
||||
|
||||
from functools import partial
|
||||
from ipaddress import ip_address
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
@@ -13,7 +14,7 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
|
||||
|
||||
from . import init_integration
|
||||
from . import init_integration, modern_forms_gen4_call_mock
|
||||
|
||||
from tests.common import async_load_fixture
|
||||
from tests.test_util.aiohttp import AiohttpClientMocker
|
||||
@@ -233,3 +234,36 @@ async def test_zeroconf_with_mac_device_exists_abort(
|
||||
|
||||
assert result.get("type") is FlowResultType.ABORT
|
||||
assert result.get("reason") == "already_configured"
|
||||
|
||||
|
||||
async def test_full_user_flow_gen4(
|
||||
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
|
||||
) -> None:
|
||||
"""Test the user flow successfully adds a Gen4 fan."""
|
||||
aioclient_mock.post("http://192.168.1.123:80/mf", text="", status=404)
|
||||
aioclient_mock.post(
|
||||
"http://192.168.1.123:80/device",
|
||||
side_effect=partial(modern_forms_gen4_call_mock, hass),
|
||||
)
|
||||
aioclient_mock.post(
|
||||
"http://192.168.1.123:80/fixture",
|
||||
side_effect=partial(modern_forms_gen4_call_mock, hass),
|
||||
)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_USER},
|
||||
)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.modern_forms.async_setup_entry",
|
||||
return_value=True,
|
||||
):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={CONF_HOST: "192.168.1.123"}
|
||||
)
|
||||
|
||||
assert result2.get("type") is FlowResultType.CREATE_ENTRY
|
||||
assert result2.get("title") == "ModernFormsFan"
|
||||
assert result2["data"][CONF_HOST] == "192.168.1.123"
|
||||
assert result2["data"][CONF_MAC] == "AA:BB:CC:00:11:22"
|
||||
|
||||
@@ -5,7 +5,7 @@ from syrupy.filters import props
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from . import init_integration
|
||||
from . import init_integration, init_integration_gen4
|
||||
|
||||
from tests.components.diagnostics import get_diagnostics_for_config_entry
|
||||
from tests.test_util.aiohttp import AiohttpClientMocker
|
||||
@@ -24,3 +24,21 @@ async def test_entry_diagnostics(
|
||||
result = await get_diagnostics_for_config_entry(hass, hass_client, entry)
|
||||
|
||||
assert result == snapshot(exclude=props("created_at", "modified_at", "entry_id"))
|
||||
|
||||
|
||||
async def test_entry_diagnostics_gen4(
|
||||
hass: HomeAssistant,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
hass_client: ClientSessionGenerator,
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> None:
|
||||
"""Test Gen4 fixture names are redacted from diagnostics."""
|
||||
entry = await init_integration_gen4(hass, aioclient_mock)
|
||||
|
||||
result = await get_diagnostics_for_config_entry(hass, hass_client, entry)
|
||||
|
||||
fixture_names = [
|
||||
fixture["name"] for fixture in result["device"]["status"]["light_fixtures"]
|
||||
]
|
||||
assert fixture_names == ["**REDACTED**", "**REDACTED**"]
|
||||
assert result == snapshot(exclude=props("created_at", "modified_at", "entry_id"))
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
"""Tests for the Modern Forms light platform."""
|
||||
|
||||
from typing import Any
|
||||
from unittest.mock import patch
|
||||
|
||||
from aiomodernforms import ModernFormsConnectionError
|
||||
import pytest
|
||||
from yarl import URL
|
||||
|
||||
from homeassistant.components.light import ATTR_BRIGHTNESS, DOMAIN as LIGHT_DOMAIN
|
||||
from homeassistant.components.modern_forms.const import (
|
||||
@@ -17,6 +19,7 @@ from homeassistant.const import (
|
||||
ATTR_FRIENDLY_NAME,
|
||||
SERVICE_TURN_OFF,
|
||||
SERVICE_TURN_ON,
|
||||
STATE_OFF,
|
||||
STATE_ON,
|
||||
STATE_UNAVAILABLE,
|
||||
)
|
||||
@@ -24,9 +27,10 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from . import init_integration
|
||||
from . import init_integration, init_integration_gen4, modern_forms_gen4_call_mock
|
||||
|
||||
from tests.test_util.aiohttp import AiohttpClientMocker
|
||||
from tests.common import async_load_json_object_fixture
|
||||
from tests.test_util.aiohttp import AiohttpClientMocker, AiohttpClientMockResponse
|
||||
|
||||
|
||||
async def test_light_state(
|
||||
@@ -164,3 +168,186 @@ async def test_light_connection_error(
|
||||
|
||||
state = hass.states.get("light.modernformsfan_light")
|
||||
assert state.state == STATE_UNAVAILABLE
|
||||
|
||||
|
||||
async def test_light_state_gen4(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
) -> None:
|
||||
"""Test a multi-fixture Gen4 fan creates one light entity per fixture."""
|
||||
await init_integration_gen4(hass, aioclient_mock)
|
||||
|
||||
state = hass.states.get("light.modernformsfan_uplight")
|
||||
assert state
|
||||
assert state.attributes.get(ATTR_FRIENDLY_NAME) == "ModernFormsFan Uplight"
|
||||
assert state.state == STATE_ON
|
||||
assert state.attributes.get(ATTR_BRIGHTNESS) == 204
|
||||
|
||||
entry = entity_registry.async_get("light.modernformsfan_uplight")
|
||||
assert entry
|
||||
assert entry.unique_id == "AA:BB:CC:00:11:22_2"
|
||||
|
||||
state = hass.states.get("light.modernformsfan_downlight")
|
||||
assert state
|
||||
assert state.attributes.get(ATTR_FRIENDLY_NAME) == "ModernFormsFan Downlight"
|
||||
assert state.state == STATE_OFF
|
||||
assert state.attributes.get(ATTR_BRIGHTNESS) is None
|
||||
|
||||
entry = entity_registry.async_get("light.modernformsfan_downlight")
|
||||
assert entry
|
||||
assert entry.unique_id == "AA:BB:CC:00:11:22_3"
|
||||
|
||||
|
||||
async def test_light_name_requires_word_boundary_gen4(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
) -> None:
|
||||
"""Test a fixture name isn't stripped without a real device-name boundary."""
|
||||
|
||||
async def partial_word_name_mock(
|
||||
hass: HomeAssistant, method: str, url: URL, data: dict[str, Any]
|
||||
) -> AiohttpClientMockResponse:
|
||||
"""Serve the normal Gen4 fixtures, with the uplight renamed."""
|
||||
if not url.path.endswith("/fixture"):
|
||||
return await modern_forms_gen4_call_mock(hass, method, url, data)
|
||||
payload = await async_load_json_object_fixture(
|
||||
hass, "fixture_gen4.json", DOMAIN
|
||||
)
|
||||
for fixture in payload["fixture"]:
|
||||
if fixture["addr"] == 2:
|
||||
fixture["name"] = "ModernFormsFancy Light"
|
||||
return AiohttpClientMockResponse(method=method, url=url, json=payload)
|
||||
|
||||
await init_integration_gen4(hass, aioclient_mock, mock_type=partial_word_name_mock)
|
||||
|
||||
entity_id = entity_registry.async_get_entity_id(
|
||||
LIGHT_DOMAIN, DOMAIN, "AA:BB:CC:00:11:22_2"
|
||||
)
|
||||
assert entity_id
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
assert (
|
||||
state.attributes.get(ATTR_FRIENDLY_NAME)
|
||||
== "ModernFormsFan ModernFormsFancy Light"
|
||||
)
|
||||
|
||||
|
||||
async def test_light_name_falls_back_to_device_name_gen4(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
) -> None:
|
||||
"""Test a fixture named exactly like the device falls back to that name alone."""
|
||||
|
||||
async def exact_match_name_mock(
|
||||
hass: HomeAssistant, method: str, url: URL, data: dict[str, Any]
|
||||
) -> AiohttpClientMockResponse:
|
||||
"""Serve the normal Gen4 fixtures, with the uplight renamed."""
|
||||
if not url.path.endswith("/fixture"):
|
||||
return await modern_forms_gen4_call_mock(hass, method, url, data)
|
||||
payload = await async_load_json_object_fixture(
|
||||
hass, "fixture_gen4.json", DOMAIN
|
||||
)
|
||||
for fixture in payload["fixture"]:
|
||||
if fixture["addr"] == 2:
|
||||
fixture["name"] = "ModernFormsFan"
|
||||
return AiohttpClientMockResponse(method=method, url=url, json=payload)
|
||||
|
||||
await init_integration_gen4(hass, aioclient_mock, mock_type=exact_match_name_mock)
|
||||
|
||||
entity_id = entity_registry.async_get_entity_id(
|
||||
LIGHT_DOMAIN, DOMAIN, "AA:BB:CC:00:11:22_2"
|
||||
)
|
||||
assert entity_id
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
assert state.attributes.get(ATTR_FRIENDLY_NAME) == "ModernFormsFan"
|
||||
|
||||
|
||||
async def test_light_unavailable_when_fixture_disappears_gen4(
|
||||
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
|
||||
) -> None:
|
||||
"""Test a Gen4 light entity goes unavailable if its fixture disappears."""
|
||||
removed_addresses: set[int] = set()
|
||||
|
||||
async def fixture_removal_mock(
|
||||
hass: HomeAssistant, method: str, url: URL, data: dict[str, Any]
|
||||
) -> AiohttpClientMockResponse:
|
||||
"""Serve the normal Gen4 fixtures, minus any addresses removed."""
|
||||
if not url.path.endswith("/fixture") or not removed_addresses:
|
||||
return await modern_forms_gen4_call_mock(hass, method, url, data)
|
||||
payload = await async_load_json_object_fixture(
|
||||
hass, "fixture_gen4.json", DOMAIN
|
||||
)
|
||||
payload["fixture"] = [
|
||||
fixture
|
||||
for fixture in payload["fixture"]
|
||||
if fixture["addr"] not in removed_addresses
|
||||
]
|
||||
return AiohttpClientMockResponse(method=method, url=url, json=payload)
|
||||
|
||||
entry = await init_integration_gen4(
|
||||
hass, aioclient_mock, mock_type=fixture_removal_mock
|
||||
)
|
||||
|
||||
state = hass.states.get("light.modernformsfan_uplight")
|
||||
assert state
|
||||
assert state.state == STATE_ON
|
||||
|
||||
removed_addresses.add(2)
|
||||
await entry.runtime_data.async_refresh()
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("light.modernformsfan_uplight")
|
||||
assert state
|
||||
assert state.state == STATE_UNAVAILABLE
|
||||
|
||||
|
||||
async def test_light_change_state_gen4(
|
||||
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
|
||||
) -> None:
|
||||
"""Test Gen4 fixture entities control via light_fixture(), not light()."""
|
||||
await init_integration_gen4(hass, aioclient_mock)
|
||||
|
||||
with (
|
||||
patch("aiomodernforms.ModernFormsDevice.light_fixture") as light_fixture_mock,
|
||||
patch("aiomodernforms.ModernFormsDevice.light") as light_mock,
|
||||
):
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
SERVICE_TURN_OFF,
|
||||
{ATTR_ENTITY_ID: "light.modernformsfan_uplight"},
|
||||
blocking=True,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
light_fixture_mock.assert_called_once_with(2, on=False)
|
||||
light_mock.assert_not_called()
|
||||
|
||||
|
||||
async def test_sleep_timer_services_gen4(
|
||||
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
|
||||
) -> None:
|
||||
"""Test Gen4 sleep timer services pass sleep through light_fixture()."""
|
||||
await init_integration_gen4(hass, aioclient_mock)
|
||||
|
||||
with patch("aiomodernforms.ModernFormsDevice.light_fixture") as light_fixture_mock:
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_SET_LIGHT_SLEEP_TIMER,
|
||||
{ATTR_ENTITY_ID: "light.modernformsfan_uplight", ATTR_SLEEP_TIME: 1},
|
||||
blocking=True,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
light_fixture_mock.assert_called_once_with(2, sleep=60)
|
||||
|
||||
with patch("aiomodernforms.ModernFormsDevice.light_fixture") as light_fixture_mock:
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_CLEAR_LIGHT_SLEEP_TIMER,
|
||||
{ATTR_ENTITY_ID: "light.modernformsfan_uplight"},
|
||||
blocking=True,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
light_fixture_mock.assert_called_once_with(2, sleep=0)
|
||||
|
||||
Reference in New Issue
Block a user