Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add LedSC integration #116957

Closed
wants to merge 29 commits into from
Closed
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
a33bbb6
Add LedSC integration
PatrikKr010 May 6, 2024
b6f4d36
The code formatted using Ruff
PatrikKr010 May 8, 2024
d2f0009
Add docstrings and clean code.
PatrikKr010 May 8, 2024
78997ac
update manifest and call script.gen_requirements_all
PatrikKr010 May 8, 2024
24449bd
Merge branch 'dev' into dev
PatrikKr010 May 8, 2024
57564cb
LedSC: Update documentation link
PatrikKr010 May 8, 2024
cd78812
Merge remote-tracking branch 'origin/dev' into dev
PatrikKr010 May 8, 2024
88067b1
Merge branch 'dev' into dev
PatrikKr010 May 8, 2024
2103aab
Merge branch 'dev' into dev
PatrikKr010 May 8, 2024
a138c21
Merge branch 'dev' into dev
PatrikKr010 May 9, 2024
fbf365e
Merge branch 'dev' into dev
PatrikKr010 May 12, 2024
958b7ae
Merge branch 'dev' into dev
PatrikKr010 May 29, 2024
a01e9e9
Added abstraction layer for communication with WebSC
PatrikKr010 May 29, 2024
3b4c752
Use cv.port to validate port number
PatrikKr010 May 30, 2024
678243a
Fix docstrings & code formatting
PatrikKr010 May 30, 2024
50f5892
Fix docstrings & code formatting
PatrikKr010 May 30, 2024
2a5e9af
Add untested files to .coveragerc
PatrikKr010 May 30, 2024
b77ff3a
Set key in error as `cannot_connect`
PatrikKr010 May 31, 2024
06057d2
Optimize imports & Exceptions & up websc-client to version 1.0.2 & Fi…
PatrikKr010 May 31, 2024
823aecd
Remove default host in config flow.
PatrikKr010 May 31, 2024
bf0e38c
Fix tests && Up websc-client to 1.0.3
PatrikKr010 Jun 2, 2024
bc6eed7
Update config_flow && light id.
PatrikKr010 Jun 2, 2024
03b6e44
Add strings.json
PatrikKr010 Jun 2, 2024
5ef977e
Merge branch 'dev' into dev
PatrikKr010 Jun 2, 2024
2338692
Up websc-client to 1.0.4
PatrikKr010 Jun 2, 2024
f5cd6fe
Merge branch 'dev' into dev
PatrikKr010 Jun 10, 2024
4cb5e8e
Merge branch 'dev' into dev
PatrikKr010 Jun 18, 2024
deca64e
Update homeassistant/components/ledsc/strings.json
PatrikKr010 Jun 27, 2024
036288c
Cleaning & Docs
PatrikKr010 Jun 27, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -768,6 +768,7 @@ build.json @home-assistant/supervisor
/tests/components/leaone/ @bdraco
/homeassistant/components/led_ble/ @bdraco
/tests/components/led_ble/ @bdraco
/homeassistant/components/ledsc/ @PatrikKr010
/homeassistant/components/lg_netcast/ @Drafteed @splinter98
/tests/components/lg_netcast/ @Drafteed @splinter98
/homeassistant/components/lidarr/ @tkdrob
Expand Down
43 changes: 43 additions & 0 deletions homeassistant/components/ledsc/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
"""Platform for LedSC integration."""

from __future__ import annotations

import logging

import voluptuous as vol

from homeassistant.components.light import PLATFORM_SCHEMA
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, CONF_PORT
from homeassistant.core import HomeAssistant
import homeassistant.helpers.config_validation as cv

from .consts import PLATFORMS

_LOGGER = logging.getLogger(__name__)
PatrikKr010 marked this conversation as resolved.
Show resolved Hide resolved

# Validation of the user's configuration
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_PORT): cv.string,
PatrikKr010 marked this conversation as resolved.
Show resolved Hide resolved
}
)


async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up LedSC from a config entry."""
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
entry.async_on_unload(entry.add_update_listener(options_update_listener))
return True


async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)


async def options_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Handle options update."""
_LOGGER.debug("Configuration options updated, reloading LedSC integration")
await hass.config_entries.async_reload(entry.entry_id)
67 changes: 67 additions & 0 deletions homeassistant/components/ledsc/config_flow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
"""Config flow for LedSC integration."""

from __future__ import annotations

import logging
from typing import Any
import voluptuous as vol
from websc_client import WebSClientAsync as WebSClient

from homeassistant import config_entries
PatrikKr010 marked this conversation as resolved.
Show resolved Hide resolved
from homeassistant.const import CONF_HOST, CONF_PORT
from homeassistant.core import HomeAssistant
from homeassistant.config_entries import ConfigFlowResult
import homeassistant.helpers.config_validation as cv

