From a47f73244c0b3fbe4210ffd7731be69b3984fbbd Mon Sep 17 00:00:00 2001
From: Leonardo Figueiro <leoagfig@gmail.com>
Date: Mon, 24 Aug 2020 09:15:07 -0300
Subject: [PATCH] Add Wilight integration with SSDP (#36694)

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
---
 CODEOWNERS                                    |   1 +
 homeassistant/components/wilight/__init__.py  | 125 ++++++
 .../components/wilight/config_flow.py         | 106 +++++
 homeassistant/components/wilight/const.py     |  14 +
 homeassistant/components/wilight/light.py     | 179 +++++++++
 .../components/wilight/manifest.json          |  14 +
 .../components/wilight/parent_device.py       | 102 +++++
 homeassistant/components/wilight/strings.json |  16 +
 .../components/wilight/translations/en.json   |  16 +
 homeassistant/generated/config_flows.py       |   1 +
 homeassistant/generated/ssdp.py               |   5 +
 requirements_all.txt                          |   3 +
 requirements_test_all.txt                     |   3 +
 tests/components/wilight/__init__.py          |  83 ++++
 tests/components/wilight/test_config_flow.py  | 157 ++++++++
 tests/components/wilight/test_init.py         |  65 +++
 tests/components/wilight/test_light.py        | 369 ++++++++++++++++++
 17 files changed, 1259 insertions(+)
 create mode 100644 homeassistant/components/wilight/__init__.py
 create mode 100644 homeassistant/components/wilight/config_flow.py
 create mode 100644 homeassistant/components/wilight/const.py
 create mode 100644 homeassistant/components/wilight/light.py
 create mode 100644 homeassistant/components/wilight/manifest.json
 create mode 100644 homeassistant/components/wilight/parent_device.py
 create mode 100644 homeassistant/components/wilight/strings.json
 create mode 100644 homeassistant/components/wilight/translations/en.json
 create mode 100644 tests/components/wilight/__init__.py
 create mode 100644 tests/components/wilight/test_config_flow.py
 create mode 100644 tests/components/wilight/test_init.py
 create mode 100644 tests/components/wilight/test_light.py

diff --git a/CODEOWNERS b/CODEOWNERS
index d91591a4526..eb9c736bc5e 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -474,6 +474,7 @@ homeassistant/components/weather/* @fabaff
 homeassistant/components/webostv/* @bendavid
 homeassistant/components/websocket_api/* @home-assistant/core
 homeassistant/components/wiffi/* @mampfes
+homeassistant/components/wilight/* @leofig-rj
 homeassistant/components/withings/* @vangorra
 homeassistant/components/wled/* @frenck
 homeassistant/components/wolflink/* @adamkrol93
diff --git a/homeassistant/components/wilight/__init__.py b/homeassistant/components/wilight/__init__.py
new file mode 100644
index 00000000000..8821190bd32
--- /dev/null
+++ b/homeassistant/components/wilight/__init__.py
@@ -0,0 +1,125 @@
+"""The WiLight integration."""
+import asyncio
+
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.core import HomeAssistant, callback
+from homeassistant.exceptions import ConfigEntryNotReady
+from homeassistant.helpers.entity import Entity
+
+from .const import DOMAIN
+from .parent_device import WiLightParent
+
+# List the platforms that you want to support.
+PLATFORMS = ["light"]
+
+
+async def async_setup(hass: HomeAssistant, config: dict):
+    """Set up the WiLight with Config Flow component."""
+
+    hass.data[DOMAIN] = {}
+
+    return True
+
+
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+    """Set up a wilight config entry."""
+
+    parent = WiLightParent(hass, entry)
+
+    if not await parent.async_setup():
+        raise ConfigEntryNotReady
+
+    hass.data[DOMAIN][entry.entry_id] = parent
+
+    # Set up all platforms for this device/entry.
+    for component in PLATFORMS:
+        hass.async_create_task(
+            hass.config_entries.async_forward_entry_setup(entry, component)
+        )
+
+    return True
+
+
+async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
+    """Unload WiLight config entry."""
+
+    # Unload entities for this entry/device.
+    await asyncio.gather(
+        *(
+            hass.config_entries.async_forward_entry_unload(entry, component)
+            for component in PLATFORMS
+        )
+    )
+
+    # Cleanup
+    parent = hass.data[DOMAIN][entry.entry_id]
+    await parent.async_reset()
+    del hass.data[DOMAIN][entry.entry_id]
+
+    return True
+
+
+class WiLightDevice(Entity):
+    """Representation of a WiLight device.
+
+    Contains the common logic for WiLight entities.
+    """
+
+    def __init__(self, api_device, index, item_name):
+        """Initialize the device."""
+        # WiLight specific attributes for every component type
+        self._device_id = api_device.device_id
+        self._sw_version = api_device.swversion
+        self._client = api_device.client
+        self._model = api_device.model
+        self._name = item_name
+        self._index = index
+        self._unique_id = f"{self._device_id}_{self._index}"
+        self._status = {}
+
+    @property
+    def should_poll(self):
+        """No polling needed."""
+        return False
+
+    @property
+    def name(self):
+        """Return a name for this WiLight item."""
+        return self._name
+
+    @property
+    def unique_id(self):
+        """Return the unique ID for this WiLight item."""
+        return self._unique_id
+
+    @property
+    def device_info(self):
+        """Return the device info."""
+        return {
+            "name": self._name,
+            "identifiers": {(DOMAIN, self._unique_id)},
+            "model": self._model,
+            "manufacturer": "WiLight",
+            "sw_version": self._sw_version,
+            "via_device": (DOMAIN, self._device_id),
+        }
+
+    @property
+    def available(self):
+        """Return True if entity is available."""
+        return bool(self._client.is_connected)
+
+    @callback
+    def handle_event_callback(self, states):
+        """Propagate changes through ha."""
+        self._status = states
+        self.async_write_ha_state()
+
+    async def async_update(self):
+        """Synchronize state with api_device."""
+        await self._client.status(self._index)
+
+    async def async_added_to_hass(self):
+        """Register update callback."""
+        self._client.register_status_callback(self.handle_event_callback, self._index)
+        await self._client.status(self._index)
diff --git a/homeassistant/components/wilight/config_flow.py b/homeassistant/components/wilight/config_flow.py
new file mode 100644
index 00000000000..724bbb6547d
--- /dev/null
+++ b/homeassistant/components/wilight/config_flow.py
@@ -0,0 +1,106 @@
+"""Config flow to configure WiLight."""
+import logging
+from urllib.parse import urlparse
+
+import pywilight
+
+from homeassistant.components import ssdp
+from homeassistant.config_entries import CONN_CLASS_LOCAL_PUSH, ConfigFlow
+from homeassistant.const import CONF_HOST
+
+from .const import DOMAIN  # pylint: disable=unused-import
+
+CONF_SERIAL_NUMBER = "serial_number"
+CONF_MODEL_NAME = "model_name"
+
+WILIGHT_MANUFACTURER = "All Automacao Ltda"
+
+# List the components supported by this integration.
+ALLOWED_WILIGHT_COMPONENTS = ["light"]
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class WiLightFlowHandler(ConfigFlow, domain=DOMAIN):
+    """Handle a WiLight config flow."""
+
+    VERSION = 1
+    CONNECTION_CLASS = CONN_CLASS_LOCAL_PUSH
+
+    def __init__(self):
+        """Initialize the WiLight flow."""
+        self._host = None
+        self._serial_number = None
+        self._title = None
+        self._model_name = None
+        self._wilight_components = []
+        self._components_text = ""
+
+    def _wilight_update(self, host, serial_number, model_name):
+        self._host = host
+        self._serial_number = serial_number
+        self._title = f"WL{serial_number}"
+        self._model_name = model_name
+        self._wilight_components = pywilight.get_components_from_model(model_name)
+        self._components_text = ", ".join(self._wilight_components)
+        return self._components_text != ""
+
+    def _get_entry(self):
+        data = {
+            CONF_HOST: self._host,
+            CONF_SERIAL_NUMBER: self._serial_number,
+            CONF_MODEL_NAME: self._model_name,
+        }
+        return self.async_create_entry(title=self._title, data=data)
+
+    async def async_step_ssdp(self, discovery_info):
+        """Handle a discovered WiLight."""
+        # Filter out basic information
+        if (
+            ssdp.ATTR_SSDP_LOCATION not in discovery_info
+            or ssdp.ATTR_UPNP_MANUFACTURER not in discovery_info
+            or ssdp.ATTR_UPNP_SERIAL not in discovery_info
+            or ssdp.ATTR_UPNP_MODEL_NAME not in discovery_info
+            or ssdp.ATTR_UPNP_MODEL_NUMBER not in discovery_info
+        ):
+            return self.async_abort(reason="not_wilight_device")
+        # Filter out non-WiLight devices
+        if discovery_info[ssdp.ATTR_UPNP_MANUFACTURER] != WILIGHT_MANUFACTURER:
+            return self.async_abort(reason="not_wilight_device")
+
+        host = urlparse(discovery_info[ssdp.ATTR_SSDP_LOCATION]).hostname
+        serial_number = discovery_info[ssdp.ATTR_UPNP_SERIAL]
+        model_name = discovery_info[ssdp.ATTR_UPNP_MODEL_NAME]
+
+        if not self._wilight_update(host, serial_number, model_name):
+            return self.async_abort(reason="not_wilight_device")
+
+        # Check if all components of this WiLight are allowed in this version of the HA integration
+        component_ok = all(
+            wilight_component in ALLOWED_WILIGHT_COMPONENTS
+            for wilight_component in self._wilight_components
+        )
+
+        if not component_ok:
+            return self.async_abort(reason="not_supported_device")
+
+        await self.async_set_unique_id(self._serial_number)
+        self._abort_if_unique_id_configured(updates={CONF_HOST: self._host})
+
+        # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
+        self.context["title_placeholders"] = {"name": self._title}
+        return await self.async_step_confirm()
+
+    async def async_step_confirm(self, user_input=None):
+        """Handle user-confirmation of discovered WiLight."""
+        if user_input is not None:
+            return self._get_entry()
+
+        return self.async_show_form(
+            step_id="confirm",
+            description_placeholders={
+                "name": self._title,
+                "components": self._components_text,
+            },
+            errors={},
+        )
diff --git a/homeassistant/components/wilight/const.py b/homeassistant/components/wilight/const.py
new file mode 100644
index 00000000000..a3d77da44ef
--- /dev/null
+++ b/homeassistant/components/wilight/const.py
@@ -0,0 +1,14 @@
+"""Constants for the WiLight integration."""
+
+DOMAIN = "wilight"
+
+# Item types
+ITEM_LIGHT = "light"
+
+# Light types
+LIGHT_ON_OFF = "light_on_off"
+LIGHT_DIMMER = "light_dimmer"
+LIGHT_COLOR = "light_rgb"
+
+# Light service support
+SUPPORT_NONE = 0
diff --git a/homeassistant/components/wilight/light.py b/homeassistant/components/wilight/light.py
new file mode 100644
index 00000000000..e4bf504165d
--- /dev/null
+++ b/homeassistant/components/wilight/light.py
@@ -0,0 +1,179 @@
+"""Support for WiLight lights."""
+
+from homeassistant.components.light import (
+    ATTR_BRIGHTNESS,
+    ATTR_HS_COLOR,
+    SUPPORT_BRIGHTNESS,
+    SUPPORT_COLOR,
+    LightEntity,
+)
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.core import HomeAssistant
+
+from . import WiLightDevice
+from .const import (
+    DOMAIN,
+    ITEM_LIGHT,
+    LIGHT_COLOR,
+    LIGHT_DIMMER,
+    LIGHT_ON_OFF,
+    SUPPORT_NONE,
+)
+
+
+def entities_from_discovered_wilight(hass, api_device):
+    """Parse configuration and add WiLight light entities."""
+    entities = []
+    for item in api_device.items:
+        if item["type"] != ITEM_LIGHT:
+            continue
+        index = item["index"]
+        item_name = item["name"]
+        if item["sub_type"] == LIGHT_ON_OFF:
+            entity = WiLightLightOnOff(api_device, index, item_name)
+        elif item["sub_type"] == LIGHT_DIMMER:
+            entity = WiLightLightDimmer(api_device, index, item_name)
+        elif item["sub_type"] == LIGHT_COLOR:
+            entity = WiLightLightColor(api_device, index, item_name)
+        else:
+            continue
+        entities.append(entity)
+
+    return entities
+
+
+async def async_setup_entry(
+    hass: HomeAssistant, entry: ConfigEntry, async_add_entities
+):
+    """Set up WiLight lights from a config entry."""
+    parent = hass.data[DOMAIN][entry.entry_id]
+
+    # Handle a discovered WiLight device.
+    entities = entities_from_discovered_wilight(hass, parent.api)
+    async_add_entities(entities)
+
+
+class WiLightLightOnOff(WiLightDevice, LightEntity):
+    """Representation of a WiLights light on-off."""
+
+    @property
+    def supported_features(self):
+        """Flag supported features."""
+        return SUPPORT_NONE
+
+    @property
+    def is_on(self):
+        """Return true if device is on."""
+        return self._status.get("on")
+
+    async def async_turn_on(self, **kwargs):
+        """Turn the device on."""
+        await self._client.turn_on(self._index)
+
+    async def async_turn_off(self, **kwargs):
+        """Turn the device off."""
+        await self._client.turn_off(self._index)
+
+
+class WiLightLightDimmer(WiLightDevice, LightEntity):
+    """Representation of a WiLights light dimmer."""
+
+    @property
+    def supported_features(self):
+        """Flag supported features."""
+        return SUPPORT_BRIGHTNESS
+
+    @property
+    def brightness(self):
+        """Return the brightness of this light between 0..255."""
+        return int(self._status.get("brightness", 0))
+
+    @property
+    def is_on(self):
+        """Return true if device is on."""
+        return self._status.get("on")
+
+    async def async_turn_on(self, **kwargs):
+        """Turn the device on,set brightness if needed."""
+        # Dimmer switches use a range of [0, 255] to control
+        # brightness. Level 255 might mean to set it to previous value
+        if ATTR_BRIGHTNESS in kwargs:
+            brightness = kwargs[ATTR_BRIGHTNESS]
+            await self._client.set_brightness(self._index, brightness)
+        else:
+            await self._client.turn_on(self._index)
+
+    async def async_turn_off(self, **kwargs):
+        """Turn the device off."""
+        await self._client.turn_off(self._index)
+
+
+def wilight_to_hass_hue(value):
+    """Convert wilight hue 1..255 to hass 0..360 scale."""
+    return min(360, round((value * 360) / 255, 3))
+
+
+def hass_to_wilight_hue(value):
+    """Convert hass hue 0..360 to wilight 1..255 scale."""
+    return min(255, round((value * 255) / 360))
+
+
+def wilight_to_hass_saturation(value):
+    """Convert wilight saturation 1..255 to hass 0..100 scale."""
+    return min(100, round((value * 100) / 255, 3))
+
+
+def hass_to_wilight_saturation(value):
+    """Convert hass saturation 0..100 to wilight 1..255 scale."""
+    return min(255, round((value * 255) / 100))
+
+
+class WiLightLightColor(WiLightDevice, LightEntity):
+    """Representation of a WiLights light rgb."""
+
+    @property
+    def supported_features(self):
+        """Flag supported features."""
+        return SUPPORT_BRIGHTNESS | SUPPORT_COLOR
+
+    @property
+    def brightness(self):
+        """Return the brightness of this light between 0..255."""
+        return int(self._status.get("brightness", 0))
+
+    @property
+    def hs_color(self):
+        """Return the hue and saturation color value [float, float]."""
+        return [
+            wilight_to_hass_hue(int(self._status.get("hue", 0))),
+            wilight_to_hass_saturation(int(self._status.get("saturation", 0))),
+        ]
+
+    @property
+    def is_on(self):
+        """Return true if device is on."""
+        return self._status.get("on")
+
+    async def async_turn_on(self, **kwargs):
+        """Turn the device on,set brightness if needed."""
+        # Brightness use a range of [0, 255] to control
+        # Hue use a range of [0, 360] to control
+        # Saturation use a range of [0, 100] to control
+        if ATTR_BRIGHTNESS in kwargs and ATTR_HS_COLOR in kwargs:
+            brightness = kwargs[ATTR_BRIGHTNESS]
+            hue = hass_to_wilight_hue(kwargs[ATTR_HS_COLOR][0])
+            saturation = hass_to_wilight_saturation(kwargs[ATTR_HS_COLOR][1])
+            await self._client.set_hsb_color(self._index, hue, saturation, brightness)
+        elif ATTR_BRIGHTNESS in kwargs and ATTR_HS_COLOR not in kwargs:
+            brightness = kwargs[ATTR_BRIGHTNESS]
+            await self._client.set_brightness(self._index, brightness)
+        elif ATTR_BRIGHTNESS not in kwargs and ATTR_HS_COLOR in kwargs:
+            hue = hass_to_wilight_hue(kwargs[ATTR_HS_COLOR][0])
+            saturation = hass_to_wilight_saturation(kwargs[ATTR_HS_COLOR][1])
+            await self._client.set_hs_color(self._index, hue, saturation)
+        else:
+            await self._client.turn_on(self._index)
+
+    async def async_turn_off(self, **kwargs):
+        """Turn the device off."""
+        await self._client.turn_off(self._index)
diff --git a/homeassistant/components/wilight/manifest.json b/homeassistant/components/wilight/manifest.json
new file mode 100644
index 00000000000..bb20da2b1ce
--- /dev/null
+++ b/homeassistant/components/wilight/manifest.json
@@ -0,0 +1,14 @@
+{
+  "domain": "wilight",
+  "name": "WiLight",
+  "config_flow": true,
+  "documentation": "https://www.home-assistant.io/integrations/wilight",
+  "requirements": ["pywilight==0.0.65"],
+  "ssdp": [
+    {
+      "manufacturer": "All Automacao Ltda"
+    }
+  ],
+  "codeowners": ["@leofig-rj"],
+  "quality_scale": "silver"
+}
diff --git a/homeassistant/components/wilight/parent_device.py b/homeassistant/components/wilight/parent_device.py
new file mode 100644
index 00000000000..9c2c0c1a1de
--- /dev/null
+++ b/homeassistant/components/wilight/parent_device.py
@@ -0,0 +1,102 @@
+"""The WiLight Device integration."""
+import asyncio
+import logging
+
+import pywilight
+import requests
+
+from homeassistant.const import CONF_HOST, EVENT_HOMEASSISTANT_STOP
+from homeassistant.core import callback
+from homeassistant.helpers.dispatcher import async_dispatcher_send
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class WiLightParent:
+    """Manages a single WiLight Parent Device."""
+
+    def __init__(self, hass, config_entry):
+        """Initialize the system."""
+        self._host = config_entry.data[CONF_HOST]
+        self._hass = hass
+        self._api = None
+
+    @property
+    def host(self):
+        """Return the host of this parent."""
+        return self._host
+
+    @property
+    def api(self):
+        """Return the api of this parent."""
+        return self._api
+
+    async def async_setup(self):
+        """Set up a WiLight Parent Device based on host parameter."""
+        host = self._host
+        hass = self._hass
+
+        api_device = await hass.async_add_executor_job(create_api_device, host)
+
+        if api_device is None:
+            return False
+
+        @callback
+        def disconnected():
+            # Schedule reconnect after connection has been lost.
+            _LOGGER.warning("WiLight %s disconnected", api_device.device_id)
+            async_dispatcher_send(
+                hass, f"wilight_device_available_{api_device.device_id}", False
+            )
+
+        @callback
+        def reconnected():
+            # Schedule reconnect after connection has been lost.
+            _LOGGER.warning("WiLight %s reconnect", api_device.device_id)
+            async_dispatcher_send(
+                hass, f"wilight_device_available_{api_device.device_id}", True
+            )
+
+        async def connect(api_device):
+            # Set up connection and hook it into HA for reconnect/shutdown.
+            _LOGGER.debug("Initiating connection to %s", api_device.device_id)
+
+            client = await api_device.config_client(
+                disconnect_callback=disconnected,
+                reconnect_callback=reconnected,
+                loop=asyncio.get_running_loop(),
+                logger=_LOGGER,
+            )
+
+            # handle shutdown of WiLight asyncio transport
+            hass.bus.async_listen_once(
+                EVENT_HOMEASSISTANT_STOP, lambda x: client.stop()
+            )
+
+            _LOGGER.info("Connected to WiLight device: %s", api_device.device_id)
+
+        await connect(api_device)
+
+        self._api = api_device
+
+        return True
+
+    async def async_reset(self):
+        """Reset api."""
+
+        # If the initialization was wrong.
+        if self._api is None:
+            return True
+
+        self._api.client.stop()
+
+
+def create_api_device(host):
+    """Create an API Device."""
+    try:
+        device = pywilight.device_from_host(host)
+    except (requests.exceptions.ConnectionError, requests.exceptions.Timeout,) as err:
+        _LOGGER.error("Unable to access WiLight at %s (%s)", host, err)
+        return None
+
+    return device
diff --git a/homeassistant/components/wilight/strings.json b/homeassistant/components/wilight/strings.json
new file mode 100644
index 00000000000..710543a5a53
--- /dev/null
+++ b/homeassistant/components/wilight/strings.json
@@ -0,0 +1,16 @@
+{
+  "config": {
+    "flow_title": "WiLight: {name}",
+    "step": {
+      "confirm": {
+        "title": "WiLight",
+        "description": "Do you want to set up WiLight {name}?\n\n It supports: {components}"
+      }
+    },
+    "abort": {
+      "already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
+      "not_supported_device": "This WiLight is currently not supported",
+      "not_wilight_device": "This Device is not WiLight"
+    }
+  }
+}
diff --git a/homeassistant/components/wilight/translations/en.json b/homeassistant/components/wilight/translations/en.json
new file mode 100644
index 00000000000..fb121e3b2fa
--- /dev/null
+++ b/homeassistant/components/wilight/translations/en.json
@@ -0,0 +1,16 @@
+{
+    "config": {
+        "abort": {
+			"already_configured_device": "This WiLight is already configured",
+			"not_supported_device": "This WiLight is currently not supported",
+			"not_wilight_device": "This Device is not WiLight"
+        },
+		"flow_title": "WiLight: {name}",
+		"step": {
+			"confirm": {
+				"title": "WiLight",
+				"description": "Do you want to set up WiLight {name}?\n\n It supports: {components}"
+			}
+		}
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py
index b5db34ec485..ac9ebae5264 100644
--- a/homeassistant/generated/config_flows.py
+++ b/homeassistant/generated/config_flows.py
@@ -199,6 +199,7 @@ FLOWS = [
     "volumio",
     "wemo",
     "wiffi",
+    "wilight",
     "withings",
     "wled",
     "wolflink",
diff --git a/homeassistant/generated/ssdp.py b/homeassistant/generated/ssdp.py
index d58842fe88e..f66c5f0999d 100644
--- a/homeassistant/generated/ssdp.py
+++ b/homeassistant/generated/ssdp.py
@@ -172,5 +172,10 @@ SSDP = {
         {
             "manufacturer": "Belkin International Inc."
         }
+    ],
+    "wilight": [
+        {
+            "manufacturer": "All Automacao Ltda"
+        }
     ]
 }
diff --git a/requirements_all.txt b/requirements_all.txt
index 153cb8117ad..3b60480f7da 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -1852,6 +1852,9 @@ pywebpush==1.9.2
 # homeassistant.components.wemo
 pywemo==0.4.46
 
+# homeassistant.components.wilight
+pywilight==0.0.65
+
 # homeassistant.components.xeoma
 pyxeoma==1.4.1
 
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index d3f45827bc4..f2c11ca739a 100644
--- a/requirements_test_all.txt
+++ b/requirements_test_all.txt
@@ -857,6 +857,9 @@ pyvolumio==0.1.1
 # homeassistant.components.html5
 pywebpush==1.9.2
 
+# homeassistant.components.wilight
+pywilight==0.0.65
+
 # homeassistant.components.zerproc
 pyzerproc==0.2.5
 
diff --git a/tests/components/wilight/__init__.py b/tests/components/wilight/__init__.py
new file mode 100644
index 00000000000..9c7ba13fc7c
--- /dev/null
+++ b/tests/components/wilight/__init__.py
@@ -0,0 +1,83 @@
+"""Tests for the WiLight component."""
+from homeassistant.components.ssdp import (
+    ATTR_SSDP_LOCATION,
+    ATTR_UPNP_MANUFACTURER,
+    ATTR_UPNP_MODEL_NAME,
+    ATTR_UPNP_MODEL_NUMBER,
+    ATTR_UPNP_SERIAL,
+)
+from homeassistant.components.wilight.config_flow import (
+    CONF_MODEL_NAME,
+    CONF_SERIAL_NUMBER,
+)
+from homeassistant.components.wilight.const import DOMAIN
+from homeassistant.const import CONF_HOST
+from homeassistant.helpers.typing import HomeAssistantType
+
+from tests.common import MockConfigEntry
+
+HOST = "127.0.0.1"
+WILIGHT_ID = "000000000099"
+SSDP_LOCATION = "http://127.0.0.1/"
+UPNP_MANUFACTURER = "All Automacao Ltda"
+UPNP_MODEL_NAME_P_B = "WiLight 0102001800010009-10010010"
+UPNP_MODEL_NAME_DIMMER = "WiLight 0100001700020009-10010010"
+UPNP_MODEL_NAME_COLOR = "WiLight 0107001800020009-11010"
+UPNP_MODEL_NAME_LIGHT_FAN = "WiLight 0104001800010009-10"
+UPNP_MODEL_NUMBER = "123456789012345678901234567890123456"
+UPNP_SERIAL = "000000000099"
+UPNP_MAC_ADDRESS = "5C:CF:7F:8B:CA:56"
+UPNP_MANUFACTURER_NOT_WILIGHT = "Test"
+CONF_COMPONENTS = "components"
+
+MOCK_SSDP_DISCOVERY_INFO_P_B = {
+    ATTR_SSDP_LOCATION: SSDP_LOCATION,
+    ATTR_UPNP_MANUFACTURER: UPNP_MANUFACTURER,
+    ATTR_UPNP_MODEL_NAME: UPNP_MODEL_NAME_P_B,
+    ATTR_UPNP_MODEL_NUMBER: UPNP_MODEL_NUMBER,
+    ATTR_UPNP_SERIAL: UPNP_SERIAL,
+}
+
+MOCK_SSDP_DISCOVERY_INFO_WRONG_MANUFACTORER = {
+    ATTR_SSDP_LOCATION: SSDP_LOCATION,
+    ATTR_UPNP_MANUFACTURER: UPNP_MANUFACTURER_NOT_WILIGHT,
+    ATTR_UPNP_MODEL_NAME: UPNP_MODEL_NAME_P_B,
+    ATTR_UPNP_MODEL_NUMBER: UPNP_MODEL_NUMBER,
+    ATTR_UPNP_SERIAL: ATTR_UPNP_SERIAL,
+}
+
+MOCK_SSDP_DISCOVERY_INFO_MISSING_MANUFACTORER = {
+    ATTR_SSDP_LOCATION: SSDP_LOCATION,
+    ATTR_UPNP_MODEL_NAME: UPNP_MODEL_NAME_P_B,
+    ATTR_UPNP_MODEL_NUMBER: UPNP_MODEL_NUMBER,
+    ATTR_UPNP_SERIAL: ATTR_UPNP_SERIAL,
+}
+
+MOCK_SSDP_DISCOVERY_INFO_LIGHT_FAN = {
+    ATTR_SSDP_LOCATION: SSDP_LOCATION,
+    ATTR_UPNP_MANUFACTURER: UPNP_MANUFACTURER,
+    ATTR_UPNP_MODEL_NAME: UPNP_MODEL_NAME_LIGHT_FAN,
+    ATTR_UPNP_MODEL_NUMBER: UPNP_MODEL_NUMBER,
+    ATTR_UPNP_SERIAL: ATTR_UPNP_SERIAL,
+}
+
+
+async def setup_integration(hass: HomeAssistantType,) -> MockConfigEntry:
+    """Mock ConfigEntry in Home Assistant."""
+
+    entry = MockConfigEntry(
+        domain=DOMAIN,
+        unique_id=WILIGHT_ID,
+        data={
+            CONF_HOST: HOST,
+            CONF_SERIAL_NUMBER: UPNP_SERIAL,
+            CONF_MODEL_NAME: UPNP_MODEL_NAME_P_B,
+        },
+    )
+
+    entry.add_to_hass(hass)
+
+    await hass.config_entries.async_setup(entry.entry_id)
+    await hass.async_block_till_done()
+
+    return entry
diff --git a/tests/components/wilight/test_config_flow.py b/tests/components/wilight/test_config_flow.py
new file mode 100644
index 00000000000..1de190c51d9
--- /dev/null
+++ b/tests/components/wilight/test_config_flow.py
@@ -0,0 +1,157 @@
+"""Test the WiLight config flow."""
+from asynctest import patch
+import pytest
+
+from homeassistant.components.wilight.config_flow import (
+    CONF_MODEL_NAME,
+    CONF_SERIAL_NUMBER,
+)
+from homeassistant.components.wilight.const import DOMAIN
+from homeassistant.config_entries import SOURCE_SSDP
+from homeassistant.const import CONF_HOST, CONF_NAME, CONF_SOURCE
+from homeassistant.data_entry_flow import (
+    RESULT_TYPE_ABORT,
+    RESULT_TYPE_CREATE_ENTRY,
+    RESULT_TYPE_FORM,
+)
+from homeassistant.helpers.typing import HomeAssistantType
+
+from tests.common import MockConfigEntry
+from tests.components.wilight import (
+    CONF_COMPONENTS,
+    HOST,
+    MOCK_SSDP_DISCOVERY_INFO_LIGHT_FAN,
+    MOCK_SSDP_DISCOVERY_INFO_MISSING_MANUFACTORER,
+    MOCK_SSDP_DISCOVERY_INFO_P_B,
+    MOCK_SSDP_DISCOVERY_INFO_WRONG_MANUFACTORER,
+    UPNP_MODEL_NAME_P_B,
+    UPNP_SERIAL,
+    WILIGHT_ID,
+)
+
+
+@pytest.fixture(name="dummy_get_components_from_model_clear")
+def mock_dummy_get_components_from_model():
+    """Mock a clear components list."""
+    components = []
+    with patch(
+        "pywilight.get_components_from_model", return_value=components,
+    ):
+        yield components
+
+
+async def test_show_ssdp_form(hass: HomeAssistantType) -> None:
+    """Test that the ssdp confirmation form is served."""
+
+    discovery_info = MOCK_SSDP_DISCOVERY_INFO_P_B.copy()
+    result = await hass.config_entries.flow.async_init(
+        DOMAIN, context={CONF_SOURCE: SOURCE_SSDP}, data=discovery_info
+    )
+
+    assert result["type"] == RESULT_TYPE_FORM
+    assert result["step_id"] == "confirm"
+    assert result["description_placeholders"] == {
+        CONF_NAME: f"WL{WILIGHT_ID}",
+        CONF_COMPONENTS: "light",
+    }
+
+
+async def test_ssdp_not_wilight_abort_1(hass: HomeAssistantType) -> None:
+    """Test that the ssdp aborts not_wilight."""
+
+    discovery_info = MOCK_SSDP_DISCOVERY_INFO_WRONG_MANUFACTORER.copy()
+    result = await hass.config_entries.flow.async_init(
+        DOMAIN, context={CONF_SOURCE: SOURCE_SSDP}, data=discovery_info
+    )
+
+    assert result["type"] == RESULT_TYPE_ABORT
+    assert result["reason"] == "not_wilight_device"
+
+
+async def test_ssdp_not_wilight_abort_2(hass: HomeAssistantType) -> None:
+    """Test that the ssdp aborts not_wilight."""
+
+    discovery_info = MOCK_SSDP_DISCOVERY_INFO_MISSING_MANUFACTORER.copy()
+    result = await hass.config_entries.flow.async_init(
+        DOMAIN, context={CONF_SOURCE: SOURCE_SSDP}, data=discovery_info
+    )
+
+    assert result["type"] == RESULT_TYPE_ABORT
+    assert result["reason"] == "not_wilight_device"
+
+
+async def test_ssdp_not_wilight_abort_3(
+    hass: HomeAssistantType, dummy_get_components_from_model_clear
+) -> None:
+    """Test that the ssdp aborts not_wilight."""
+
+    discovery_info = MOCK_SSDP_DISCOVERY_INFO_P_B.copy()
+    result = await hass.config_entries.flow.async_init(
+        DOMAIN, context={CONF_SOURCE: SOURCE_SSDP}, data=discovery_info
+    )
+
+    assert result["type"] == RESULT_TYPE_ABORT
+    assert result["reason"] == "not_wilight_device"
+
+
+async def test_ssdp_not_supported_abort(hass: HomeAssistantType) -> None:
+    """Test that the ssdp aborts not_supported."""
+
+    discovery_info = MOCK_SSDP_DISCOVERY_INFO_LIGHT_FAN.copy()
+    result = await hass.config_entries.flow.async_init(
+        DOMAIN, context={CONF_SOURCE: SOURCE_SSDP}, data=discovery_info
+    )
+
+    assert result["type"] == RESULT_TYPE_ABORT
+    assert result["reason"] == "not_supported_device"
+
+
+async def test_ssdp_device_exists_abort(hass: HomeAssistantType) -> None:
+    """Test abort SSDP flow if WiLight already configured."""
+    entry = MockConfigEntry(
+        domain=DOMAIN,
+        unique_id=WILIGHT_ID,
+        data={
+            CONF_HOST: HOST,
+            CONF_SERIAL_NUMBER: UPNP_SERIAL,
+            CONF_MODEL_NAME: UPNP_MODEL_NAME_P_B,
+        },
+    )
+
+    entry.add_to_hass(hass)
+
+    discovery_info = MOCK_SSDP_DISCOVERY_INFO_P_B.copy()
+    result = await hass.config_entries.flow.async_init(
+        DOMAIN, context={CONF_SOURCE: SOURCE_SSDP}, data=discovery_info,
+    )
+
+    assert result["type"] == RESULT_TYPE_ABORT
+    assert result["reason"] == "already_configured"
+
+
+async def test_full_ssdp_flow_implementation(hass: HomeAssistantType) -> None:
+    """Test the full SSDP flow from start to finish."""
+
+    discovery_info = MOCK_SSDP_DISCOVERY_INFO_P_B.copy()
+    result = await hass.config_entries.flow.async_init(
+        DOMAIN, context={CONF_SOURCE: SOURCE_SSDP}, data=discovery_info
+    )
+
+    assert result["type"] == RESULT_TYPE_FORM
+    assert result["step_id"] == "confirm"
+    assert result["description_placeholders"] == {
+        CONF_NAME: f"WL{WILIGHT_ID}",
+        "components": "light",
+    }
+
+    result = await hass.config_entries.flow.async_configure(
+        result["flow_id"], user_input={}
+    )
+
+    assert result["type"] == RESULT_TYPE_CREATE_ENTRY
+    assert result["title"] == f"WL{WILIGHT_ID}"
+
+    assert result["data"]
+    assert result["data"][CONF_HOST] == HOST
+    assert result["data"][CONF_SERIAL_NUMBER] == UPNP_SERIAL
+    assert result["data"][CONF_MODEL_NAME] == UPNP_MODEL_NAME_P_B
diff --git a/tests/components/wilight/test_init.py b/tests/components/wilight/test_init.py
new file mode 100644
index 00000000000..11b86d0c367
--- /dev/null
+++ b/tests/components/wilight/test_init.py
@@ -0,0 +1,65 @@
+"""Tests for the WiLight integration."""
+from asynctest import patch
+import pytest
+import pywilight
+
+from homeassistant.components.wilight.const import DOMAIN
+from homeassistant.config_entries import (
+    ENTRY_STATE_LOADED,
+    ENTRY_STATE_NOT_LOADED,
+    ENTRY_STATE_SETUP_RETRY,
+)
+from homeassistant.helpers.typing import HomeAssistantType
+
+from tests.components.wilight import (
+    HOST,
+    UPNP_MAC_ADDRESS,
+    UPNP_MODEL_NAME_P_B,
+    UPNP_MODEL_NUMBER,
+    UPNP_SERIAL,
+    setup_integration,
+)
+
+
+@pytest.fixture(name="dummy_device_from_host")
+def mock_dummy_device_from_host():
+    """Mock a valid api_devce."""
+
+    device = pywilight.wilight_from_discovery(
+        f"http://{HOST}:45995/wilight.xml",
+        UPNP_MAC_ADDRESS,
+        UPNP_MODEL_NAME_P_B,
+        UPNP_SERIAL,
+        UPNP_MODEL_NUMBER,
+    )
+
+    device.set_dummy(True)
+
+    with patch(
+        "pywilight.device_from_host", return_value=device,
+    ):
+        yield device
+
+
+async def test_config_entry_not_ready(hass: HomeAssistantType) -> None:
+    """Test the WiLight configuration entry not ready."""
+    entry = await setup_integration(hass)
+
+    assert entry.state == ENTRY_STATE_SETUP_RETRY
+
+
+async def test_unload_config_entry(
+    hass: HomeAssistantType, dummy_device_from_host
+) -> None:
+    """Test the WiLight configuration entry unloading."""
+    entry = await setup_integration(hass)
+
+    assert entry.entry_id in hass.data[DOMAIN]
+    assert entry.state == ENTRY_STATE_LOADED
+
+    await hass.config_entries.async_unload(entry.entry_id)
+    await hass.async_block_till_done()
+
+    if DOMAIN in hass.data:
+        assert entry.entry_id not in hass.data[DOMAIN]
+        assert entry.state == ENTRY_STATE_NOT_LOADED
diff --git a/tests/components/wilight/test_light.py b/tests/components/wilight/test_light.py
new file mode 100644
index 00000000000..555c1487bec
--- /dev/null
+++ b/tests/components/wilight/test_light.py
@@ -0,0 +1,369 @@
+"""Tests for the WiLight integration."""
+from asynctest import patch
+import pytest
+import pywilight
+
+from homeassistant.components.light import (
+    ATTR_BRIGHTNESS,
+    ATTR_HS_COLOR,
+    DOMAIN as LIGHT_DOMAIN,
+)
+from homeassistant.const import (
+    ATTR_ENTITY_ID,
+    SERVICE_TURN_OFF,
+    SERVICE_TURN_ON,
+    STATE_OFF,
+    STATE_ON,
+)
+from homeassistant.helpers.typing import HomeAssistantType
+
+from tests.components.wilight import (
+    HOST,
+    UPNP_MAC_ADDRESS,
+    UPNP_MODEL_NAME_COLOR,
+    UPNP_MODEL_NAME_DIMMER,
+    UPNP_MODEL_NAME_LIGHT_FAN,
+    UPNP_MODEL_NAME_P_B,
+    UPNP_MODEL_NUMBER,
+    UPNP_SERIAL,
+    WILIGHT_ID,
+    setup_integration,
+)
+
+
+@pytest.fixture(name="dummy_get_components_from_model_light")
+def mock_dummy_get_components_from_model_light():
+    """Mock a components list with light."""
+    components = ["light"]
+    with patch(
+        "pywilight.get_components_from_model", return_value=components,
+    ):
+        yield components
+
+
+@pytest.fixture(name="dummy_device_from_host_light_fan")
+def mock_dummy_device_from_host_light_fan():
+    """Mock a valid api_devce."""
+
+    device = pywilight.wilight_from_discovery(
+        f"http://{HOST}:45995/wilight.xml",
+        UPNP_MAC_ADDRESS,
+        UPNP_MODEL_NAME_LIGHT_FAN,
+        UPNP_SERIAL,
+        UPNP_MODEL_NUMBER,
+    )
+
+    device.set_dummy(True)
+
+    with patch(
+        "pywilight.device_from_host", return_value=device,
+    ):
+        yield device
+
+
+@pytest.fixture(name="dummy_device_from_host_pb")
+def mock_dummy_device_from_host_pb():
+    """Mock a valid api_devce."""
+
+    device = pywilight.wilight_from_discovery(
+        f"http://{HOST}:45995/wilight.xml",
+        UPNP_MAC_ADDRESS,
+        UPNP_MODEL_NAME_P_B,
+        UPNP_SERIAL,
+        UPNP_MODEL_NUMBER,
+    )
+
+    device.set_dummy(True)
+
+    with patch(
+        "pywilight.device_from_host", return_value=device,
+    ):
+        yield device
+
+
+@pytest.fixture(name="dummy_device_from_host_dimmer")
+def mock_dummy_device_from_host_dimmer():
+    """Mock a valid api_devce."""
+
+    device = pywilight.wilight_from_discovery(
+        f"http://{HOST}:45995/wilight.xml",
+        UPNP_MAC_ADDRESS,
+        UPNP_MODEL_NAME_DIMMER,
+        UPNP_SERIAL,
+        UPNP_MODEL_NUMBER,
+    )
+
+    device.set_dummy(True)
+
+    with patch(
+        "pywilight.device_from_host", return_value=device,
+    ):
+        yield device
+
+
+@pytest.fixture(name="dummy_device_from_host_color")
+def mock_dummy_device_from_host_color():
+    """Mock a valid api_devce."""
+
+    device = pywilight.wilight_from_discovery(
+        f"http://{HOST}:45995/wilight.xml",
+        UPNP_MAC_ADDRESS,
+        UPNP_MODEL_NAME_COLOR,
+        UPNP_SERIAL,
+        UPNP_MODEL_NUMBER,
+    )
+
+    device.set_dummy(True)
+
+    with patch(
+        "pywilight.device_from_host", return_value=device,
+    ):
+        yield device
+
+
+async def test_loading_light(
+    hass: HomeAssistantType,
+    dummy_device_from_host_light_fan,
+    dummy_get_components_from_model_light,
+) -> None:
+    """Test the WiLight configuration entry loading."""
+
+    # Using light_fan and removind fan from get_components_from_model
+    # to test light.py line 28
+    entry = await setup_integration(hass)
+    assert entry
+    assert entry.unique_id == WILIGHT_ID
+
+    entity_registry = await hass.helpers.entity_registry.async_get_registry()
+
+    # First segment of the strip
+    state = hass.states.get("light.wl000000000099_1")
+    assert state
+    assert state.state == STATE_OFF
+
+    entry = entity_registry.async_get("light.wl000000000099_1")
+    assert entry
+    assert entry.unique_id == "WL000000000099_0"
+
+
+async def test_on_off_light_state(
+    hass: HomeAssistantType, dummy_device_from_host_pb
+) -> None:
+    """Test the change of state of the light switches."""
+    await setup_integration(hass)
+
+    # Turn on
+    await hass.services.async_call(
+        LIGHT_DOMAIN,
+        SERVICE_TURN_ON,
+        {ATTR_ENTITY_ID: "light.wl000000000099_1"},
+        blocking=True,
+    )
+
+    await hass.async_block_till_done()
+    state = hass.states.get("light.wl000000000099_1")
+    assert state
+    assert state.state == STATE_ON
+
+    # Turn off
+    await hass.services.async_call(
+        LIGHT_DOMAIN,
+        SERVICE_TURN_OFF,
+        {ATTR_ENTITY_ID: "light.wl000000000099_1"},
+        blocking=True,
+    )
+
+    await hass.async_block_till_done()
+    state = hass.states.get("light.wl000000000099_1")
+    assert state
+    assert state.state == STATE_OFF
+
+
+async def test_dimmer_light_state(
+    hass: HomeAssistantType, dummy_device_from_host_dimmer
+) -> None:
+    """Test the change of state of the light switches."""
+    await setup_integration(hass)
+
+    await hass.services.async_call(
+        LIGHT_DOMAIN,
+        SERVICE_TURN_ON,
+        {ATTR_BRIGHTNESS: 42, ATTR_ENTITY_ID: "light.wl000000000099_1"},
+        blocking=True,
+    )
+    await hass.async_block_till_done()
+
+    state = hass.states.get("light.wl000000000099_1")
+    assert state
+    assert state.state == STATE_ON
+    assert state.attributes.get(ATTR_BRIGHTNESS) == 42
+
+    await hass.services.async_call(
+        LIGHT_DOMAIN,
+        SERVICE_TURN_ON,
+        {ATTR_BRIGHTNESS: 0, ATTR_ENTITY_ID: "light.wl000000000099_1"},
+        blocking=True,
+    )
+    await hass.async_block_till_done()
+
+    state = hass.states.get("light.wl000000000099_1")
+    assert state
+    assert state.state == STATE_OFF
+
+    await hass.services.async_call(
+        LIGHT_DOMAIN,
+        SERVICE_TURN_ON,
+        {ATTR_BRIGHTNESS: 100, ATTR_ENTITY_ID: "light.wl000000000099_1"},
+        blocking=True,
+    )
+    await hass.async_block_till_done()
+
+    state = hass.states.get("light.wl000000000099_1")
+    assert state
+    assert state.state == STATE_ON
+    assert state.attributes.get(ATTR_BRIGHTNESS) == 100
+
+    await hass.services.async_call(
+        LIGHT_DOMAIN,
+        SERVICE_TURN_OFF,
+        {ATTR_ENTITY_ID: "light.wl000000000099_1"},
+        blocking=True,
+    )
+
+    await hass.async_block_till_done()
+    state = hass.states.get("light.wl000000000099_1")
+    assert state
+    assert state.state == STATE_OFF
+
+    # Turn on
+    await hass.services.async_call(
+        LIGHT_DOMAIN,
+        SERVICE_TURN_ON,
+        {ATTR_ENTITY_ID: "light.wl000000000099_1"},
+        blocking=True,
+    )
+
+    await hass.async_block_till_done()
+    state = hass.states.get("light.wl000000000099_1")
+    assert state
+    assert state.state == STATE_ON
+
+
+async def test_color_light_state(
+    hass: HomeAssistantType, dummy_device_from_host_color
+) -> None:
+    """Test the change of state of the light switches."""
+    await setup_integration(hass)
+
+    await hass.services.async_call(
+        LIGHT_DOMAIN,
+        SERVICE_TURN_ON,
+        {
+            ATTR_BRIGHTNESS: 42,
+            ATTR_HS_COLOR: [0, 100],
+            ATTR_ENTITY_ID: "light.wl000000000099_1",
+        },
+        blocking=True,
+    )
+    await hass.async_block_till_done()
+
+    state = hass.states.get("light.wl000000000099_1")
+    assert state
+    assert state.state == STATE_ON
+    assert state.attributes.get(ATTR_BRIGHTNESS) == 42
+    state_color = [
+        round(state.attributes.get(ATTR_HS_COLOR)[0]),
+        round(state.attributes.get(ATTR_HS_COLOR)[1]),
+    ]
+    assert state_color == [0, 100]
+
+    await hass.services.async_call(
+        LIGHT_DOMAIN,
+        SERVICE_TURN_ON,
+        {ATTR_BRIGHTNESS: 0, ATTR_ENTITY_ID: "light.wl000000000099_1"},
+        blocking=True,
+    )
+    await hass.async_block_till_done()
+
+    state = hass.states.get("light.wl000000000099_1")
+    assert state
+    assert state.state == STATE_OFF
+
+    await hass.services.async_call(
+        LIGHT_DOMAIN,
+        SERVICE_TURN_ON,
+        {
+            ATTR_BRIGHTNESS: 100,
+            ATTR_HS_COLOR: [270, 50],
+            ATTR_ENTITY_ID: "light.wl000000000099_1",
+        },
+        blocking=True,
+    )
+    await hass.async_block_till_done()
+
+    state = hass.states.get("light.wl000000000099_1")
+    assert state
+    assert state.state == STATE_ON
+    assert state.attributes.get(ATTR_BRIGHTNESS) == 100
+    state_color = [
+        round(state.attributes.get(ATTR_HS_COLOR)[0]),
+        round(state.attributes.get(ATTR_HS_COLOR)[1]),
+    ]
+    assert state_color == [270, 50]
+
+    await hass.services.async_call(
+        LIGHT_DOMAIN,
+        SERVICE_TURN_OFF,
+        {ATTR_ENTITY_ID: "light.wl000000000099_1"},
+        blocking=True,
+    )
+
+    await hass.async_block_till_done()
+    state = hass.states.get("light.wl000000000099_1")
+    assert state
+    assert state.state == STATE_OFF
+
+    # Turn on
+    await hass.services.async_call(
+        LIGHT_DOMAIN,
+        SERVICE_TURN_ON,
+        {ATTR_ENTITY_ID: "light.wl000000000099_1"},
+        blocking=True,
+    )
+
+    await hass.async_block_till_done()
+    state = hass.states.get("light.wl000000000099_1")
+    assert state
+    assert state.state == STATE_ON
+
+    # Hue = 0, Saturation = 100
+    await hass.services.async_call(
+        LIGHT_DOMAIN,
+        SERVICE_TURN_ON,
+        {ATTR_HS_COLOR: [0, 100], ATTR_ENTITY_ID: "light.wl000000000099_1"},
+        blocking=True,
+    )
+    await hass.async_block_till_done()
+
+    state = hass.states.get("light.wl000000000099_1")
+    assert state
+    assert state.state == STATE_ON
+    state_color = [
+        round(state.attributes.get(ATTR_HS_COLOR)[0]),
+        round(state.attributes.get(ATTR_HS_COLOR)[1]),
+    ]
+    assert state_color == [0, 100]
+
+    # Brightness = 60
+    await hass.services.async_call(
+        LIGHT_DOMAIN,
+        SERVICE_TURN_ON,
+        {ATTR_BRIGHTNESS: 60, ATTR_ENTITY_ID: "light.wl000000000099_1"},
+        blocking=True,
+    )
+    await hass.async_block_till_done()
+
+    state = hass.states.get("light.wl000000000099_1")
+    assert state
+    assert state.state == STATE_ON
+    assert state.attributes.get(ATTR_BRIGHTNESS) == 60
-- 
GitLab