diff --git a/.strict-typing b/.strict-typing index 2566a5349c2..c8aa9878413 100644 --- a/.strict-typing +++ b/.strict-typing @@ -139,6 +139,7 @@ homeassistant.components.cpuspeed.* homeassistant.components.crownstone.* homeassistant.components.date.* homeassistant.components.datetime.* +homeassistant.components.deako.* homeassistant.components.deconz.* homeassistant.components.default_config.* homeassistant.components.demo.* diff --git a/CODEOWNERS b/CODEOWNERS index 6f118ca1ba8..990ed679d2b 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -294,6 +294,8 @@ build.json @home-assistant/supervisor /tests/components/date/ @home-assistant/core /homeassistant/components/datetime/ @home-assistant/core /tests/components/datetime/ @home-assistant/core +/homeassistant/components/deako/ @sebirdman @balake @deakolights +/tests/components/deako/ @sebirdman @balake @deakolights /homeassistant/components/debugpy/ @frenck /tests/components/debugpy/ @frenck /homeassistant/components/deconz/ @Kane610 diff --git a/homeassistant/components/deako/__init__.py b/homeassistant/components/deako/__init__.py new file mode 100644 index 00000000000..fdcf09fad60 --- /dev/null +++ b/homeassistant/components/deako/__init__.py @@ -0,0 +1,59 @@ +"""The deako integration.""" + +from __future__ import annotations + +import logging + +from pydeako.deako import Deako, DeviceListTimeout, FindDevicesTimeout +from pydeako.discover import DeakoDiscoverer + +from homeassistant.components import zeroconf +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady + +_LOGGER: logging.Logger = logging.getLogger(__name__) + +PLATFORMS: list[Platform] = [Platform.LIGHT] + +type DeakoConfigEntry = ConfigEntry[Deako] + + +async def async_setup_entry(hass: HomeAssistant, entry: DeakoConfigEntry) -> bool: + """Set up deako.""" + _zc = await zeroconf.async_get_instance(hass) + discoverer = DeakoDiscoverer(_zc) + + connection = Deako(discoverer.get_address) + + await connection.connect() + try: + await connection.find_devices() + except DeviceListTimeout as exc: # device list never received + _LOGGER.warning("Device not responding to device list") + await connection.disconnect() + raise ConfigEntryNotReady(exc) from exc + except FindDevicesTimeout as exc: # total devices expected not received + _LOGGER.warning("Device not responding to device requests") + await connection.disconnect() + raise ConfigEntryNotReady(exc) from exc + + # If deako devices are advertising on mdns, we should be able to get at least one device + devices = connection.get_devices() + if len(devices) == 0: + await connection.disconnect() + raise ConfigEntryNotReady(devices) + + entry.runtime_data = connection + + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: DeakoConfigEntry) -> bool: + """Unload a config entry.""" + await entry.runtime_data.disconnect() + + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/deako/config_flow.py b/homeassistant/components/deako/config_flow.py new file mode 100644 index 00000000000..d0676fa81d9 --- /dev/null +++ b/homeassistant/components/deako/config_flow.py @@ -0,0 +1,26 @@ +"""Config flow for deako.""" + +from pydeako.discover import DeakoDiscoverer, DevicesNotFoundException + +from homeassistant.components import zeroconf +from homeassistant.core import HomeAssistant +from homeassistant.helpers import config_entry_flow + +from .const import DOMAIN, NAME + + +async def _async_has_devices(hass: HomeAssistant) -> bool: + """Return if there are devices that can be discovered.""" + _zc = await zeroconf.async_get_instance(hass) + discoverer = DeakoDiscoverer(_zc) + + try: + await discoverer.get_address() + except DevicesNotFoundException: + return False + else: + # address exists, there's at least one device + return True + + +config_entry_flow.register_discovery_flow(DOMAIN, NAME, _async_has_devices) diff --git a/homeassistant/components/deako/const.py b/homeassistant/components/deako/const.py new file mode 100644 index 00000000000..f6b688b9b07 --- /dev/null +++ b/homeassistant/components/deako/const.py @@ -0,0 +1,5 @@ +"""Constants for Deako.""" + +# Base component constants +NAME = "Deako" +DOMAIN = "deako" diff --git a/homeassistant/components/deako/light.py b/homeassistant/components/deako/light.py new file mode 100644 index 00000000000..c7ff8765402 --- /dev/null +++ b/homeassistant/components/deako/light.py @@ -0,0 +1,96 @@ +"""Binary sensor platform for integration_blueprint.""" + +from typing import Any + +from pydeako.deako import Deako + +from homeassistant.components.light import ATTR_BRIGHTNESS, ColorMode, LightEntity +from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import DeakoConfigEntry +from .const import DOMAIN + +# Model names +MODEL_SMART = "smart" +MODEL_DIMMER = "dimmer" + + +async def async_setup_entry( + hass: HomeAssistant, + config: DeakoConfigEntry, + add_entities: AddEntitiesCallback, +) -> None: + """Configure the platform.""" + client = config.runtime_data + + add_entities([DeakoLightEntity(client, uuid) for uuid in client.get_devices()]) + + +class DeakoLightEntity(LightEntity): + """Deako LightEntity class.""" + + _attr_has_entity_name = True + _attr_name = None + _attr_is_on = False + _attr_available = True + + client: Deako + + def __init__(self, client: Deako, uuid: str) -> None: + """Save connection reference.""" + self.client = client + self._attr_unique_id = uuid + + dimmable = client.is_dimmable(uuid) + + model = MODEL_SMART + self._attr_color_mode = ColorMode.ONOFF + if dimmable: + model = MODEL_DIMMER + self._attr_color_mode = ColorMode.BRIGHTNESS + + self._attr_supported_color_modes = {self._attr_color_mode} + + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, uuid)}, + name=client.get_name(uuid), + manufacturer="Deako", + model=model, + ) + + client.set_state_callback(uuid, self.on_update) + self.update() # set initial state + + def on_update(self) -> None: + """State update callback.""" + self.update() + self.schedule_update_ha_state() + + async def control_device(self, power: bool, dim: int | None = None) -> None: + """Control entity state via client.""" + assert self._attr_unique_id is not None + await self.client.control_device(self._attr_unique_id, power, dim) + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn on the light.""" + dim = None + if ATTR_BRIGHTNESS in kwargs: + dim = round(kwargs[ATTR_BRIGHTNESS] / 2.55, 0) + await self.control_device(True, dim) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn off the device.""" + await self.control_device(False) + + def update(self) -> None: + """Call to update state.""" + assert self._attr_unique_id is not None + state = self.client.get_state(self._attr_unique_id) or {} + self._attr_is_on = bool(state.get("power", False)) + if ( + self._attr_supported_color_modes is not None + and ColorMode.BRIGHTNESS in self._attr_supported_color_modes + ): + self._attr_brightness = int(round(state.get("dim", 0) * 2.55)) diff --git a/homeassistant/components/deako/manifest.json b/homeassistant/components/deako/manifest.json new file mode 100644 index 00000000000..e8f6f235107 --- /dev/null +++ b/homeassistant/components/deako/manifest.json @@ -0,0 +1,13 @@ +{ + "domain": "deako", + "name": "Deako", + "codeowners": ["@sebirdman", "@balake", "@deakolights"], + "config_flow": true, + "dependencies": ["zeroconf"], + "documentation": "https://www.home-assistant.io/integrations/deako", + "iot_class": "local_polling", + "loggers": ["pydeako"], + "requirements": ["pydeako==0.4.0"], + "single_config_entry": true, + "zeroconf": ["_deako._tcp.local."] +} diff --git a/homeassistant/components/deako/strings.json b/homeassistant/components/deako/strings.json new file mode 100644 index 00000000000..6bb292d74a9 --- /dev/null +++ b/homeassistant/components/deako/strings.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "confirm": { + "description": "Please confirm setting up the Deako integration" + } + }, + "abort": { + "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", + "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]" + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index afa906fd371..ee6658a2515 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -114,6 +114,7 @@ FLOWS = { "cpuspeed", "crownstone", "daikin", + "deako", "deconz", "deluge", "denonavr", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index e204170a06f..aad03b25390 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -1091,6 +1091,13 @@ "config_flow": false, "iot_class": "local_polling" }, + "deako": { + "name": "Deako", + "integration_type": "hub", + "config_flow": true, + "iot_class": "local_polling", + "single_config_entry": true + }, "debugpy": { "name": "Remote Python Debugger", "integration_type": "service", diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index 389a4435910..3d5b0b4cfa1 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -423,6 +423,11 @@ ZEROCONF = { "domain": "forked_daapd", }, ], + "_deako._tcp.local.": [ + { + "domain": "deako", + }, + ], "_devialet-http._tcp.local.": [ { "domain": "devialet", diff --git a/mypy.ini b/mypy.ini index a312a77122f..c7a31d7354c 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1145,6 +1145,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.deako.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.deconz.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/requirements_all.txt b/requirements_all.txt index 1660c94ac11..c8c7412abe2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1803,6 +1803,9 @@ pydaikin==2.13.4 # homeassistant.components.danfoss_air pydanfossair==0.1.0 +# homeassistant.components.deako +pydeako==0.4.0 + # homeassistant.components.deconz pydeconz==116 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e4cda97ea71..871957558da 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1447,6 +1447,9 @@ pycsspeechtts==1.0.8 # homeassistant.components.daikin pydaikin==2.13.4 +# homeassistant.components.deako +pydeako==0.4.0 + # homeassistant.components.deconz pydeconz==116 diff --git a/tests/components/deako/__init__.py b/tests/components/deako/__init__.py new file mode 100644 index 00000000000..248a389f2e6 --- /dev/null +++ b/tests/components/deako/__init__.py @@ -0,0 +1 @@ +"""Tests for the Deako integration.""" diff --git a/tests/components/deako/conftest.py b/tests/components/deako/conftest.py new file mode 100644 index 00000000000..659634b8784 --- /dev/null +++ b/tests/components/deako/conftest.py @@ -0,0 +1,45 @@ +"""deako session fixtures.""" + +from collections.abc import Generator +from unittest.mock import MagicMock, patch + +import pytest + +from homeassistant.components.deako.const import DOMAIN + +from tests.common import MockConfigEntry + + +@pytest.fixture +def mock_config_entry() -> MockConfigEntry: + """Return the default mocked config entry.""" + return MockConfigEntry( + domain=DOMAIN, + ) + + +@pytest.fixture(autouse=True) +def pydeako_deako_mock() -> Generator[MagicMock]: + """Mock pydeako deako client.""" + with patch("homeassistant.components.deako.Deako", autospec=True) as mock: + yield mock + + +@pytest.fixture(autouse=True) +def pydeako_discoverer_mock(mock_async_zeroconf: MagicMock) -> Generator[MagicMock]: + """Mock pydeako discovery client.""" + with ( + patch("homeassistant.components.deako.DeakoDiscoverer", autospec=True) as mock, + patch("homeassistant.components.deako.config_flow.DeakoDiscoverer", new=mock), + ): + yield mock + + +@pytest.fixture +def mock_deako_setup() -> Generator[MagicMock]: + """Mock async_setup_entry for config flow tests.""" + with patch( + "homeassistant.components.deako.async_setup_entry", + return_value=True, + ) as mock_setup: + yield mock_setup diff --git a/tests/components/deako/snapshots/test_light.ambr b/tests/components/deako/snapshots/test_light.ambr new file mode 100644 index 00000000000..7bc170654e1 --- /dev/null +++ b/tests/components/deako/snapshots/test_light.ambr @@ -0,0 +1,168 @@ +# serializer version: 1 +# name: test_dimmable_light_props[light.kitchen-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'supported_color_modes': list([ + , + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.kitchen', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'deako', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'uuid', + 'unit_of_measurement': None, + }) +# --- +# name: test_dimmable_light_props[light.kitchen-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'brightness': 127, + 'color_mode': , + 'friendly_name': 'kitchen', + 'supported_color_modes': list([ + , + ]), + 'supported_features': , + }), + 'context': , + 'entity_id': 'light.kitchen', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_light_initial_props[light.kitchen-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'supported_color_modes': list([ + , + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.kitchen', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'deako', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'uuid', + 'unit_of_measurement': None, + }) +# --- +# name: test_light_initial_props[light.kitchen-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'color_mode': None, + 'friendly_name': 'kitchen', + 'supported_color_modes': list([ + , + ]), + 'supported_features': , + }), + 'context': , + 'entity_id': 'light.kitchen', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_light_setup_with_device[light.some_device-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'supported_color_modes': list([ + , + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.some_device', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'deako', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'some_device', + 'unit_of_measurement': None, + }) +# --- +# name: test_light_setup_with_device[light.some_device-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'brightness': 1, + 'color_mode': , + 'friendly_name': 'some device', + 'supported_color_modes': list([ + , + ]), + 'supported_features': , + }), + 'context': , + 'entity_id': 'light.some_device', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- diff --git a/tests/components/deako/test_config_flow.py b/tests/components/deako/test_config_flow.py new file mode 100644 index 00000000000..21b10eaaa36 --- /dev/null +++ b/tests/components/deako/test_config_flow.py @@ -0,0 +1,80 @@ +"""Tests for the deako component config flow.""" + +from unittest.mock import MagicMock + +from pydeako.discover import DevicesNotFoundException + +from homeassistant.components.deako.const import DOMAIN +from homeassistant.config_entries import SOURCE_USER +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from tests.common import MockConfigEntry + + +async def test_found( + hass: HomeAssistant, + pydeako_discoverer_mock: MagicMock, + mock_deako_setup: MagicMock, +) -> None: + """Test finding a Deako device.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + # Confirmation form + assert result["type"] is FlowResultType.FORM + + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.CREATE_ENTRY + pydeako_discoverer_mock.return_value.get_address.assert_called_once() + + mock_deako_setup.assert_called_once() + + +async def test_not_found( + hass: HomeAssistant, + pydeako_discoverer_mock: MagicMock, + mock_deako_setup: MagicMock, +) -> None: + """Test not finding any Deako devices.""" + pydeako_discoverer_mock.return_value.get_address.side_effect = ( + DevicesNotFoundException() + ) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + # Confirmation form + assert result["type"] is FlowResultType.FORM + + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "no_devices_found" + pydeako_discoverer_mock.return_value.get_address.assert_called_once() + + mock_deako_setup.assert_not_called() + + +async def test_already_configured( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_deako_setup: MagicMock, +) -> None: + """Test flow aborts when already configured.""" + + mock_config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "single_instance_allowed" + + mock_deako_setup.assert_not_called() diff --git a/tests/components/deako/test_init.py b/tests/components/deako/test_init.py new file mode 100644 index 00000000000..b4c0e8bb1f7 --- /dev/null +++ b/tests/components/deako/test_init.py @@ -0,0 +1,87 @@ +"""Tests for the deako component init.""" + +from unittest.mock import MagicMock + +from pydeako.deako import DeviceListTimeout, FindDevicesTimeout + +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +async def test_deako_async_setup_entry( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + pydeako_deako_mock: MagicMock, + pydeako_discoverer_mock: MagicMock, +) -> None: + """Test successful setup entry.""" + pydeako_deako_mock.return_value.get_devices.return_value = { + "id1": {}, + "id2": {}, + } + + mock_config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + pydeako_deako_mock.assert_called_once_with( + pydeako_discoverer_mock.return_value.get_address + ) + pydeako_deako_mock.return_value.connect.assert_called_once() + pydeako_deako_mock.return_value.find_devices.assert_called_once() + pydeako_deako_mock.return_value.get_devices.assert_called() + + assert mock_config_entry.runtime_data == pydeako_deako_mock.return_value + + +async def test_deako_async_setup_entry_device_list_timeout( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + pydeako_deako_mock: MagicMock, + pydeako_discoverer_mock: MagicMock, +) -> None: + """Test async_setup_entry raises ConfigEntryNotReady when pydeako raises DeviceListTimeout.""" + + mock_config_entry.add_to_hass(hass) + + pydeako_deako_mock.return_value.find_devices.side_effect = DeviceListTimeout() + + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + pydeako_deako_mock.assert_called_once_with( + pydeako_discoverer_mock.return_value.get_address + ) + pydeako_deako_mock.return_value.connect.assert_called_once() + pydeako_deako_mock.return_value.find_devices.assert_called_once() + pydeako_deako_mock.return_value.disconnect.assert_called_once() + + assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY + + +async def test_deako_async_setup_entry_find_devices_timeout( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + pydeako_deako_mock: MagicMock, + pydeako_discoverer_mock: MagicMock, +) -> None: + """Test async_setup_entry raises ConfigEntryNotReady when pydeako raises FindDevicesTimeout.""" + + mock_config_entry.add_to_hass(hass) + + pydeako_deako_mock.return_value.find_devices.side_effect = FindDevicesTimeout() + + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + pydeako_deako_mock.assert_called_once_with( + pydeako_discoverer_mock.return_value.get_address + ) + pydeako_deako_mock.return_value.connect.assert_called_once() + pydeako_deako_mock.return_value.find_devices.assert_called_once() + pydeako_deako_mock.return_value.disconnect.assert_called_once() + + assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY diff --git a/tests/components/deako/test_light.py b/tests/components/deako/test_light.py new file mode 100644 index 00000000000..b969c7f71cb --- /dev/null +++ b/tests/components/deako/test_light.py @@ -0,0 +1,192 @@ +"""Tests for the light module.""" + +from unittest.mock import MagicMock + +import pytest +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 +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from tests.common import MockConfigEntry, snapshot_platform + + +async def test_light_setup_with_device( + hass: HomeAssistant, + pydeako_deako_mock: MagicMock, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, +) -> None: + """Test light platform setup with device returned.""" + mock_config_entry.add_to_hass(hass) + + pydeako_deako_mock.return_value.get_devices.return_value = { + "some_device": {}, + } + pydeako_deako_mock.return_value.get_name.return_value = "some device" + + 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_initial_props( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + pydeako_deako_mock: MagicMock, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, +) -> None: + """Test on/off light is setup with accurate initial properties.""" + mock_config_entry.add_to_hass(hass) + + pydeako_deako_mock.return_value.get_devices.return_value = { + "uuid": { + "name": "kitchen", + } + } + pydeako_deako_mock.return_value.get_name.return_value = "kitchen" + pydeako_deako_mock.return_value.get_state.return_value = { + "power": False, + } + pydeako_deako_mock.return_value.is_dimmable.return_value = False + + 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_dimmable_light_props( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + pydeako_deako_mock: MagicMock, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, +) -> None: + """Test dimmable on/off light is setup with accurate initial properties.""" + mock_config_entry.add_to_hass(hass) + + pydeako_deako_mock.return_value.get_devices.return_value = { + "uuid": { + "name": "kitchen", + } + } + pydeako_deako_mock.return_value.get_name.return_value = "kitchen" + pydeako_deako_mock.return_value.get_state.return_value = { + "power": True, + "dim": 50, + } + pydeako_deako_mock.return_value.is_dimmable.return_value = True + + 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_power_change_on( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + pydeako_deako_mock: MagicMock, +) -> None: + """Test turing on a deako device.""" + mock_config_entry.add_to_hass(hass) + + pydeako_deako_mock.return_value.get_devices.return_value = { + "uuid": { + "name": "kitchen", + } + } + pydeako_deako_mock.return_value.get_name.return_value = "kitchen" + + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "light.kitchen"}, + blocking=True, + ) + + pydeako_deako_mock.return_value.control_device.assert_called_once_with( + "uuid", True, None + ) + + +async def test_light_power_change_off( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + pydeako_deako_mock: MagicMock, +) -> None: + """Test turing off a deako device.""" + mock_config_entry.add_to_hass(hass) + + pydeako_deako_mock.return_value.get_devices.return_value = { + "uuid": { + "name": "kitchen", + } + } + pydeako_deako_mock.return_value.get_name.return_value = "kitchen" + + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "light.kitchen"}, + blocking=True, + ) + + pydeako_deako_mock.return_value.control_device.assert_called_once_with( + "uuid", False, None + ) + + +@pytest.mark.parametrize( + ("dim_input", "expected_dim_value"), + [ + (3, 1), + (255, 100), + (127, 50), + ], +) +async def test_light_brightness_change( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + pydeako_deako_mock: MagicMock, + dim_input: int, + expected_dim_value: int, +) -> None: + """Test turing on a deako device.""" + mock_config_entry.add_to_hass(hass) + + pydeako_deako_mock.return_value.get_devices.return_value = { + "uuid": { + "name": "kitchen", + } + } + pydeako_deako_mock.return_value.get_name.return_value = "kitchen" + + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: "light.kitchen", + ATTR_BRIGHTNESS: dim_input, + }, + blocking=True, + ) + + pydeako_deako_mock.return_value.control_device.assert_called_once_with( + "uuid", True, expected_dim_value + )