from .consts import DEFAULT_HOST, DEFAULT_PORT, DOMAIN

_LOGGER = logging.getLogger(__name__)

DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_HOST, default=DEFAULT_HOST): str,
vol.Required(CONF_PORT, default=DEFAULT_PORT): cv.port,
}
)


async def validate_input(hass: HomeAssistant, data: dict) -> dict[str, Any]:
"""Validate the user input allows us to connect."""
host = data[CONF_HOST]
port = data[CONF_PORT]

client = WebSClient(host, port)
await client.connect()
await client.disconnect()

return {"title": f"LedSC server {host}:{port}"}


class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for LedSC."""

VERSION = 1

async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the initial step."""
PatrikKr010 marked this conversation as resolved.
Show resolved Hide resolved
errors: dict[str, str] = {}
if user_input:
self._async_abort_entries_match(
{
CONF_HOST: user_input[CONF_HOST],
CONF_PORT: user_input[CONF_PORT],
}
)
PatrikKr010 marked this conversation as resolved.
Show resolved Hide resolved

try:
info = await validate_input(self.hass, user_input)
except ConnectionError:
errors["base"] = "cannot connect"
PatrikKr010 marked this conversation as resolved.
Show resolved Hide resolved
else:
PatrikKr010 marked this conversation as resolved.
Show resolved Hide resolved
return self.async_create_entry(title=info["title"], data=user_input)

return self.async_show_form(
step_id="user", data_schema=DATA_SCHEMA, errors=errors
)
PatrikKr010 marked this conversation as resolved.
Show resolved Hide resolved
8 changes: 8 additions & 0 deletions homeassistant/components/ledsc/consts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
"""Constant variables."""
PatrikKr010 marked this conversation as resolved.
Show resolved Hide resolved

from homeassistant.const import Platform

DOMAIN = "ledsc"
PLATFORMS: list[str] = [Platform.LIGHT]
PatrikKr010 marked this conversation as resolved.
Show resolved Hide resolved
DEFAULT_HOST = "demo.ledsc.eu"
PatrikKr010 marked this conversation as resolved.
Show resolved Hide resolved
DEFAULT_PORT = 8443
166 changes: 166 additions & 0 deletions homeassistant/components/ledsc/light.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
"""LedSC light."""

import logging
from typing import Any

from homeassistant.config_entries import ConfigEntry
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.components.light import ColorMode, LightEntity
from homeassistant.core import HomeAssistant

from websc_client import WebSClientAsync as WebSClient
from websc_client import WebSCAsync
PatrikKr010 marked this conversation as resolved.
Show resolved Hide resolved

_LOGGER = logging.getLogger(__name__)


def setup_platform(
hass: HomeAssistant, config, add_entities: AddEntitiesCallback, discovery_info=None
):
"""Redirects to '__setup'."""
hass.async_create_task(__setup(hass, dict(config), add_entities))
PatrikKr010 marked this conversation as resolved.
Show resolved Hide resolved


async def async_setup_entry(
hass: HomeAssistant, config: ConfigEntry, add_entities: AddEntitiesCallback
):
"""Redirects to '__setup'."""
await __setup(hass, dict(config.data), add_entities)


async def __setup(hass: HomeAssistant, config: dict, add_entities: AddEntitiesCallback):
"""
Connect to WebSC.

load the configured devices and add them to hass.
"""
PatrikKr010 marked this conversation as resolved.
Show resolved Hide resolved
host = config["host"]
port = config["port"]

client = WebSClient(host=host, port=port)
PatrikKr010 marked this conversation as resolved.
Show resolved Hide resolved
await client.connect()
hass.async_create_background_task(client.observer(), name="ledsc-observer")

devices: list[LedSC] = list()
PatrikKr010 marked this conversation as resolved.
Show resolved Hide resolved
for websc in client.devices.values():
ledsc = LedSC(
client_id=f"{host}:{port}",
PatrikKr010 marked this conversation as resolved.
Show resolved Hide resolved
websc=websc,
hass=hass,
)
websc.set_callback(__generate_callback(ledsc))
devices.append(ledsc)
add_entities(devices, True)


class LedSC(LightEntity):
"""Representation of an Awesome Light."""
PatrikKr010 marked this conversation as resolved.
Show resolved Hide resolved

def __init__(
self,
client_id: str,
websc: WebSCAsync,
hass: HomeAssistant,
) -> None:
"""Initialize an AwesomeLight."""
self._hass: HomeAssistant = hass
self._websc: WebSCAsync = websc
self._attr_unique_id = f"{client_id}-{websc.name}"
_LOGGER.debug(f"LedSC '%s' initialized!", self.name)

@property
def supported_color_modes(self) -> set[ColorMode] | set[str] | None:
"""List of supported color modes."""
return {ColorMode.RGBW}

