mirror of https://github.com/home-assistant/core
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.esphome.*
|
||||
homeassistant.components.energy.*
|
||||
homeassistant.components.evil_genius_labs.*
|
||||
homeassistant.components.fastdotcom.*
|
||||
homeassistant.components.fitbit.*
|
||||
homeassistant.components.flunearyou.*
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
)
|
|
@ -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."""
|
|
@ -0,0 +1,3 @@
|
|||
"""Constants for the Evil Genius Labs integration."""
|
||||
|
||||
DOMAIN = "evil_genius_labs"
|
|
@ -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)
|
|
@ -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"
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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",
|
||||
"epson",
|
||||
"esphome",
|
||||
"evil_genius_labs",
|
||||
"ezviz",
|
||||
"faa_delays",
|
||||
"fireservicerota",
|
||||
|
|
11
mypy.ini
11
mypy.ini
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
"""Tests for the Evil Genius Labs integration."""
|
|
@ -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
|
|
@ -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
|
||||
}
|
||||
]
|
|
@ -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"
|
||||
}
|
|
@ -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"}
|
|
@ -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
|
|
@ -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…
Reference in New Issue