diff --git a/homeassistant/components/flux_led/__init__.py b/homeassistant/components/flux_led/__init__.py index 3898d954692..ddd7a0bdbe8 100644 --- a/homeassistant/components/flux_led/__init__.py +++ b/homeassistant/components/flux_led/__init__.py @@ -49,6 +49,7 @@ PLATFORMS_BY_TYPE: Final = { Platform.LIGHT, Platform.NUMBER, Platform.SELECT, + Platform.SENSOR, Platform.SWITCH, ], DeviceType.Switch: [Platform.BUTTON, Platform.SELECT, Platform.SWITCH], diff --git a/homeassistant/components/flux_led/button.py b/homeassistant/components/flux_led/button.py index c6369373aa4..3f74090f075 100644 --- a/homeassistant/components/flux_led/button.py +++ b/homeassistant/components/flux_led/button.py @@ -2,9 +2,14 @@ from __future__ import annotations from flux_led.aio import AIOWifiLedBulb +from flux_led.protocol import RemoteConfig from homeassistant import config_entries -from homeassistant.components.button import ButtonEntity +from homeassistant.components.button import ( + ButtonDeviceClass, + ButtonEntity, + ButtonEntityDescription, +) from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import EntityCategory @@ -14,6 +19,16 @@ from .const import DOMAIN from .coordinator import FluxLedUpdateCoordinator from .entity import FluxBaseEntity +_RESTART_KEY = "restart" +_UNPAIR_REMOTES_KEY = "unpair_remotes" + +RESTART_BUTTON_DESCRIPTION = ButtonEntityDescription( + key=_RESTART_KEY, name="Restart", device_class=ButtonDeviceClass.RESTART +) +UNPAIR_REMOTES_DESCRIPTION = ButtonEntityDescription( + key=_UNPAIR_REMOTES_KEY, name="Unpair Remotes", icon="mdi:remote-off" +) + async def async_setup_entry( hass: HomeAssistant, @@ -22,11 +37,20 @@ async def async_setup_entry( ) -> None: """Set up Magic Home button based on a config entry.""" coordinator: FluxLedUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - async_add_entities([FluxRestartButton(coordinator.device, entry)]) + device = coordinator.device + entities: list[FluxButton] = [ + FluxButton(coordinator.device, entry, RESTART_BUTTON_DESCRIPTION) + ] + if device.paired_remotes is not None: + entities.append( + FluxButton(coordinator.device, entry, UNPAIR_REMOTES_DESCRIPTION) + ) + + async_add_entities(entities) -class FluxRestartButton(FluxBaseEntity, ButtonEntity): - """Representation of a Flux restart button.""" +class FluxButton(FluxBaseEntity, ButtonEntity): + """Representation of a Flux button.""" _attr_entity_category = EntityCategory.CONFIG @@ -34,13 +58,19 @@ class FluxRestartButton(FluxBaseEntity, ButtonEntity): self, device: AIOWifiLedBulb, entry: config_entries.ConfigEntry, + description: ButtonEntityDescription, ) -> None: - """Initialize the reboot button.""" + """Initialize the button.""" + self.entity_description = description super().__init__(device, entry) - self._attr_name = f"{entry.data[CONF_NAME]} Restart" + self._attr_name = f"{entry.data[CONF_NAME]} {description.name}" if entry.unique_id: - self._attr_unique_id = f"{entry.unique_id}_restart" + self._attr_unique_id = f"{entry.unique_id}_{description.key}" async def async_press(self) -> None: - """Send out a restart command.""" - await self._device.async_reboot() + """Send out a command.""" + if self.entity_description.key == _RESTART_KEY: + await self._device.async_reboot() + else: + await self._device.async_unpair_remotes() + await self._device.async_config_remotes(RemoteConfig.OPEN) diff --git a/homeassistant/components/flux_led/select.py b/homeassistant/components/flux_led/select.py index 4e34382cc7b..be99148a72a 100644 --- a/homeassistant/components/flux_led/select.py +++ b/homeassistant/components/flux_led/select.py @@ -3,7 +3,7 @@ from __future__ import annotations from flux_led.aio import AIOWifiLedBulb from flux_led.base_device import DeviceType -from flux_led.protocol import PowerRestoreState +from flux_led.protocol import PowerRestoreState, RemoteConfig from homeassistant import config_entries from homeassistant.components.select import SelectEntity @@ -30,6 +30,7 @@ async def async_setup_entry( | FluxOperatingModesSelect | FluxWiringsSelect | FluxICTypeSelect + | FluxRemoteConfigSelect ] = [] name = entry.data[CONF_NAME] unique_id = entry.unique_id @@ -50,6 +51,12 @@ async def async_setup_entry( entities.append( FluxICTypeSelect(coordinator, unique_id, f"{name} IC Type", "ic_type") ) + if device.remote_config: + entities.append( + FluxRemoteConfigSelect( + coordinator, unique_id, f"{name} Remote Config", "remote_config" + ) + ) if entities: async_add_entities(entities) @@ -165,3 +172,33 @@ class FluxOperatingModesSelect(FluxConfigSelect): self.hass.async_create_task( self.hass.config_entries.async_reload(self.coordinator.entry.entry_id) ) + + +class FluxRemoteConfigSelect(FluxConfigSelect): + """Representation of Flux remote config type.""" + + def __init__( + self, + coordinator: FluxLedUpdateCoordinator, + unique_id: str | None, + name: str, + key: str, + ) -> None: + """Initialize the remote config type select.""" + super().__init__(coordinator, unique_id, name, key) + assert self._device.remote_config is not None + self._name_to_state = { + _human_readable_option(option.name): option for option in RemoteConfig + } + self._attr_options = list(self._name_to_state) + + @property + def current_option(self) -> str | None: + """Return the current remote config.""" + assert self._device.remote_config is not None + return _human_readable_option(self._device.remote_config.name) + + async def async_select_option(self, option: str) -> None: + """Change the remote config setting.""" + remote_config: RemoteConfig = self._name_to_state[option] + await self._device.async_config_remotes(remote_config) diff --git a/homeassistant/components/flux_led/sensor.py b/homeassistant/components/flux_led/sensor.py new file mode 100644 index 00000000000..6d67ced1fe2 --- /dev/null +++ b/homeassistant/components/flux_led/sensor.py @@ -0,0 +1,46 @@ +"""Support for Magic Home sensors.""" +from __future__ import annotations + +from homeassistant import config_entries +from homeassistant.components.sensor import SensorEntity +from homeassistant.const import CONF_NAME +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .coordinator import FluxLedUpdateCoordinator +from .entity import FluxEntity + + +async def async_setup_entry( + hass: HomeAssistant, + entry: config_entries.ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Magic Home sensors.""" + coordinator: FluxLedUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + if coordinator.device.paired_remotes is not None: + async_add_entities( + [ + FluxPairedRemotes( + coordinator, + entry.unique_id, + f"{entry.data[CONF_NAME]} Paired Remotes", + "paired_remotes", + ) + ] + ) + + +class FluxPairedRemotes(FluxEntity, SensorEntity): + """Representation of a Magic Home paired remotes sensor.""" + + _attr_icon = "mdi:remote" + _attr_entity_category = EntityCategory.CONFIG + + @property + def native_value(self) -> int: + """Return the number of paired remotes.""" + assert self._device.paired_remotes is not None + return self._device.paired_remotes diff --git a/tests/components/flux_led/__init__.py b/tests/components/flux_led/__init__.py index e6a76e08db9..96fc0b78bfd 100644 --- a/tests/components/flux_led/__init__.py +++ b/tests/components/flux_led/__init__.py @@ -14,18 +14,28 @@ from flux_led.const import ( COLOR_MODE_RGB as FLUX_COLOR_MODE_RGB, ) from flux_led.models_db import MODEL_MAP -from flux_led.protocol import LEDENETRawState, PowerRestoreState, PowerRestoreStates +from flux_led.protocol import ( + LEDENETRawState, + PowerRestoreState, + PowerRestoreStates, + RemoteConfig, +) from flux_led.scanner import FluxLEDDiscovery from homeassistant.components import dhcp +from homeassistant.components.flux_led.const import DOMAIN +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST, CONF_NAME from homeassistant.core import HomeAssistant +from tests.common import MockConfigEntry + MODULE = "homeassistant.components.flux_led" MODULE_CONFIG_FLOW = "homeassistant.components.flux_led.config_flow" IP_ADDRESS = "127.0.0.1" MODEL_NUM_HEX = "0x35" MODEL_NUM = 0x35 -MODEL = "AZ120444" +MODEL = "AK001-ZJ2149" MODEL_DESCRIPTION = "Bulb RGBCW" MAC_ADDRESS = "aa:bb:cc:dd:ee:ff" FLUX_MAC_ADDRESS = "AABBCCDDEEFF" @@ -64,6 +74,16 @@ FLUX_DISCOVERY = FluxLEDDiscovery( ) +def _mock_config_entry_for_bulb(hass: HomeAssistant) -> ConfigEntry: + config_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: IP_ADDRESS, CONF_NAME: DEFAULT_ENTRY_TITLE}, + unique_id=MAC_ADDRESS, + ) + config_entry.add_to_hass(hass) + return config_entry + + def _mocked_bulb() -> AIOWifiLedBulb: bulb = MagicMock(auto_spec=AIOWifiLedBulb) @@ -74,6 +94,8 @@ def _mocked_bulb() -> AIOWifiLedBulb: bulb.requires_turn_on = True bulb.async_setup = AsyncMock(side_effect=_save_setup_callback) bulb.effect_list = ["some_effect"] + bulb.remote_config = RemoteConfig.OPEN + bulb.async_unpair_remotes = AsyncMock() bulb.async_set_time = AsyncMock() bulb.async_set_music_mode = AsyncMock() bulb.async_set_custom_pattern = AsyncMock() @@ -82,6 +104,8 @@ def _mocked_bulb() -> AIOWifiLedBulb: bulb.async_set_white_temp = AsyncMock() bulb.async_set_brightness = AsyncMock() bulb.async_set_device_config = AsyncMock() + bulb.async_config_remotes = AsyncMock() + bulb.paired_remotes = 2 bulb.pixels_per_segment = 300 bulb.segments = 2 bulb.music_pixels_per_segment = 150 diff --git a/tests/components/flux_led/test_button.py b/tests/components/flux_led/test_button.py index 1117373fcd6..992d8b18ce6 100644 --- a/tests/components/flux_led/test_button.py +++ b/tests/components/flux_led/test_button.py @@ -8,8 +8,11 @@ from homeassistant.setup import async_setup_component from . import ( DEFAULT_ENTRY_TITLE, + FLUX_DISCOVERY, IP_ADDRESS, MAC_ADDRESS, + _mock_config_entry_for_bulb, + _mocked_bulb, _mocked_switch, _patch_discovery, _patch_wifibulb, @@ -18,7 +21,7 @@ from . import ( from tests.common import MockConfigEntry -async def test_switch_reboot(hass: HomeAssistant) -> None: +async def test_button_reboot(hass: HomeAssistant) -> None: """Test a smart plug can be rebooted.""" config_entry = MockConfigEntry( domain=DOMAIN, @@ -39,3 +42,21 @@ async def test_switch_reboot(hass: HomeAssistant) -> None: BUTTON_DOMAIN, "press", {ATTR_ENTITY_ID: entity_id}, blocking=True ) switch.async_reboot.assert_called_once() + + +async def test_button_unpair_remotes(hass: HomeAssistant) -> None: + """Test that remotes can be unpaired.""" + _mock_config_entry_for_bulb(hass) + bulb = _mocked_bulb() + bulb.discovery = FLUX_DISCOVERY + with _patch_discovery(device=FLUX_DISCOVERY), _patch_wifibulb(device=bulb): + await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "button.bulb_rgbcw_ddeeff_unpair_remotes" + assert hass.states.get(entity_id) + + await hass.services.async_call( + BUTTON_DOMAIN, "press", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + bulb.async_unpair_remotes.assert_called_once() diff --git a/tests/components/flux_led/test_select.py b/tests/components/flux_led/test_select.py index 5a6faeaa6dc..ee58a2660e3 100644 --- a/tests/components/flux_led/test_select.py +++ b/tests/components/flux_led/test_select.py @@ -1,7 +1,7 @@ """Tests for select platform.""" from unittest.mock import patch -from flux_led.protocol import PowerRestoreState +from flux_led.protocol import PowerRestoreState, RemoteConfig import pytest from homeassistant.components import flux_led @@ -13,8 +13,10 @@ from homeassistant.setup import async_setup_component from . import ( DEFAULT_ENTRY_TITLE, + FLUX_DISCOVERY, IP_ADDRESS, MAC_ADDRESS, + _mock_config_entry_for_bulb, _mocked_bulb, _mocked_switch, _patch_discovery, @@ -147,3 +149,45 @@ async def test_select_mutable_0x25_strip_config(hass: HomeAssistant) -> None: await hass.async_block_till_done() bulb.async_set_device_config.assert_called_once_with(operating_mode="CCT") assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_select_24ghz_remote_config(hass: HomeAssistant) -> None: + """Test selecting 2.4ghz remote config.""" + _mock_config_entry_for_bulb(hass) + bulb = _mocked_bulb() + bulb.discovery = FLUX_DISCOVERY + with _patch_discovery(device=FLUX_DISCOVERY), _patch_wifibulb(device=bulb): + await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) + await hass.async_block_till_done() + + remote_config_entity_id = "select.bulb_rgbcw_ddeeff_remote_config" + state = hass.states.get(remote_config_entity_id) + assert state.state == "Open" + + with pytest.raises(ValueError): + await hass.services.async_call( + SELECT_DOMAIN, + "select_option", + {ATTR_ENTITY_ID: remote_config_entity_id, ATTR_OPTION: "INVALID"}, + blocking=True, + ) + + bulb.remote_config = RemoteConfig.DISABLED + await hass.services.async_call( + SELECT_DOMAIN, + "select_option", + {ATTR_ENTITY_ID: remote_config_entity_id, ATTR_OPTION: "Disabled"}, + blocking=True, + ) + bulb.async_config_remotes.assert_called_once_with(RemoteConfig.DISABLED) + bulb.async_config_remotes.reset_mock() + + bulb.remote_config = RemoteConfig.PAIRED_ONLY + await hass.services.async_call( + SELECT_DOMAIN, + "select_option", + {ATTR_ENTITY_ID: remote_config_entity_id, ATTR_OPTION: "Paired Only"}, + blocking=True, + ) + bulb.async_config_remotes.assert_called_once_with(RemoteConfig.PAIRED_ONLY) + bulb.async_config_remotes.reset_mock() diff --git a/tests/components/flux_led/test_sensor.py b/tests/components/flux_led/test_sensor.py new file mode 100644 index 00000000000..b06a6330fde --- /dev/null +++ b/tests/components/flux_led/test_sensor.py @@ -0,0 +1,25 @@ +"""Tests for flux_led sensor platform.""" +from homeassistant.components import flux_led +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from . import ( + FLUX_DISCOVERY, + _mock_config_entry_for_bulb, + _mocked_bulb, + _patch_discovery, + _patch_wifibulb, +) + + +async def test_paired_remotes_sensor(hass: HomeAssistant) -> None: + """Test that the paired remotes sensor has the correct value.""" + _mock_config_entry_for_bulb(hass) + bulb = _mocked_bulb() + bulb.discovery = FLUX_DISCOVERY + with _patch_discovery(device=FLUX_DISCOVERY), _patch_wifibulb(device=bulb): + await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "sensor.bulb_rgbcw_ddeeff_paired_remotes" + assert hass.states.get(entity_id).state == "2"