Add Evil Genius Labs integration (#58720)

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
Paulus Schoutsen 2021-11-08 08:56:27 -08:00 committed by GitHub
parent 089353e949
commit 296f678d52
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 971 additions and 0 deletions

View File

@ -42,6 +42,7 @@ homeassistant.components.efergy.*
homeassistant.components.elgato.*
homeassistant.components.esphome.*
homeassistant.components.energy.*
homeassistant.components.evil_genius_labs.*
homeassistant.components.fastdotcom.*
homeassistant.components.fitbit.*
homeassistant.components.flunearyou.*

View File

@ -160,6 +160,7 @@ homeassistant/components/epson/* @pszafer
homeassistant/components/epsonworkforce/* @ThaStealth
homeassistant/components/eq3btsmart/* @rytilahti
homeassistant/components/esphome/* @OttoWinter @jesserockz
homeassistant/components/evil_genius_labs/* @balloob
homeassistant/components/evohome/* @zxdavb
homeassistant/components/ezviz/* @RenierM26 @baqs
homeassistant/components/faa_delays/* @ntilley905

View File

@ -0,0 +1,99 @@
"""The Evil Genius Labs integration."""
from __future__ import annotations
from datetime import timedelta
import logging
from typing import cast
from async_timeout import timeout
import pyevilgenius
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers import (
aiohttp_client,
device_registry as dr,
update_coordinator,
)
from homeassistant.helpers.entity import DeviceInfo
from .const import DOMAIN
PLATFORMS = ["light"]
UPDATE_INTERVAL = 10
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Evil Genius Labs from a config entry."""
coordinator = EvilGeniusUpdateCoordinator(
hass,
entry.title,
pyevilgenius.EvilGeniusDevice(
entry.data["host"], aiohttp_client.async_get_clientsession(hass)
),
)
await coordinator.async_config_entry_first_refresh()
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok
class EvilGeniusUpdateCoordinator(update_coordinator.DataUpdateCoordinator[dict]):
"""Update coordinator for Evil Genius data."""
info: dict
def __init__(
self, hass: HomeAssistant, name: str, client: pyevilgenius.EvilGeniusDevice
) -> None:
"""Initialize the data update coordinator."""
self.client = client
super().__init__(
hass,
logging.getLogger(__name__),
name=name,
update_interval=timedelta(seconds=UPDATE_INTERVAL),
)
@property
def device_name(self) -> str:
"""Return the device name."""
return cast(str, self.data["name"]["value"])
async def _async_update_data(self) -> dict:
"""Update Evil Genius data."""
if not hasattr(self, "info"):
async with timeout(5):
self.info = await self.client.get_info()
async with timeout(5):
return cast(dict, await self.client.get_data())
class EvilGeniusEntity(update_coordinator.CoordinatorEntity):
"""Base entity for Evil Genius."""
coordinator: EvilGeniusUpdateCoordinator
@property
def device_info(self) -> DeviceInfo:
"""Return device info."""
info = self.coordinator.info
return DeviceInfo(
identifiers={(DOMAIN, info["wiFiChipId"])},
connections={(dr.CONNECTION_NETWORK_MAC, info["macAddress"])},
name=self.coordinator.device_name,
manufacturer="Evil Genius Labs",
sw_version=info["coreVersion"].replace("_", "."),
configuration_url=self.coordinator.client.url,
)

View File

@ -0,0 +1,84 @@
"""Config flow for Evil Genius Labs integration."""
from __future__ import annotations
import logging
from typing import Any
import aiohttp
import pyevilgenius
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResult
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import aiohttp_client
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, Any]:
"""Validate the user input allows us to connect.
Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user.
"""
hub = pyevilgenius.EvilGeniusDevice(
data["host"], aiohttp_client.async_get_clientsession(hass)
)
try:
data = await hub.get_data()
info = await hub.get_info()
except aiohttp.ClientError as err:
raise CannotConnect from err
return {"title": data["name"]["value"], "unique_id": info["wiFiChipId"]}
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Evil Genius Labs."""
VERSION = 1
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle the initial step."""
if user_input is None:
return self.async_show_form(
step_id="user",
data_schema=vol.Schema(
{
vol.Required("host"): str,
}
),
)
errors = {}
try:
info = await validate_input(self.hass, user_input)
except CannotConnect:
errors["base"] = "cannot_connect"
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
await self.async_set_unique_id(info["unique_id"])
return self.async_create_entry(title=info["title"], data=user_input)
return self.async_show_form(
step_id="user",
data_schema=vol.Schema(
{
vol.Required("host", default=user_input["host"]): str,
}
),
errors=errors,
)
class CannotConnect(HomeAssistantError):
"""Error to indicate we cannot connect."""

View File

@ -0,0 +1,3 @@
"""Constants for the Evil Genius Labs integration."""
DOMAIN = "evil_genius_labs"

View File

@ -0,0 +1,120 @@
"""Light platform for Evil Genius Light."""
from __future__ import annotations
from typing import Any, cast
from async_timeout import timeout
from homeassistant.components import light
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import EvilGeniusEntity, EvilGeniusUpdateCoordinator
from .const import DOMAIN
from .util import update_when_done
HA_NO_EFFECT = "None"
FIB_NO_EFFECT = "Solid Color"
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the Evil Genius light platform."""
coordinator: EvilGeniusUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id]
async_add_entities([EvilGeniusLight(coordinator)])
class EvilGeniusLight(EvilGeniusEntity, light.LightEntity):
"""Evil Genius Labs light."""
_attr_supported_features = (
light.SUPPORT_BRIGHTNESS | light.SUPPORT_EFFECT | light.SUPPORT_COLOR
)
_attr_supported_color_modes = {light.COLOR_MODE_RGB}
_attr_color_mode = light.COLOR_MODE_RGB
def __init__(self, coordinator: EvilGeniusUpdateCoordinator) -> None:
"""Initialize the Evil Genius light."""
super().__init__(coordinator)
self._attr_unique_id = self.coordinator.info["wiFiChipId"]
self._attr_effect_list = [
pattern
for pattern in self.coordinator.data["pattern"]["options"]
if pattern != FIB_NO_EFFECT
]
self._attr_effect_list.insert(0, HA_NO_EFFECT)
@property
def name(self) -> str:
"""Return name."""
return cast(str, self.coordinator.data["name"]["value"])
@property
def is_on(self) -> bool:
"""Return if light is on."""
return cast(int, self.coordinator.data["power"]["value"]) == 1
@property
def brightness(self) -> int:
"""Return brightness."""
return cast(int, self.coordinator.data["brightness"]["value"])
@property
def rgb_color(self) -> tuple[int, int, int]:
"""Return the rgb color value [int, int, int]."""
return cast(
"tuple[int, int, int]",
tuple(
int(val)
for val in self.coordinator.data["solidColor"]["value"].split(",")
),
)
@property
def effect(self) -> str:
"""Return current effect."""
value = cast(
str,
self.coordinator.data["pattern"]["options"][
self.coordinator.data["pattern"]["value"]
],
)
if value == FIB_NO_EFFECT:
return HA_NO_EFFECT
return value
@update_when_done
async def async_turn_on(
self,
**kwargs: Any,
) -> None:
"""Turn light on."""
if (brightness := kwargs.get(light.ATTR_BRIGHTNESS)) is not None:
async with timeout(5):
await self.coordinator.client.set_path_value("brightness", brightness)
# Setting a color will change the effect to "Solid Color" so skip setting effect
if (rgb_color := kwargs.get(light.ATTR_RGB_COLOR)) is not None:
async with timeout(5):
await self.coordinator.client.set_rgb_color(*rgb_color)
elif (effect := kwargs.get(light.ATTR_EFFECT)) is not None:
if effect == HA_NO_EFFECT:
effect = FIB_NO_EFFECT
async with timeout(5):
await self.coordinator.client.set_path_value(
"pattern", self.coordinator.data["pattern"]["options"].index(effect)
)
async with timeout(5):
await self.coordinator.client.set_path_value("power", 1)
@update_when_done
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn light off."""
async with timeout(5):
await self.coordinator.client.set_path_value("power", 0)

View File

@ -0,0 +1,9 @@
{
"domain": "evil_genius_labs",
"name": "Evil Genius Labs",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/evil_genius_labs",
"requirements": ["pyevilgenius==1.0.0"],
"codeowners": ["@balloob"],
"iot_class": "local_polling"
}

View File

@ -0,0 +1,15 @@
{
"config": {
"step": {
"user": {
"data": {
"host": "[%key:common::config_flow::data::host%]"
}
}
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
}
}
}

View File

@ -0,0 +1,15 @@
{
"config": {
"error": {
"cannot_connect": "Failed to connect",
"unknown": "Unexpected error"
},
"step": {
"user": {
"data": {
"host": "Host"
}
}
}
}
}

View File

@ -0,0 +1,21 @@
"""Utilities for Evil Genius Labs."""
from collections.abc import Callable
from functools import wraps
from typing import Any, TypeVar, cast
from . import EvilGeniusEntity
CallableT = TypeVar("CallableT", bound=Callable)
def update_when_done(func: CallableT) -> CallableT:
"""Decorate function to trigger update when function is done."""
@wraps(func)
async def wrapper(self: EvilGeniusEntity, *args: Any, **kwargs: Any) -> Any:
"""Wrap function."""
result = await func(self, *args, **kwargs)
await self.coordinator.async_request_refresh()
return result
return cast(CallableT, wrapper)

View File

@ -82,6 +82,7 @@ FLOWS = [
"environment_canada",
"epson",
"esphome",
"evil_genius_labs",
"ezviz",
"faa_delays",
"fireservicerota",

View File

@ -473,6 +473,17 @@ no_implicit_optional = true
warn_return_any = true
warn_unreachable = true
[mypy-homeassistant.components.evil_genius_labs.*]
check_untyped_defs = true
disallow_incomplete_defs = true
disallow_subclassing_any = true
disallow_untyped_calls = true
disallow_untyped_decorators = true
disallow_untyped_defs = true
no_implicit_optional = true
warn_return_any = true
warn_unreachable = true
[mypy-homeassistant.components.fastdotcom.*]
check_untyped_defs = true
disallow_incomplete_defs = true

View File

@ -1470,6 +1470,9 @@ pyephember==0.3.1
# homeassistant.components.everlights
pyeverlights==0.1.0
# homeassistant.components.evil_genius_labs
pyevilgenius==1.0.0
# homeassistant.components.ezviz
pyezviz==0.1.9.4

View File

@ -867,6 +867,9 @@ pyefergy==0.1.4
# homeassistant.components.everlights
pyeverlights==0.1.0
# homeassistant.components.evil_genius_labs
pyevilgenius==1.0.0
# homeassistant.components.ezviz
pyezviz==0.1.9.4

View File

@ -0,0 +1 @@
"""Tests for the Evil Genius Labs integration."""

View File

@ -0,0 +1,49 @@
"""Test helpers for Evil Genius Labs."""
import json
from unittest.mock import patch
import pytest
from homeassistant.setup import async_setup_component
from tests.common import MockConfigEntry, load_fixture
@pytest.fixture(scope="session")
def data_fixture():
"""Fixture data."""
data = json.loads(load_fixture("data.json", "evil_genius_labs"))
return {item["name"]: item for item in data}
@pytest.fixture(scope="session")
def info_fixture():
"""Fixture info."""
return json.loads(load_fixture("info.json", "evil_genius_labs"))
@pytest.fixture
def config_entry(hass):
"""Evil genius labs config entry."""
entry = MockConfigEntry(domain="evil_genius_labs", data={"host": "192.168.1.113"})
entry.add_to_hass(hass)
return entry
@pytest.fixture
async def setup_evil_genius_labs(
hass, config_entry, data_fixture, info_fixture, platforms
):
"""Test up Evil Genius Labs instance."""
with patch(
"pyevilgenius.EvilGeniusDevice.get_data",
return_value=data_fixture,
), patch(
"pyevilgenius.EvilGeniusDevice.get_info",
return_value=info_fixture,
), patch(
"homeassistant.components.evil_genius_labs.PLATFORMS", platforms
):
assert await async_setup_component(hass, "evil_genius_labs", {})
await hass.async_block_till_done()
yield

View File

@ -0,0 +1,331 @@
[
{
"name": "name",
"label": "Name",
"type": "Label",
"value": "Fibonacci256-23D4"
},
{ "name": "power", "label": "Power", "type": "Boolean", "value": 1 },
{
"name": "brightness",
"label": "Brightness",
"type": "Number",
"value": 128,
"min": 1,
"max": 255
},
{
"name": "pattern",
"label": "Pattern",
"type": "Select",
"value": 70,
"options": [
"Pride",
"Pride Fibonacci",
"Color Waves",
"Color Waves Fibonacci",
"Pride Playground",
"Pride Playground Fibonacci",
"Color Waves Playground",
"Color Waves Playground Fibonacci",
"Wheel",
"Swirl Fibonacci",
"Fire Fibonacci",
"Water Fibonacci",
"Emitter Fibonacci",
"Pacifica",
"Pacifica Fibonacci",
"Angle Palette",
"Radius Palette",
"X Axis Palette",
"Y Axis Palette",
"XY Axis Palette",
"Angle Gradient Palette",
"Radius Gradient Palette",
"X Axis Gradient Palette",
"Y Axis Gradient Palette",
"XY Axis Gradient Palette",
"Fire Noise",
"Fire Noise 2",
"Lava Noise",
"Rainbow Noise",
"Rainbow Stripe Noise",
"Party Noise",
"Forest Noise",
"Cloud Noise",
"Ocean Noise",
"Black & White Noise",
"Black & Blue Noise",
"Analog Clock",
"Spiral Analog Clock 13",
"Spiral Analog Clock 21",
"Spiral Analog Clock 34",
"Spiral Analog Clock 55",
"Spiral Analog Clock 89",
"Spiral Analog Clock 21 & 34",
"Spiral Analog Clock 13, 21 & 34",
"Spiral Analog Clock 34, 21 & 13",
"Pride Playground",
"Color Waves Playground",
"Rainbow Twinkles",
"Snow Twinkles",
"Cloud Twinkles",
"Incandescent Twinkles",
"Retro C9 Twinkles",
"Red & White Twinkles",
"Blue & White Twinkles",
"Red, Green & White Twinkles",
"Fairy Light Twinkles",
"Snow 2 Twinkles",
"Holly Twinkles",
"Ice Twinkles",
"Party Twinkles",
"Forest Twinkles",
"Lava Twinkles",
"Fire Twinkles",
"Cloud 2 Twinkles",
"Ocean Twinkles",
"Rainbow",
"Rainbow With Glitter",
"Solid Rainbow",
"Confetti",
"Sinelon",
"Beat",
"Juggle",
"Fire",
"Water",
"Strand Test",
"Solid Color"
]
},
{
"name": "palette",
"label": "Palette",
"type": "Select",
"value": 0,
"options": [
"Rainbow",
"Rainbow Stripe",
"Cloud",
"Lava",
"Ocean",
"Forest",
"Party",
"Heat"
]
},
{
"name": "speed",
"label": "Speed",
"type": "Number",
"value": 30,
"min": 1,
"max": 255
},
{ "name": "autoplaySection", "label": "Autoplay", "type": "Section" },
{ "name": "autoplay", "label": "Autoplay", "type": "Boolean", "value": 0 },
{
"name": "autoplayDuration",
"label": "Autoplay Duration",
"type": "Number",
"value": 10,
"min": 0,
"max": 255
},
{ "name": "clock", "label": "Clock", "type": "Section" },
{ "name": "showClock", "label": "Show Clock", "type": "Boolean", "value": 0 },
{
"name": "clockBackgroundFade",
"label": "Background Fade",
"type": "Number",
"value": 240,
"min": 0,
"max": 255
},
{ "name": "solidColorSection", "label": "Solid Color", "type": "Section" },
{
"name": "solidColor",
"label": "Color",
"type": "Color",
"value": "0,0,255"
},
{ "name": "prideSection", "label": "Pride & ColorWaves", "type": "Section" },
{
"name": "saturationBpm",
"label": "Saturation BPM",
"type": "Number",
"value": 87,
"min": 0,
"max": 255
},
{
"name": "saturationMin",
"label": "Saturation Min",
"type": "Number",
"value": 220,
"min": 0,
"max": 255
},
{
"name": "saturationMax",
"label": "Saturation Max",
"type": "Number",
"value": 250,
"min": 0,
"max": 255
},
{
"name": "brightDepthBpm",
"label": "Brightness Depth BPM",
"type": "Number",
"value": 1,
"min": 0,
"max": 255
},
{
"name": "brightDepthMin",
"label": "Brightness Depth Min",
"type": "Number",
"value": 96,
"min": 0,
"max": 255
},
{
"name": "brightDepthMax",
"label": "Brightness Depth Max",
"type": "Number",
"value": 224,
"min": 0,
"max": 255
},
{
"name": "brightThetaIncBpm",
"label": "Bright Theta Inc BPM",
"type": "Number",
"value": 203,
"min": 0,
"max": 255
},
{
"name": "brightThetaIncMin",
"label": "Bright Theta Inc Min",
"type": "Number",
"value": 25,
"min": 0,
"max": 255
},
{
"name": "brightThetaIncMax",
"label": "Bright Theta Inc Max",
"type": "Number",
"value": 40,
"min": 0,
"max": 255
},
{
"name": "msMultiplierBpm",
"label": "Time Multiplier BPM",
"type": "Number",
"value": 147,
"min": 0,
"max": 255
},
{
"name": "msMultiplierMin",
"label": "Time Multiplier Min",
"type": "Number",
"value": 23,
"min": 0,
"max": 255
},
{
"name": "msMultiplierMax",
"label": "Time Multiplier Max",
"type": "Number",
"value": 60,
"min": 0,
"max": 255
},
{
"name": "hueIncBpm",
"label": "Hue Inc BPM",
"type": "Number",
"value": 113,
"min": 0,
"max": 255
},
{
"name": "hueIncMin",
"label": "Hue Inc Min",
"type": "Number",
"value": 1,
"min": 0,
"max": 255
},
{
"name": "hueIncMax",
"label": "Hue Inc Max",
"type": "Number",
"value": 12,
"min": 0,
"max": 255
},
{
"name": "sHueBpm",
"label": "S Hue BPM",
"type": "Number",
"value": 2,
"min": 0,
"max": 255
},
{
"name": "sHueMin",
"label": "S Hue Min",
"type": "Number",
"value": 5,
"min": 0,
"max": 255
},
{
"name": "sHueMax",
"label": "S Hue Max",
"type": "Number",
"value": 9,
"min": 0,
"max": 255
},
{ "name": "fireSection", "label": "Fire & Water", "type": "Section" },
{
"name": "cooling",
"label": "Cooling",
"type": "Number",
"value": 49,
"min": 0,
"max": 255
},
{
"name": "sparking",
"label": "Sparking",
"type": "Number",
"value": 60,
"min": 0,
"max": 255
},
{ "name": "twinklesSection", "label": "Twinkles", "type": "Section" },
{
"name": "twinkleSpeed",
"label": "Twinkle Speed",
"type": "Number",
"value": 4,
"min": 0,
"max": 8
},
{
"name": "twinkleDensity",
"label": "Twinkle Density",
"type": "Number",
"value": 5,
"min": 0,
"max": 8
}
]

View File

@ -0,0 +1,30 @@
{
"millis": 62099724,
"vcc": 3005,
"wiFiChipId": "1923d4",
"flashChipId": "1640d8",
"flashChipSize": 4194304,
"flashChipRealSize": 4194304,
"sdkVersion": "2.2.2-dev(38a443e)",
"coreVersion": "2_7_4",
"bootVersion": 6,
"cpuFreqMHz": 160,
"freeHeap": 21936,
"sketchSize": 476352,
"freeSketchSpace": 1617920,
"resetReason": "External System",
"isConnected": true,
"wiFiSsidDefault": "My Wi-Fi",
"wiFiSSID": "My Wi-Fi",
"localIP": "192.168.1.113",
"gatewayIP": "192.168.1.1",
"subnetMask": "255.255.255.0",
"dnsIP": "192.168.1.1",
"hostname": "ESP-1923D4",
"macAddress": "BC:FF:4D:19:23:D4",
"autoConnect": true,
"softAPSSID": "FaryLink_1923D4",
"softAPIP": "(IP unset)",
"BSSID": "FC:EC:DA:77:1A:CE",
"softAPmacAddress": "BE:FF:4D:19:23:D4"
}

View File

@ -0,0 +1,85 @@
"""Test the Evil Genius Labs config flow."""
from unittest.mock import patch
import aiohttp
from homeassistant import config_entries
from homeassistant.components.evil_genius_labs.const import DOMAIN
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM
async def test_form(hass: HomeAssistant, data_fixture, info_fixture) -> None:
"""Test we get the form."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == RESULT_TYPE_FORM
assert result["errors"] is None
with patch(
"pyevilgenius.EvilGeniusDevice.get_data",
return_value=data_fixture,
), patch(
"pyevilgenius.EvilGeniusDevice.get_info",
return_value=info_fixture,
), patch(
"homeassistant.components.evil_genius_labs.async_setup_entry",
return_value=True,
) as mock_setup_entry:
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
"host": "1.1.1.1",
},
)
await hass.async_block_till_done()
assert result2["type"] == RESULT_TYPE_CREATE_ENTRY
assert result2["title"] == "Fibonacci256-23D4"
assert result2["data"] == {
"host": "1.1.1.1",
}
assert len(mock_setup_entry.mock_calls) == 1
async def test_form_cannot_connect(hass: HomeAssistant) -> None:
"""Test we handle cannot connect error."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
with patch(
"pyevilgenius.EvilGeniusDevice.get_data",
side_effect=aiohttp.ClientError,
):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
"host": "1.1.1.1",
},
)
assert result2["type"] == RESULT_TYPE_FORM
assert result2["errors"] == {"base": "cannot_connect"}
async def test_form_unknown(hass: HomeAssistant) -> None:
"""Test we handle unknown error."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
with patch(
"pyevilgenius.EvilGeniusDevice.get_data",
side_effect=ValueError("BOOM"),
):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
"host": "1.1.1.1",
},
)
assert result2["type"] == RESULT_TYPE_FORM
assert result2["errors"] == {"base": "unknown"}

View File

@ -0,0 +1,13 @@
"""Test evil genius labs init."""
import pytest
from homeassistant import config_entries
from homeassistant.components.evil_genius_labs import PLATFORMS
@pytest.mark.parametrize("platforms", [PLATFORMS])
async def test_setup_unload_entry(hass, setup_evil_genius_labs, config_entry):
"""Test setting up and unloading a config entry."""
assert len(hass.states.async_entity_ids()) == 1
assert await hass.config_entries.async_unload(config_entry.entry_id)
assert config_entry.state == config_entries.ConfigEntryState.NOT_LOADED

View File

@ -0,0 +1,76 @@
"""Test Evil Genius Labs light."""
from unittest.mock import patch
import pytest
@pytest.mark.parametrize("platforms", [("light",)])
async def test_works(hass, setup_evil_genius_labs):
"""Test it works."""
state = hass.states.get("light.fibonacci256_23d4")
assert state is not None
assert state.state == "on"
assert state.attributes["brightness"] == 128
@pytest.mark.parametrize("platforms", [("light",)])
async def test_turn_on_color(hass, setup_evil_genius_labs):
"""Test turning on with a color."""
with patch(
"pyevilgenius.EvilGeniusDevice.set_path_value"
) as mock_set_path_value, patch(
"pyevilgenius.EvilGeniusDevice.set_rgb_color"
) as mock_set_rgb_color:
await hass.services.async_call(
"light",
"turn_on",
{
"entity_id": "light.fibonacci256_23d4",
"brightness": 100,
"rgb_color": (10, 20, 30),
},
blocking=True,
)
assert len(mock_set_path_value.mock_calls) == 2
mock_set_path_value.mock_calls[0][1] == ("brightness", 100)
mock_set_path_value.mock_calls[1][1] == ("power", 1)
assert len(mock_set_rgb_color.mock_calls) == 1
mock_set_rgb_color.mock_calls[0][1] == (10, 20, 30)
@pytest.mark.parametrize("platforms", [("light",)])
async def test_turn_on_effect(hass, setup_evil_genius_labs):
"""Test turning on with an effect."""
with patch("pyevilgenius.EvilGeniusDevice.set_path_value") as mock_set_path_value:
await hass.services.async_call(
"light",
"turn_on",
{
"entity_id": "light.fibonacci256_23d4",
"effect": "Pride Playground",
},
blocking=True,
)
assert len(mock_set_path_value.mock_calls) == 2
mock_set_path_value.mock_calls[0][1] == ("pattern", 4)
mock_set_path_value.mock_calls[1][1] == ("power", 1)
@pytest.mark.parametrize("platforms", [("light",)])
async def test_turn_off(hass, setup_evil_genius_labs):
"""Test turning off."""
with patch("pyevilgenius.EvilGeniusDevice.set_path_value") as mock_set_path_value:
await hass.services.async_call(
"light",
"turn_off",
{
"entity_id": "light.fibonacci256_23d4",
},
blocking=True,
)
assert len(mock_set_path_value.mock_calls) == 1
mock_set_path_value.mock_calls[0][1] == ("power", 0)