mirror of
https://github.com/home-assistant/core.git
synced 2025-07-21 04:07:08 +00:00
Add Evil Genius Labs integration (#58720)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
parent
089353e949
commit
296f678d52
@ -42,6 +42,7 @@ homeassistant.components.efergy.*
|
|||||||
homeassistant.components.elgato.*
|
homeassistant.components.elgato.*
|
||||||
homeassistant.components.esphome.*
|
homeassistant.components.esphome.*
|
||||||
homeassistant.components.energy.*
|
homeassistant.components.energy.*
|
||||||
|
homeassistant.components.evil_genius_labs.*
|
||||||
homeassistant.components.fastdotcom.*
|
homeassistant.components.fastdotcom.*
|
||||||
homeassistant.components.fitbit.*
|
homeassistant.components.fitbit.*
|
||||||
homeassistant.components.flunearyou.*
|
homeassistant.components.flunearyou.*
|
||||||
|
@ -160,6 +160,7 @@ homeassistant/components/epson/* @pszafer
|
|||||||
homeassistant/components/epsonworkforce/* @ThaStealth
|
homeassistant/components/epsonworkforce/* @ThaStealth
|
||||||
homeassistant/components/eq3btsmart/* @rytilahti
|
homeassistant/components/eq3btsmart/* @rytilahti
|
||||||
homeassistant/components/esphome/* @OttoWinter @jesserockz
|
homeassistant/components/esphome/* @OttoWinter @jesserockz
|
||||||
|
homeassistant/components/evil_genius_labs/* @balloob
|
||||||
homeassistant/components/evohome/* @zxdavb
|
homeassistant/components/evohome/* @zxdavb
|
||||||
homeassistant/components/ezviz/* @RenierM26 @baqs
|
homeassistant/components/ezviz/* @RenierM26 @baqs
|
||||||
homeassistant/components/faa_delays/* @ntilley905
|
homeassistant/components/faa_delays/* @ntilley905
|
||||||
|
99
homeassistant/components/evil_genius_labs/__init__.py
Normal file
99
homeassistant/components/evil_genius_labs/__init__.py
Normal 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,
|
||||||
|
)
|
84
homeassistant/components/evil_genius_labs/config_flow.py
Normal file
84
homeassistant/components/evil_genius_labs/config_flow.py
Normal 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."""
|
3
homeassistant/components/evil_genius_labs/const.py
Normal file
3
homeassistant/components/evil_genius_labs/const.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
"""Constants for the Evil Genius Labs integration."""
|
||||||
|
|
||||||
|
DOMAIN = "evil_genius_labs"
|
120
homeassistant/components/evil_genius_labs/light.py
Normal file
120
homeassistant/components/evil_genius_labs/light.py
Normal 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)
|
9
homeassistant/components/evil_genius_labs/manifest.json
Normal file
9
homeassistant/components/evil_genius_labs/manifest.json
Normal 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"
|
||||||
|
}
|
15
homeassistant/components/evil_genius_labs/strings.json
Normal file
15
homeassistant/components/evil_genius_labs/strings.json
Normal 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%]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"error": {
|
||||||
|
"cannot_connect": "Failed to connect",
|
||||||
|
"unknown": "Unexpected error"
|
||||||
|
},
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"host": "Host"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
21
homeassistant/components/evil_genius_labs/util.py
Normal file
21
homeassistant/components/evil_genius_labs/util.py
Normal 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)
|
@ -82,6 +82,7 @@ FLOWS = [
|
|||||||
"environment_canada",
|
"environment_canada",
|
||||||
"epson",
|
"epson",
|
||||||
"esphome",
|
"esphome",
|
||||||
|
"evil_genius_labs",
|
||||||
"ezviz",
|
"ezviz",
|
||||||
"faa_delays",
|
"faa_delays",
|
||||||
"fireservicerota",
|
"fireservicerota",
|
||||||
|
11
mypy.ini
11
mypy.ini
@ -473,6 +473,17 @@ no_implicit_optional = true
|
|||||||
warn_return_any = true
|
warn_return_any = true
|
||||||
warn_unreachable = 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.*]
|
[mypy-homeassistant.components.fastdotcom.*]
|
||||||
check_untyped_defs = true
|
check_untyped_defs = true
|
||||||
disallow_incomplete_defs = true
|
disallow_incomplete_defs = true
|
||||||
|
@ -1470,6 +1470,9 @@ pyephember==0.3.1
|
|||||||
# homeassistant.components.everlights
|
# homeassistant.components.everlights
|
||||||
pyeverlights==0.1.0
|
pyeverlights==0.1.0
|
||||||
|
|
||||||
|
# homeassistant.components.evil_genius_labs
|
||||||
|
pyevilgenius==1.0.0
|
||||||
|
|
||||||
# homeassistant.components.ezviz
|
# homeassistant.components.ezviz
|
||||||
pyezviz==0.1.9.4
|
pyezviz==0.1.9.4
|
||||||
|
|
||||||
|
@ -867,6 +867,9 @@ pyefergy==0.1.4
|
|||||||
# homeassistant.components.everlights
|
# homeassistant.components.everlights
|
||||||
pyeverlights==0.1.0
|
pyeverlights==0.1.0
|
||||||
|
|
||||||
|
# homeassistant.components.evil_genius_labs
|
||||||
|
pyevilgenius==1.0.0
|
||||||
|
|
||||||
# homeassistant.components.ezviz
|
# homeassistant.components.ezviz
|
||||||
pyezviz==0.1.9.4
|
pyezviz==0.1.9.4
|
||||||
|
|
||||||
|
1
tests/components/evil_genius_labs/__init__.py
Normal file
1
tests/components/evil_genius_labs/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
"""Tests for the Evil Genius Labs integration."""
|
49
tests/components/evil_genius_labs/conftest.py
Normal file
49
tests/components/evil_genius_labs/conftest.py
Normal 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
|
331
tests/components/evil_genius_labs/fixtures/data.json
Normal file
331
tests/components/evil_genius_labs/fixtures/data.json
Normal 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
|
||||||
|
}
|
||||||
|
]
|
30
tests/components/evil_genius_labs/fixtures/info.json
Normal file
30
tests/components/evil_genius_labs/fixtures/info.json
Normal 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"
|
||||||
|
}
|
85
tests/components/evil_genius_labs/test_config_flow.py
Normal file
85
tests/components/evil_genius_labs/test_config_flow.py
Normal 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"}
|
13
tests/components/evil_genius_labs/test_init.py
Normal file
13
tests/components/evil_genius_labs/test_init.py
Normal 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
|
76
tests/components/evil_genius_labs/test_light.py
Normal file
76
tests/components/evil_genius_labs/test_light.py
Normal 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)
|
Loading…
x
Reference in New Issue
Block a user