From 2f6bf0816520004f464c06c7ac2a46cedefdb57d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 30 Jan 2022 22:24:42 -0600 Subject: [PATCH] Fix senseme fan lights (#65217) --- homeassistant/components/senseme/light.py | 78 ++++++++++------ tests/components/senseme/__init__.py | 74 ++++++++++------ tests/components/senseme/test_light.py | 103 ++++++++++++++++++++++ 3 files changed, 200 insertions(+), 55 deletions(-) create mode 100644 tests/components/senseme/test_light.py diff --git a/homeassistant/components/senseme/light.py b/homeassistant/components/senseme/light.py index 2a4d82de6d0..75d853c4001 100644 --- a/homeassistant/components/senseme/light.py +++ b/homeassistant/components/senseme/light.py @@ -31,50 +31,30 @@ async def async_setup_entry( ) -> None: """Set up SenseME lights.""" device = hass.data[DOMAIN][entry.entry_id] - if device.has_light: - async_add_entities([HASensemeLight(device)]) + if not device.has_light: + return + if device.is_light: + async_add_entities([HASensemeStandaloneLight(device)]) + else: + async_add_entities([HASensemeFanLight(device)]) class HASensemeLight(SensemeEntity, LightEntity): """Representation of a Big Ass Fans SenseME light.""" - def __init__(self, device: SensemeDevice) -> None: + def __init__(self, device: SensemeDevice, name: str) -> None: """Initialize the entity.""" - self._device = device - if device.is_light: - name = device.name # The device itself is a light - else: - name = f"{device.name} Light" # A fan light super().__init__(device, name) - if device.is_light: - self._attr_supported_color_modes = {COLOR_MODE_COLOR_TEMP} - self._attr_color_mode = COLOR_MODE_COLOR_TEMP - else: - self._attr_supported_color_modes = {COLOR_MODE_BRIGHTNESS} - self._attr_color_mode = COLOR_MODE_BRIGHTNESS - self._attr_unique_id = f"{self._device.uuid}-LIGHT" # for legacy compat - self._attr_min_mireds = color_temperature_kelvin_to_mired( - self._device.light_color_temp_max - ) - self._attr_max_mireds = color_temperature_kelvin_to_mired( - self._device.light_color_temp_min - ) + self._attr_unique_id = f"{device.uuid}-LIGHT" # for legacy compat @callback def _async_update_attrs(self) -> None: """Update attrs from device.""" self._attr_is_on = self._device.light_on self._attr_brightness = int(min(255, self._device.light_brightness * 16)) - self._attr_color_temp = color_temperature_kelvin_to_mired( - self._device.light_color_temp - ) async def async_turn_on(self, **kwargs: Any) -> None: """Turn on the light.""" - if (color_temp := kwargs.get(ATTR_COLOR_TEMP)) is not None: - self._device.light_color_temp = color_temperature_mired_to_kelvin( - color_temp - ) if (brightness := kwargs.get(ATTR_BRIGHTNESS)) is not None: # set the brightness, which will also turn on/off light if brightness == 255: @@ -86,3 +66,45 @@ class HASensemeLight(SensemeEntity, LightEntity): async def async_turn_off(self, **kwargs: Any) -> None: """Turn off the light.""" self._device.light_on = False + + +class HASensemeFanLight(HASensemeLight): + """Representation of a Big Ass Fans SenseME light on a fan.""" + + def __init__(self, device: SensemeDevice) -> None: + """Init a fan light.""" + super().__init__(device, device.name) + self._attr_supported_color_modes = {COLOR_MODE_BRIGHTNESS} + self._attr_color_mode = COLOR_MODE_BRIGHTNESS + + +class HASensemeStandaloneLight(HASensemeLight): + """Representation of a Big Ass Fans SenseME light.""" + + def __init__(self, device: SensemeDevice) -> None: + """Init a standalone light.""" + super().__init__(device, f"{device.name} Light") + self._attr_supported_color_modes = {COLOR_MODE_COLOR_TEMP} + self._attr_color_mode = COLOR_MODE_COLOR_TEMP + self._attr_min_mireds = color_temperature_kelvin_to_mired( + device.light_color_temp_max + ) + self._attr_max_mireds = color_temperature_kelvin_to_mired( + device.light_color_temp_min + ) + + @callback + def _async_update_attrs(self) -> None: + """Update attrs from device.""" + super()._async_update_attrs() + self._attr_color_temp = color_temperature_kelvin_to_mired( + self._device.light_color_temp + ) + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn on the light.""" + if (color_temp := kwargs.get(ATTR_COLOR_TEMP)) is not None: + self._device.light_color_temp = color_temperature_mired_to_kelvin( + color_temp + ) + await super().async_turn_on(**kwargs) diff --git a/tests/components/senseme/__init__.py b/tests/components/senseme/__init__.py index 5c586d88fd5..8c9a7669889 100644 --- a/tests/components/senseme/__init__.py +++ b/tests/components/senseme/__init__.py @@ -12,32 +12,38 @@ MOCK_UUID = "77a6b7b3-925d-4695-a415-76d76dca4444" MOCK_ADDRESS = "127.0.0.1" MOCK_MAC = "20:F8:5E:92:5A:75" -device = MagicMock(auto_spec=SensemeDevice) -device.async_update = AsyncMock() -device.model = "Haiku Fan" -device.fan_speed_max = 7 -device.mac = "aa:bb:cc:dd:ee:ff" -device.fan_dir = "REV" -device.room_name = "Main" -device.room_type = "Main" -device.fw_version = "1" -device.fan_autocomfort = "on" -device.fan_smartmode = "on" -device.fan_whoosh_mode = "on" -device.name = MOCK_NAME -device.uuid = MOCK_UUID -device.address = MOCK_ADDRESS -device.get_device_info = { - "name": MOCK_NAME, - "uuid": MOCK_UUID, - "mac": MOCK_ADDRESS, - "address": MOCK_ADDRESS, - "base_model": "FAN,HAIKU,HSERIES", - "has_light": False, - "has_sensor": True, - "is_fan": True, - "is_light": False, -} + +def _mock_device(): + device = MagicMock(auto_spec=SensemeDevice) + device.async_update = AsyncMock() + device.model = "Haiku Fan" + device.fan_speed_max = 7 + device.mac = "aa:bb:cc:dd:ee:ff" + device.fan_dir = "REV" + device.has_light = True + device.is_light = False + device.light_brightness = 50 + device.room_name = "Main" + device.room_type = "Main" + device.fw_version = "1" + device.fan_autocomfort = "COOLING" + device.fan_smartmode = "OFF" + device.fan_whoosh_mode = "on" + device.name = MOCK_NAME + device.uuid = MOCK_UUID + device.address = MOCK_ADDRESS + device.get_device_info = { + "name": MOCK_NAME, + "uuid": MOCK_UUID, + "mac": MOCK_ADDRESS, + "address": MOCK_ADDRESS, + "base_model": "FAN,HAIKU,HSERIES", + "has_light": False, + "has_sensor": True, + "is_fan": True, + "is_light": False, + } + return device device_alternate_ip = MagicMock(auto_spec=SensemeDevice) @@ -99,7 +105,7 @@ device_no_uuid = MagicMock(auto_spec=SensemeDevice) device_no_uuid.uuid = None -MOCK_DEVICE = device +MOCK_DEVICE = _mock_device() MOCK_DEVICE_ALTERNATE_IP = device_alternate_ip MOCK_DEVICE2 = device2 MOCK_DEVICE_NO_UUID = device_no_uuid @@ -121,3 +127,17 @@ def _patch_discovery(device=None, no_device=None): yield return _patcher() + + +def _patch_device(device=None, no_device=False): + async def _device_mocker(*args, **kwargs): + if no_device: + return False, None + if device: + return True, device + return True, _mock_device() + + return patch( + "homeassistant.components.senseme.async_get_device_by_device_info", + new=_device_mocker, + ) diff --git a/tests/components/senseme/test_light.py b/tests/components/senseme/test_light.py new file mode 100644 index 00000000000..21811452610 --- /dev/null +++ b/tests/components/senseme/test_light.py @@ -0,0 +1,103 @@ +"""Tests for senseme light platform.""" + + +from aiosenseme import SensemeDevice + +from homeassistant.components import senseme +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_COLOR_MODE, + ATTR_COLOR_TEMP, + ATTR_SUPPORTED_COLOR_MODES, + COLOR_MODE_BRIGHTNESS, + COLOR_MODE_COLOR_TEMP, + DOMAIN as LIGHT_DOMAIN, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, +) +from homeassistant.components.senseme.const import DOMAIN +from homeassistant.const import ATTR_ENTITY_ID, STATE_ON +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er +from homeassistant.setup import async_setup_component + +from . import _mock_device, _patch_device, _patch_discovery + +from tests.common import MockConfigEntry + + +async def _setup_mocked_entry(hass: HomeAssistant, device: SensemeDevice) -> None: + """Set up a mocked entry.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={"info": device.get_device_info}, + unique_id=device.uuid, + ) + entry.add_to_hass(hass) + with _patch_discovery(), _patch_device(device=device): + await async_setup_component(hass, senseme.DOMAIN, {senseme.DOMAIN: {}}) + await hass.async_block_till_done() + + +async def test_light_unique_id(hass: HomeAssistant) -> None: + """Test a light unique id.""" + device = _mock_device() + await _setup_mocked_entry(hass, device) + entity_id = "light.haiku_fan" + entity_registry = er.async_get(hass) + assert entity_registry.async_get(entity_id).unique_id == f"{device.uuid}-LIGHT" + state = hass.states.get(entity_id) + assert state.state == STATE_ON + + +async def test_fan_light(hass: HomeAssistant) -> None: + """Test a fan light.""" + device = _mock_device() + await _setup_mocked_entry(hass, device) + entity_id = "light.haiku_fan" + + state = hass.states.get(entity_id) + assert state.state == STATE_ON + attributes = state.attributes + assert attributes[ATTR_BRIGHTNESS] == 255 + assert attributes[ATTR_COLOR_MODE] == COLOR_MODE_BRIGHTNESS + assert attributes[ATTR_SUPPORTED_COLOR_MODES] == [COLOR_MODE_BRIGHTNESS] + + await hass.services.async_call( + LIGHT_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + assert device.light_on is False + + await hass.services.async_call( + LIGHT_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + assert device.light_on is True + + +async def test_standalone_light(hass: HomeAssistant) -> None: + """Test a standalone light.""" + device = _mock_device() + device.is_light = True + device.light_color_temp_max = 6500 + device.light_color_temp_min = 2700 + device.light_color_temp = 4000 + await _setup_mocked_entry(hass, device) + entity_id = "light.haiku_fan_light" + + state = hass.states.get(entity_id) + assert state.state == STATE_ON + attributes = state.attributes + assert attributes[ATTR_BRIGHTNESS] == 255 + assert attributes[ATTR_COLOR_MODE] == COLOR_MODE_COLOR_TEMP + assert attributes[ATTR_SUPPORTED_COLOR_MODES] == [COLOR_MODE_COLOR_TEMP] + assert attributes[ATTR_COLOR_TEMP] == 250 + + await hass.services.async_call( + LIGHT_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + assert device.light_on is False + + await hass.services.async_call( + LIGHT_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + assert device.light_on is True