@property
def color_mode(self) -> ColorMode | str | None:
"""Return the current color mode (static)."""
return ColorMode.RGBW

@property
def available(self) -> bool:
"""Check if light is available."""
return not self._websc.is_lost

@property
def name(self) -> str:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use the entity_description attribute instead and use strings.json for device names instead

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can use entity_description for more detailed device information, but the entity name is dynamic based on the WebSC configuration.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you don't even need a name for the entity, if you leave it empty, and set the name of the device and the manufacturer in the DeviceInfo it will use the generic name "Light", the entity will then be named light.ledsc_devicename_light

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image

Do is not a very nice default name.
If you don't mind, I would like to leave this property. This name is set in WebSC.
Snímek obrazovky z 2024-05-30 18-33-19

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's tricky to get this right, haven't done this yet with the light platform. I would like to test it, is there some way to do this? The host demo.ledsc.eu did not work.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The demo.ledsc.eu domain is still under development. I am currently creating a way to create a virtual instance of WebSC so that anyone who wants to try the demo can run their own instance of WebSC for a limited time. However, for testing purposes I can run one static instance on this domain. Otherwise you can run your own instances of course.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"""Return the display name of this light."""
return self._websc.name

@property
def brightness(self) -> int | None:
"""Return the brightness of the light."""
return max(self._websc.rgbw)

@brightness.setter
def brightness(self, value: int) -> None:
"""Set brightness of the light."""
actual = self.brightness
if actual is None or actual == 0:
self.hass.async_create_task(
self._websc.set_rgbw(red=value, green=value, blue=value, white=value)
)
else:
diff = value - actual
ratio = diff / actual
self.hass.async_create_task(
self._websc.set_rgbw(
red=round(self._websc.red * (1 + ratio)),
green=round(self._websc.green * (1 + ratio)),
blue=round(self._websc.blue * (1 + ratio)),
white=round(self._websc.white * (1 + ratio)),
)
)

@property
def rgbw_color(self) -> tuple[int, int, int, int] | None:
"""Get color."""
return self._websc.rgbw

@rgbw_color.setter
def rgbw_color(self, value: tuple[int, int, int, int]) -> None:
"""Set color to WebSC."""
self.hass.async_create_task(
self._websc.set_rgbw(
red=value[0],
green=value[1],
blue=value[2],
white=value[3],
)
)

@property
def is_on(self) -> bool | None:
"""Return true if light is on."""
return bool(
self._websc.red
or self._websc.green
or self._websc.blue
or self._websc.white
)

async def async_turn_on(self, **kwargs: Any) -> None:
"""Instruct the light to turn on."""
if "brightness" in kwargs:
self.brightness = kwargs["brightness"]
elif "rgbw_color" in kwargs:
self.rgbw_color = kwargs["rgbw_color"]
elif not self.is_on:
await self.switch()

async def async_turn_off(self, **kwargs: Any) -> None:
"""Instruct the light to turn off."""
if self.is_on:
await self.switch()

async def switch(self) -> None:
"""Send switch event to WebSC."""
await self._websc.do_px_trigger()


def __generate_callback(ledsc: LedSC):
async def on_device_change(data: dict[str, int]):
await ledsc.async_update_ha_state(force_refresh=True)

return on_device_change
10 changes: 10 additions & 0 deletions homeassistant/components/ledsc/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"domain": "ledsc",
"name": "LedSC",
"codeowners": ["@PatrikKr010"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/ledsc/",
"integration_type": "hub",
PatrikKr010 marked this conversation as resolved.
Show resolved Hide resolved
"iot_class": "local_polling",
"requirements": ["websc-client==1.0.1"]
}
1 change: 1 addition & 0 deletions homeassistant/generated/config_flows.py
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,7 @@
"ld2410_ble",
"leaone",
"led_ble",
"ledsc",
"lg_netcast",
"lg_soundbar",
"lidarr",
Expand Down
6 changes: 6 additions & 0 deletions homeassistant/generated/integrations.json
Original file line number Diff line number Diff line change
Expand Up @@ -3193,6 +3193,12 @@
"config_flow": true,
"iot_class": "local_polling"
},
"ledsc": {
"name": "LedSC",
"integration_type": "hub",
"config_flow": true,
"iot_class": "local_polling"
},
"legrand": {
"name": "Legrand",
"integration_type": "virtual",
Expand Down
3 changes: 3 additions & 0 deletions requirements_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2875,6 +2875,9 @@ webmin-xmlrpc==0.0.2
# homeassistant.components.assist_pipeline
webrtc-noise-gain==1.2.3

# homeassistant.components.ledsc
websc-client==1.0.1

# homeassistant.components.whirlpool
whirlpool-sixth-sense==0.18.8

Expand Down