Skip to content

Commit

Permalink
Merge pull request #15 from dmamontov/dev
Browse files Browse the repository at this point in the history
Add support version 2.x
  • Loading branch information
dmamontov authored Sep 1, 2022
2 parents 53c5123 + 0c2d766 commit 2d645c2
Show file tree
Hide file tree
Showing 38 changed files with 13,910 additions and 248 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
Component for deep integration [LedFx](https://github.com/LedFx/LedFx) from [Home Assistant](https://www.home-assistant.io/).

## Requirements
* LedFx version [0.10.7](https://github.com/LedFx/LedFx/releases/tag/v0.10.7)
* LedFx version >= [0.10.7](https://github.com/LedFx/LedFx/releases/tag/v0.10.7)

## Important information
* ❗ Effect controls (number, switch, select) are disabled by default. They must be enabled manually.
Expand Down
74 changes: 61 additions & 13 deletions custom_components/ledfx/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,14 +76,14 @@ async def request(
path: str,
method: Method = Method.GET,
body: dict | None = None,
validate_field: str = "status",
validate_field: str | tuple = "status",
) -> dict:
"""Request method.
:param path: str: api path
:param method: Method: api method
:param body: dict | None: api body
:param validate_field: str: validate field
:param validate_field: str | tuple: validate field
:return dict: dict with api data.
"""

Expand Down Expand Up @@ -122,7 +122,10 @@ async def request(

raise LedFxRequestError("Request error")

if validate_field not in _data: # pragma: no cover
if (isinstance(validate_field, str) and validate_field not in _data) or (
isinstance(validate_field, tuple)
and not any(key for key in validate_field if key in _data)
): # pragma: no cover
self._debug("Invalid response received", _url, _data, path)

raise LedFxRequestError("Request error")
Expand All @@ -145,6 +148,14 @@ async def devices(self) -> dict:

return await self.request("devices")

async def virtuals(self) -> dict:
"""virtuals method.
:return dict: dict with api data.
"""

return await self.request("virtuals")

async def scenes(self) -> dict:
"""scenes method.
Expand Down Expand Up @@ -175,71 +186,108 @@ async def config(self) -> dict:
:return dict: dict with api data.
"""

return await self.request("config", validate_field="config")
return await self.request(
"config", validate_field=("config", "configuration_version")
)

async def colors(self) -> dict:
"""colors method.
async def device_on(self, device_code: str, effect: str) -> dict:
:return dict: dict with api data.
"""

return await self.request("colors", validate_field="colors")

async def device_on(
self, device_code: str, effect: str, is_virtual: bool = False
) -> dict:
"""devices/effects on method.
:param device_code: str: device code
:param effect: str: effect code
:param is_virtual: bool: Is virtual device
:return dict: dict with api data.
"""

prefix: str = "virtuals" if is_virtual else "devices"

return await self.request(
f"devices/{device_code}/effects",
f"{prefix}/{device_code}/effects",
Method.POST,
{"config": {"active": True}, "type": effect},
)

async def device_off(self, device_code: str) -> dict:
async def device_off(self, device_code: str, is_virtual: bool = False) -> dict:
"""devices/effects off method.
:param device_code: str: device code
:param is_virtual: bool: Is virtual device
:return dict: dict with api data.
"""

return await self.request(f"devices/{device_code}/effects", Method.DELETE)
prefix: str = "virtuals" if is_virtual else "devices"

return await self.request(f"{prefix}/{device_code}/effects", Method.DELETE)

async def preset(
self, device_code: str, category: str, effect: str, preset: str
self,
device_code: str,
category: str,
effect: str,
preset: str,
is_virtual: bool = False,
) -> dict:
"""devices/presets on method.
:param device_code: str: device code
:param category: str: preset category
:param effect: str: effect code
:param preset: str: preset code
:param is_virtual: bool: Is virtual device
:return dict: dict with api data.
"""

prefix: str = "virtuals" if is_virtual else "devices"

return await self.request(
f"devices/{device_code}/presets",
f"{prefix}/{device_code}/presets",
Method.PUT,
{"category": category, "effect_id": effect, "preset_id": preset},
)

async def effect(self, device_code: str, effect: str, config: dict) -> dict:
async def effect(
self, device_code: str, effect: str, config: dict, is_virtual: bool = False
) -> dict:
"""devices/effects update method.
:param device_code: str: device code
:param effect: str: effect code
:param config: dict: effect config
:param is_virtual: bool: Is virtual device
:return dict: dict with api data.
"""

prefix: str = "virtuals" if is_virtual else "devices"

return await self.request(
f"devices/{device_code}/effects",
f"{prefix}/{device_code}/effects",
Method.PUT,
{"config": config, "type": effect},
)

async def set_audio_device(self, index: int) -> dict:
async def set_audio_device(self, index: int, is_new: bool = False) -> dict:
"""audio/devices set method.
:param index: int: device index
:param is_new: bool: Is new api
:return dict: dict with api data.
"""

if is_new:
return await self.request(
"config", Method.PUT, {"audio": {"audio_device": index}}
)

return await self.request("audio/devices", Method.PUT, {"index": index})

async def run_scene(self, scene_id: str) -> dict:
Expand Down
5 changes: 5 additions & 0 deletions custom_components/ledfx/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
ATTR_DEVICE: Final = "device"
ATTR_DEVICE_SW_VERSION: Final = "device_sw_version"
ATTR_FIELD: Final = "field"
ATTR_FIELD_TYPE: Final = "type"
ATTR_FIELD_EFFECTS: Final = "effects"
ATTR_FIELD_OPTIONS: Final = "options"

Expand All @@ -67,6 +68,7 @@
"""Light attributes"""
ATTR_LIGHT_STATE: Final = "state"
ATTR_LIGHT_BRIGHTNESS: Final = "brightness"
ATTR_LIGHT_COLOR: Final = "color"
ATTR_LIGHT_CONFIG: Final = "config"
ATTR_LIGHT_EFFECT: Final = "effect"
ATTR_LIGHT_EFFECT_CONFIG: Final = "effect_config"
Expand All @@ -79,6 +81,9 @@
"fft_size": "mdi:numeric",
"mic_rate": "mdi:microphone-settings",
"host_api": "mdi:api",
"sample_rate": "mdi:numeric",
"min_volume": "mdi:volume-minus",
"delay_ms": "mdi:sleep",
}

SELECT_ICONS: Final = {
Expand Down
44 changes: 41 additions & 3 deletions custom_components/ledfx/entity.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
"""MiWifi entity."""
"""LedFx entity."""

from __future__ import annotations

import copy
import logging
from typing import Any

Expand All @@ -10,12 +11,15 @@
from homeassistant.helpers.update_coordinator import CoordinatorEntity

from .const import (
ATTR_FIELD_TYPE,
ATTR_LIGHT_BRIGHTNESS,
ATTR_LIGHT_COLOR,
ATTR_LIGHT_EFFECT,
ATTR_LIGHT_EFFECT_CONFIG,
ATTR_STATE,
ATTRIBUTION,
)
from .enum import Version
from .helper import generate_entity_id
from .updater import LedFxUpdater, convert_brightness

Expand All @@ -27,6 +31,7 @@ class LedFxEntity(CoordinatorEntity):

_attr_attribution: str = ATTRIBUTION
_attr_device_code: str | None = None
_attr_field_type: str | None = None

def __init__(
self,
Expand Down Expand Up @@ -106,9 +111,17 @@ async def async_update_effect(
)
)
}
| {code: value}
)

if self._updater.version == Version.V2:
config |= {
"background_color": self._updater.data.get(
f"{self._attr_device_code}_{ATTR_LIGHT_COLOR}"
)
}

config |= {code: value}

effect: str | None = self._updater.data.get(
f"{self._attr_device_code}_{ATTR_LIGHT_EFFECT}"
)
Expand All @@ -117,7 +130,8 @@ async def async_update_effect(
await self._updater.client.effect(
self._attr_device_code,
effect,
config,
self._convert_config(config, code, value),
self._updater.version == Version.V2,
)

self._updater.data[
Expand All @@ -127,3 +141,27 @@ async def async_update_effect(
}

self._updater.async_update_listeners()

def _convert_config(self, config: dict, code: str, value: Any) -> dict:
"""Convert config
:param config: dict
:param code: str: Code
:param value: Any: Value
:return dict
"""

result: dict = copy.deepcopy(config)

if (
code in self._updater.effect_properties
and self._updater.effect_properties[code][ATTR_FIELD_TYPE] == "color"
): # pragma: no cover
if value in self._updater.colors:
result[code] = self._updater.colors[value]
elif value in self._updater.gradients:
result[code] = self._updater.gradients[value]
else:
del result[code]

return result
7 changes: 7 additions & 0 deletions custom_components/ledfx/enum.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@ class Method(str, Enum):
DELETE = "DELETE"


class Version(Enum):
"""Version enum"""

V1 = 1
V2 = 2


class ActionType(str, Enum):
"""ActionType enum"""

Expand Down
68 changes: 55 additions & 13 deletions custom_components/ledfx/helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,9 +144,11 @@ def build_effects(

for effect in effects:
full_effects.append(effect)
full_effects += [f"* {preset}" for preset in default_presets.get(effect, [])]
full_effects += [
f"** {preset}"
f"{effect} - {preset}" for preset in default_presets.get(effect, [])
]
full_effects += [
f"{effect} - {preset}"
for preset in custom_presets.get(effect, [])
if effect in custom_presets
]
Expand All @@ -165,16 +167,56 @@ def find_effect(
:return tuple[str | None, str | None, EffectCategory]
"""

if "**" in effect:
effect = effect.replace("** ", "")
for code, presets in custom_presets.items():
if effect in presets:
return code, effect, EffectCategory.CUSTOM
preset: str | None = None
category: EffectCategory = EffectCategory.NONE

if " - " in effect:
chunk: list = effect.split(" - ")
effect = chunk[0]
preset = " - ".join(chunk[1:])

if effect in custom_presets and preset in custom_presets[effect]:
category = EffectCategory.CUSTOM
elif effect in default_presets and preset in default_presets[effect]:
category = EffectCategory.DEFAULT

return effect, preset, category


def hex_to_rgbw(
color: str | None,
) -> tuple[int, int, int, int] | None: # pragma: no cover
"""Convert hex color to rgbw
:param color: str | None
:return tuple[int, int, int, int] | None
"""

if not color:
return None

color = color.lstrip("#")

if color == "ffffff":
return 0, 0, 0, 255

if color == "000000":
return 0, 0, 0, 0

return tuple([int(color[i : i + 2], 16) for i in (0, 2, 4)] + [0]) # type: ignore


def rgbw_to_hex(color: tuple[int, int, int, int] | None) -> str | None:
"""Convert hex color to rgbw
:param color: tuple[int, int, int, int] | None
:return str
"""

if not color: # pragma: no cover
return None

if "*" in effect:
effect = effect.replace("* ", "")
for code, presets in default_presets.items():
if effect in presets:
return code, effect, EffectCategory.DEFAULT
if color[0] == 0 and color[1] == 0 and color[2] == 0:
return "#ffffff" if color[3] > 0 else "#000000"

return effect, None, EffectCategory.NONE
return "#%02x%02x%02x" % color[:3] # pylint: disable=consider-using-f-string
Loading

0 comments on commit 2d645c2

Please sign in to comment.