Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
f58051e
Add WLED integration
frenck Nov 4, 2019
4131186
Use f-string for uniq id in sensor platform
frenck Nov 5, 2019
4e7e058
Typing improvements
frenck Nov 5, 2019
6836460
Removes sensor & light platform
frenck Nov 5, 2019
2e42d7e
Remove PARALLEL_UPDATES from integration level
frenck Nov 5, 2019
6baeb09
Correct type in code comment 'themselves'
frenck Nov 5, 2019
c6bd8ea
Use async_track_time_interval in async context
frenck Nov 5, 2019
c4a2e3f
Remove stale code
frenck Nov 5, 2019
c17b52c
Remove decorator from Flow handler
frenck Nov 5, 2019
f46335d
Remove unused __init__ from config flow
frenck Nov 5, 2019
82fe294
Move show form methods to sync
frenck Nov 5, 2019
70a3c0b
Only wrap lines that can raise in try except block
frenck Nov 5, 2019
a1fd2bc
Remove domain and platform from uniq id
frenck Nov 5, 2019
6f266ca
Wrap light state in bool object in is_on method
frenck Nov 5, 2019
1f84360
Use async_schedule_update_ha_state in async context
frenck Nov 5, 2019
4262ccf
Return empty dict in device state attributes instead of None
frenck Nov 5, 2019
935acbe
Remove unneeded setdefault call in setup entry
frenck Nov 5, 2019
8761383
Cancel update timer on entry unload
frenck Nov 5, 2019
0eca44e
Restructure config flow code
frenck Nov 5, 2019
9894aa3
Adjust tests for new uniq id
frenck Nov 5, 2019
b6c9386
Correct typo AdGuard Home -> WLED in config flow file comment
frenck Nov 5, 2019
1e57586
Convert internal package imports to be relative
frenck Nov 6, 2019
9ab71db
Reformat JSON files with Prettier
frenck Nov 6, 2019
8723b5d
Improve tests based on review comments
frenck Nov 6, 2019
9ccc7c4
Add test for zeroconf when no data is provided
frenck Nov 6, 2019
a7800cb
Cleanup and extended tests
frenck Nov 6, 2019
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
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,7 @@ homeassistant/components/weblink/* @home-assistant/core
homeassistant/components/websocket_api/* @home-assistant/core
homeassistant/components/wemo/* @sqldiablo
homeassistant/components/withings/* @vangorra
homeassistant/components/wled/* @frenck
homeassistant/components/worldclock/* @fabaff
homeassistant/components/wwlln/* @bachya
homeassistant/components/xbox_live/* @MartinHjelmare
Expand Down
26 changes: 26 additions & 0 deletions homeassistant/components/wled/.translations/en.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"config": {
"title": "WLED",
"flow_title": "WLED: {name}",
"step": {
"user": {
"title": "Link your WLED",
"description": "Set up your WLED to integrate with Home Assistant.",
"data": {
"host": "Host or IP address"
}
},
"zeroconf_confirm": {
"description": "Do you want to add the WLED named `{name}` to Home Assistant?",
"title": "Discovered WLED device"
}
},
"error": {
"connection_error": "Failed to connect to WLED device."
},
"abort": {
"already_configured": "This WLED device is already configured.",
"connection_error": "Failed to connect to WLED device."
}
}
}
182 changes: 182 additions & 0 deletions homeassistant/components/wled/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
"""Support for WLED."""
from datetime import timedelta
import logging
from typing import Any, Dict, Optional, Union

from wled import WLED, WLEDConnectionError, WLEDError

from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_NAME, CONF_HOST
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.dispatcher import (
async_dispatcher_connect,
async_dispatcher_send,
)
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.event import async_track_time_interval
from homeassistant.helpers.typing import ConfigType
from homeassistant.util import dt as dt_util

from .const import (
ATTR_IDENTIFIERS,
ATTR_MANUFACTURER,
ATTR_MODEL,
ATTR_SOFTWARE_VERSION,
DATA_WLED_CLIENT,
DATA_WLED_TIMER,
DATA_WLED_UPDATED,
DOMAIN,
)

SCAN_INTERVAL = timedelta(seconds=5)

_LOGGER = logging.getLogger(__name__)


async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the WLED components."""
return True


async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up WLED from a config entry."""

# Create WLED instance for this entry
session = async_get_clientsession(hass)
wled = WLED(entry.data[CONF_HOST], loop=hass.loop, session=session)

# Ensure we can connect and talk to it
try:
await wled.update()
except WLEDConnectionError as exception:
raise ConfigEntryNotReady from exception

hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][entry.entry_id] = {DATA_WLED_CLIENT: wled}

# Set up all platforms for this device/entry.
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(entry, LIGHT_DOMAIN)
)

async def interval_update(now: dt_util.dt.datetime = None) -> None:
"""Poll WLED device function, dispatches event after update."""
try:
await wled.update()
except WLEDError:
_LOGGER.debug("An error occurred while updating WLED", exc_info=True)

# Even if the update failed, we still send out the event.
# To allow entities to make themselves unavailable.
async_dispatcher_send(hass, DATA_WLED_UPDATED, entry.entry_id)

# Schedule update interval
hass.data[DOMAIN][entry.entry_id][DATA_WLED_TIMER] = async_track_time_interval(
hass, interval_update, SCAN_INTERVAL
)

return True


async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload WLED config entry."""

# Cancel update timer for this entry/device.
cancel_timer = hass.data[DOMAIN][entry.entry_id][DATA_WLED_TIMER]
cancel_timer()

# Unload entities for this entry/device.
await hass.config_entries.async_forward_entry_unload(entry, LIGHT_DOMAIN)

# Cleanup
del hass.data[DOMAIN][entry.entry_id]
if not hass.data[DOMAIN]:
del hass.data[DOMAIN]
Comment thread
frenck marked this conversation as resolved.

return True


class WLEDEntity(Entity):
"""Defines a base WLED entity."""

def __init__(self, entry_id: str, wled: WLED, name: str, icon: str) -> None:
"""Initialize the WLED entity."""
self._attributes: Dict[str, Union[str, int, float]] = {}
self._available = True
self._entry_id = entry_id
self._icon = icon
self._name = name
self._unsub_dispatcher = None
self.wled = wled

@property
def name(self) -> str:
"""Return the name of the entity."""
return self._name

@property
def icon(self) -> str:
"""Return the mdi icon of the entity."""
return self._icon

@property
def available(self) -> bool:
"""Return True if entity is available."""
return self._available

@property
def should_poll(self) -> bool:
"""Return the polling requirement of the entity."""
return False

@property
def device_state_attributes(self) -> Optional[Dict[str, Any]]:
"""Return the state attributes of the entity."""
return self._attributes

async def async_added_to_hass(self) -> None:
"""Connect to dispatcher listening for entity data notifications."""
self._unsub_dispatcher = async_dispatcher_connect(
self.hass, DATA_WLED_UPDATED, self._schedule_immediate_update
)

async def async_will_remove_from_hass(self) -> None:
"""Disconnect from update signal."""
self._unsub_dispatcher()

@callback
def _schedule_immediate_update(self, entry_id: str) -> None:
"""Schedule an immediate update of the entity."""
if entry_id == self._entry_id:
self.async_schedule_update_ha_state(True)

async def async_update(self) -> None:
"""Update WLED entity."""
if self.wled.device is None:
self._available = False
return

self._available = True
await self._wled_update()

async def _wled_update(self) -> None:
"""Update WLED entity."""
raise NotImplementedError()


class WLEDDeviceEntity(WLEDEntity):
"""Defines a WLED device entity."""

@property
def device_info(self) -> Dict[str, Any]:
"""Return device information about this WLED device."""
return {
ATTR_IDENTIFIERS: {(DOMAIN, self.wled.device.info.mac_address)},
ATTR_NAME: self.wled.device.info.name,
ATTR_MANUFACTURER: self.wled.device.info.brand,
ATTR_MODEL: self.wled.device.info.product,
ATTR_SOFTWARE_VERSION: self.wled.device.info.version,
}
123 changes: 123 additions & 0 deletions homeassistant/components/wled/config_flow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
"""Config flow to configure the WLED integration."""
import logging
from typing import Any, Dict, Optional

import voluptuous as vol
from wled import WLED, WLEDConnectionError

from homeassistant.config_entries import (
CONN_CLASS_LOCAL_POLL,
SOURCE_ZEROCONF,
ConfigFlow,
)
from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME
from homeassistant.helpers import ConfigType
from homeassistant.helpers.aiohttp_client import async_get_clientsession

from .const import DOMAIN # pylint: disable=W0611

_LOGGER = logging.getLogger(__name__)


class WLEDFlowHandler(ConfigFlow, domain=DOMAIN):
"""Handle a WLED config flow."""

VERSION = 1
CONNECTION_CLASS = CONN_CLASS_LOCAL_POLL

async def async_step_user(
self, user_input: Optional[ConfigType] = None
) -> Dict[str, Any]:
"""Handle a flow initiated by the user."""
return await self._handle_config_flow(user_input)

async def async_step_zeroconf(
self, user_input: Optional[ConfigType] = None
) -> Dict[str, Any]:
"""Handle zeroconf discovery."""
if user_input is None:
return self.async_abort(reason="connection_error")
Comment thread
frenck marked this conversation as resolved.

# Hostname is format: wled-livingroom.local.
host = user_input["hostname"].rstrip(".")
name, _ = host.rsplit(".")
Comment thread
frenck marked this conversation as resolved.

# pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
self.context.update(
{CONF_HOST: host, CONF_NAME: name, "title_placeholders": {"name": name}}
)

# Prepare configuration flow
return await self._handle_config_flow(user_input, True)

async def async_step_zeroconf_confirm(
self, user_input: ConfigType = None
) -> Dict[str, Any]:
"""Handle a flow initiated by zeroconf."""
return await self._handle_config_flow(user_input)

async def _handle_config_flow(
self, user_input: Optional[ConfigType] = None, prepare: bool = False
) -> Dict[str, Any]:
"""Config flow handler for WLED."""
# pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
source = self.context.get("source")

# Request user input, unless we are preparing discovery flow
if user_input is None and not prepare:
if source == SOURCE_ZEROCONF:
return self._show_confirm_dialog()
return self._show_setup_form()

if source == SOURCE_ZEROCONF:
# pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
user_input[CONF_HOST] = self.context.get(CONF_HOST)

errors = {}
session = async_get_clientsession(self.hass)
wled = WLED(user_input[CONF_HOST], loop=self.hass.loop, session=session)

try:
device = await wled.update()
except WLEDConnectionError:
if source == SOURCE_ZEROCONF:
return self.async_abort(reason="connection_error")
errors["base"] = "connection_error"
return self._show_setup_form(errors)

# Check if already configured
mac_address = device.info.mac_address
for entry in self._async_current_entries():
if entry.data[CONF_MAC] == mac_address:
# This mac address is already configured
return self.async_abort(reason="already_configured")

title = user_input[CONF_HOST]
if source == SOURCE_ZEROCONF:
# pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
title = self.context.get(CONF_NAME)

if prepare:
return await self.async_step_zeroconf_confirm()

return self.async_create_entry(
title=title, data={CONF_HOST: user_input[CONF_HOST], CONF_MAC: mac_address}
)

def _show_setup_form(self, errors: Optional[Dict] = None) -> Dict[str, Any]:
"""Show the setup form to the user."""
return self.async_show_form(
step_id="user",
data_schema=vol.Schema({vol.Required(CONF_HOST): str}),
errors=errors or {},
)

def _show_confirm_dialog(self, errors: Optional[Dict] = None) -> Dict[str, Any]:
"""Show the confirm dialog to the user."""
# pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
name = self.context.get(CONF_NAME)
return self.async_show_form(
step_id="zeroconf_confirm",
description_placeholders={"name": name},
errors=errors or {},
)
25 changes: 25 additions & 0 deletions homeassistant/components/wled/const.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
"""Constants for the WLED integration."""

# Integration domain
DOMAIN = "wled"

# Hass data keys
DATA_WLED_CLIENT = "wled_client"
DATA_WLED_TIMER = "wled_timer"
DATA_WLED_UPDATED = "wled_updated"

# Attributes
ATTR_COLOR_PRIMARY = "color_primary"
ATTR_DURATION = "duration"
ATTR_IDENTIFIERS = "identifiers"
ATTR_INTENSITY = "intensity"
ATTR_MANUFACTURER = "manufacturer"
ATTR_MODEL = "model"
ATTR_ON = "on"
ATTR_PALETTE = "palette"
ATTR_PLAYLIST = "playlist"
ATTR_PRESET = "preset"
ATTR_SEGMENT_ID = "segment_id"
ATTR_SOFTWARE_VERSION = "sw_version"
ATTR_SPEED = "speed"
ATTR_TARGET_BRIGHTNESS = "target_brightness"
Loading