mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 21:27:38 +00:00
Move yeelight into component (#21593)
This commit is contained in:
parent
71ebc4f594
commit
9214934d47
@ -46,6 +46,7 @@ SERVICE_ROKU = 'roku'
|
|||||||
SERVICE_SABNZBD = 'sabnzbd'
|
SERVICE_SABNZBD = 'sabnzbd'
|
||||||
SERVICE_SAMSUNG_PRINTER = 'samsung_printer'
|
SERVICE_SAMSUNG_PRINTER = 'samsung_printer'
|
||||||
SERVICE_TELLDUSLIVE = 'tellstick'
|
SERVICE_TELLDUSLIVE = 'tellstick'
|
||||||
|
SERVICE_YEELIGHT = 'yeelight'
|
||||||
SERVICE_WEMO = 'belkin_wemo'
|
SERVICE_WEMO = 'belkin_wemo'
|
||||||
SERVICE_WINK = 'wink'
|
SERVICE_WINK = 'wink'
|
||||||
SERVICE_XIAOMI_GW = 'xiaomi_gw'
|
SERVICE_XIAOMI_GW = 'xiaomi_gw'
|
||||||
@ -79,6 +80,7 @@ SERVICE_HANDLERS = {
|
|||||||
SERVICE_KONNECTED: ('konnected', None),
|
SERVICE_KONNECTED: ('konnected', None),
|
||||||
SERVICE_OCTOPRINT: ('octoprint', None),
|
SERVICE_OCTOPRINT: ('octoprint', None),
|
||||||
SERVICE_FREEBOX: ('freebox', None),
|
SERVICE_FREEBOX: ('freebox', None),
|
||||||
|
SERVICE_YEELIGHT: ('yeelight', None),
|
||||||
'panasonic_viera': ('media_player', 'panasonic_viera'),
|
'panasonic_viera': ('media_player', 'panasonic_viera'),
|
||||||
'plex_mediaserver': ('media_player', 'plex'),
|
'plex_mediaserver': ('media_player', 'plex'),
|
||||||
'yamaha': ('media_player', 'yamaha'),
|
'yamaha': ('media_player', 'yamaha'),
|
||||||
@ -86,7 +88,6 @@ SERVICE_HANDLERS = {
|
|||||||
'directv': ('media_player', 'directv'),
|
'directv': ('media_player', 'directv'),
|
||||||
'denonavr': ('media_player', 'denonavr'),
|
'denonavr': ('media_player', 'denonavr'),
|
||||||
'samsung_tv': ('media_player', 'samsungtv'),
|
'samsung_tv': ('media_player', 'samsungtv'),
|
||||||
'yeelight': ('light', 'yeelight'),
|
|
||||||
'frontier_silicon': ('media_player', 'frontier_silicon'),
|
'frontier_silicon': ('media_player', 'frontier_silicon'),
|
||||||
'openhome': ('media_player', 'openhome'),
|
'openhome': ('media_player', 'openhome'),
|
||||||
'harmony': ('remote', 'harmony'),
|
'harmony': ('remote', 'harmony'),
|
||||||
|
@ -178,29 +178,3 @@ xiaomi_miio_set_delayed_turn_off:
|
|||||||
time_period:
|
time_period:
|
||||||
description: Time period for the delayed turn off.
|
description: Time period for the delayed turn off.
|
||||||
example: "5, '0:05', {'minutes': 5}"
|
example: "5, '0:05', {'minutes': 5}"
|
||||||
|
|
||||||
yeelight_set_mode:
|
|
||||||
description: Set a operation mode.
|
|
||||||
fields:
|
|
||||||
entity_id:
|
|
||||||
description: Name of the light entity.
|
|
||||||
example: 'light.yeelight'
|
|
||||||
mode:
|
|
||||||
description: Operation mode. Valid values are 'last', 'normal', 'rgb', 'hsv', 'color_flow', 'moonlight'.
|
|
||||||
example: 'moonlight'
|
|
||||||
|
|
||||||
yeelight_start_flow:
|
|
||||||
description: Start a custom flow, using transitions from https://yeelight.readthedocs.io/en/stable/yeelight.html#flow-objects
|
|
||||||
fields:
|
|
||||||
entity_id:
|
|
||||||
description: Name of the light entity.
|
|
||||||
example: 'light.yeelight'
|
|
||||||
count:
|
|
||||||
description: The number of times to run this flow (0 to run forever).
|
|
||||||
example: 0
|
|
||||||
action:
|
|
||||||
description: The action to take after the flow stops. Can be 'recover', 'stay', 'off'. (default 'recover')
|
|
||||||
example: 'stay'
|
|
||||||
transitions:
|
|
||||||
description: Array of transitions, for desired effect. Examples https://yeelight.readthedocs.io/en/stable/flow.html
|
|
||||||
example: '[{ "TemperatureTransition": [1900, 1000, 80] }, { "TemperatureTransition": [1900, 1000, 10] }]'
|
|
||||||
|
@ -1 +1,357 @@
|
|||||||
"""The yeelight component."""
|
"""
|
||||||
|
Support for Xiaomi Yeelight Wifi color bulb.
|
||||||
|
|
||||||
|
For more details about this platform, please refer to the documentation at
|
||||||
|
https://home-assistant.io/components/yeelight/
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
import voluptuous as vol
|
||||||
|
from homeassistant.components.discovery import SERVICE_YEELIGHT
|
||||||
|
from homeassistant.const import CONF_DEVICES, CONF_NAME, CONF_SCAN_INTERVAL, \
|
||||||
|
CONF_HOST, ATTR_ENTITY_ID, CONF_LIGHTS
|
||||||
|
from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN
|
||||||
|
from homeassistant.helpers import discovery
|
||||||
|
from homeassistant.helpers.discovery import load_platform
|
||||||
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
from homeassistant.helpers.dispatcher import dispatcher_send
|
||||||
|
from homeassistant.helpers.event import async_track_time_interval
|
||||||
|
from homeassistant.helpers.service import extract_entity_ids
|
||||||
|
|
||||||
|
REQUIREMENTS = ['yeelight==0.4.3']
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
DOMAIN = "yeelight"
|
||||||
|
DATA_YEELIGHT = DOMAIN
|
||||||
|
DATA_UPDATED = '{}_data_updated'.format(DOMAIN)
|
||||||
|
|
||||||
|
DEFAULT_NAME = 'Yeelight'
|
||||||
|
DEFAULT_TRANSITION = 350
|
||||||
|
|
||||||
|
CONF_MODEL = 'model'
|
||||||
|
CONF_TRANSITION = 'transition'
|
||||||
|
CONF_SAVE_ON_CHANGE = 'save_on_change'
|
||||||
|
CONF_MODE_MUSIC = 'use_music_mode'
|
||||||
|
CONF_FLOW_PARAMS = 'flow_params'
|
||||||
|
CONF_CUSTOM_EFFECTS = 'custom_effects'
|
||||||
|
|
||||||
|
ATTR_MODE = 'mode'
|
||||||
|
ATTR_COUNT = 'count'
|
||||||
|
ATTR_ACTION = 'action'
|
||||||
|
ATTR_TRANSITIONS = 'transitions'
|
||||||
|
|
||||||
|
ACTION_RECOVER = 'recover'
|
||||||
|
ACTION_STAY = 'stay'
|
||||||
|
ACTION_OFF = 'off'
|
||||||
|
|
||||||
|
MODE_MOONLIGHT = 'moonlight'
|
||||||
|
MODE_DAYLIGHT = 'normal'
|
||||||
|
|
||||||
|
SCAN_INTERVAL = timedelta(seconds=30)
|
||||||
|
|
||||||
|
YEELIGHT_RGB_TRANSITION = 'RGBTransition'
|
||||||
|
YEELIGHT_HSV_TRANSACTION = 'HSVTransition'
|
||||||
|
YEELIGHT_TEMPERATURE_TRANSACTION = 'TemperatureTransition'
|
||||||
|
YEELIGHT_SLEEP_TRANSACTION = 'SleepTransition'
|
||||||
|
|
||||||
|
SERVICE_SET_MODE = 'set_mode'
|
||||||
|
SERVICE_START_FLOW = 'start_flow'
|
||||||
|
|
||||||
|
YEELIGHT_FLOW_TRANSITION_SCHEMA = {
|
||||||
|
vol.Optional(ATTR_COUNT, default=0): cv.positive_int,
|
||||||
|
vol.Optional(ATTR_ACTION, default=ACTION_RECOVER):
|
||||||
|
vol.Any(ACTION_RECOVER, ACTION_OFF, ACTION_STAY),
|
||||||
|
vol.Required(ATTR_TRANSITIONS): [{
|
||||||
|
vol.Exclusive(YEELIGHT_RGB_TRANSITION, CONF_TRANSITION):
|
||||||
|
vol.All(cv.ensure_list, [cv.positive_int]),
|
||||||
|
vol.Exclusive(YEELIGHT_HSV_TRANSACTION, CONF_TRANSITION):
|
||||||
|
vol.All(cv.ensure_list, [cv.positive_int]),
|
||||||
|
vol.Exclusive(YEELIGHT_TEMPERATURE_TRANSACTION, CONF_TRANSITION):
|
||||||
|
vol.All(cv.ensure_list, [cv.positive_int]),
|
||||||
|
vol.Exclusive(YEELIGHT_SLEEP_TRANSACTION, CONF_TRANSITION):
|
||||||
|
vol.All(cv.ensure_list, [cv.positive_int]),
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
DEVICE_SCHEMA = vol.Schema({
|
||||||
|
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||||
|
vol.Optional(CONF_TRANSITION, default=DEFAULT_TRANSITION): cv.positive_int,
|
||||||
|
vol.Optional(CONF_MODE_MUSIC, default=False): cv.boolean,
|
||||||
|
vol.Optional(CONF_SAVE_ON_CHANGE, default=False): cv.boolean,
|
||||||
|
vol.Optional(CONF_MODEL): cv.string,
|
||||||
|
})
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = vol.Schema({
|
||||||
|
DOMAIN: vol.Schema({
|
||||||
|
vol.Optional(CONF_DEVICES, default={}): {cv.string: DEVICE_SCHEMA},
|
||||||
|
vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL):
|
||||||
|
cv.time_period,
|
||||||
|
vol.Optional(CONF_CUSTOM_EFFECTS): [{
|
||||||
|
vol.Required(CONF_NAME): cv.string,
|
||||||
|
vol.Required(CONF_FLOW_PARAMS): YEELIGHT_FLOW_TRANSITION_SCHEMA
|
||||||
|
}]
|
||||||
|
}),
|
||||||
|
}, extra=vol.ALLOW_EXTRA)
|
||||||
|
|
||||||
|
YEELIGHT_SERVICE_SCHEMA = vol.Schema({
|
||||||
|
vol.Required(ATTR_ENTITY_ID): cv.entity_ids,
|
||||||
|
})
|
||||||
|
|
||||||
|
NIGHTLIGHT_SUPPORTED_MODELS = [
|
||||||
|
"ceiling3",
|
||||||
|
'ceiling4'
|
||||||
|
]
|
||||||
|
|
||||||
|
UPDATE_REQUEST_PROPERTIES = [
|
||||||
|
"power",
|
||||||
|
"bright",
|
||||||
|
"ct",
|
||||||
|
"rgb",
|
||||||
|
"hue",
|
||||||
|
"sat",
|
||||||
|
"color_mode",
|
||||||
|
"flowing",
|
||||||
|
"music_on",
|
||||||
|
"nl_br",
|
||||||
|
"active_mode",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _transitions_config_parser(transitions):
|
||||||
|
"""Parse transitions config into initialized objects."""
|
||||||
|
import yeelight
|
||||||
|
|
||||||
|
transition_objects = []
|
||||||
|
for transition_config in transitions:
|
||||||
|
transition, params = list(transition_config.items())[0]
|
||||||
|
transition_objects.append(getattr(yeelight, transition)(*params))
|
||||||
|
|
||||||
|
return transition_objects
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_custom_effects(effects_config):
|
||||||
|
import yeelight
|
||||||
|
|
||||||
|
effects = {}
|
||||||
|
for config in effects_config:
|
||||||
|
params = config[CONF_FLOW_PARAMS]
|
||||||
|
action = yeelight.Flow.actions[params[ATTR_ACTION]]
|
||||||
|
transitions = _transitions_config_parser(
|
||||||
|
params[ATTR_TRANSITIONS])
|
||||||
|
|
||||||
|
effects[config[CONF_NAME]] = {
|
||||||
|
ATTR_COUNT: params[ATTR_COUNT],
|
||||||
|
ATTR_ACTION: action,
|
||||||
|
ATTR_TRANSITIONS: transitions
|
||||||
|
}
|
||||||
|
|
||||||
|
return effects
|
||||||
|
|
||||||
|
|
||||||
|
def setup(hass, config):
|
||||||
|
"""Set up the Yeelight bulbs."""
|
||||||
|
from yeelight.enums import PowerMode
|
||||||
|
|
||||||
|
conf = config[DOMAIN]
|
||||||
|
yeelight_data = hass.data[DATA_YEELIGHT] = {
|
||||||
|
CONF_DEVICES: {},
|
||||||
|
CONF_LIGHTS: {},
|
||||||
|
}
|
||||||
|
|
||||||
|
def device_discovered(service, info):
|
||||||
|
_LOGGER.debug("Adding autodetected %s", info['hostname'])
|
||||||
|
|
||||||
|
device_type = info['device_type']
|
||||||
|
|
||||||
|
name = "yeelight_%s_%s" % (device_type,
|
||||||
|
info['properties']['mac'])
|
||||||
|
ipaddr = info[CONF_HOST]
|
||||||
|
device_config = DEVICE_SCHEMA({
|
||||||
|
CONF_NAME: name,
|
||||||
|
CONF_MODEL: device_type
|
||||||
|
})
|
||||||
|
|
||||||
|
_setup_device(hass, config, ipaddr, device_config)
|
||||||
|
|
||||||
|
discovery.listen(hass, SERVICE_YEELIGHT, device_discovered)
|
||||||
|
|
||||||
|
def async_update(event):
|
||||||
|
for device in yeelight_data[CONF_DEVICES].values():
|
||||||
|
device.update()
|
||||||
|
|
||||||
|
async_track_time_interval(
|
||||||
|
hass, async_update, conf[CONF_SCAN_INTERVAL]
|
||||||
|
)
|
||||||
|
|
||||||
|
def service_handler(service):
|
||||||
|
"""Dispatch service calls to target entities."""
|
||||||
|
params = {key: value for key, value in service.data.items()
|
||||||
|
if key != ATTR_ENTITY_ID}
|
||||||
|
|
||||||
|
entity_ids = extract_entity_ids(hass, service)
|
||||||
|
target_devices = [dev.device for dev in
|
||||||
|
yeelight_data[CONF_LIGHTS].values()
|
||||||
|
if dev.entity_id in entity_ids]
|
||||||
|
|
||||||
|
for target_device in target_devices:
|
||||||
|
if service.service == SERVICE_SET_MODE:
|
||||||
|
target_device.set_mode(**params)
|
||||||
|
elif service.service == SERVICE_START_FLOW:
|
||||||
|
params[ATTR_TRANSITIONS] = \
|
||||||
|
_transitions_config_parser(params[ATTR_TRANSITIONS])
|
||||||
|
target_device.start_flow(**params)
|
||||||
|
|
||||||
|
service_schema_set_mode = YEELIGHT_SERVICE_SCHEMA.extend({
|
||||||
|
vol.Required(ATTR_MODE):
|
||||||
|
vol.In([mode.name.lower() for mode in PowerMode])
|
||||||
|
})
|
||||||
|
hass.services.register(
|
||||||
|
DOMAIN, SERVICE_SET_MODE, service_handler,
|
||||||
|
schema=service_schema_set_mode)
|
||||||
|
|
||||||
|
service_schema_start_flow = YEELIGHT_SERVICE_SCHEMA.extend(
|
||||||
|
YEELIGHT_FLOW_TRANSITION_SCHEMA
|
||||||
|
)
|
||||||
|
hass.services.register(
|
||||||
|
DOMAIN, SERVICE_START_FLOW, service_handler,
|
||||||
|
schema=service_schema_start_flow)
|
||||||
|
|
||||||
|
for ipaddr, device_config in conf[CONF_DEVICES].items():
|
||||||
|
_LOGGER.debug("Adding configured %s", device_config[CONF_NAME])
|
||||||
|
_setup_device(hass, config, ipaddr, device_config)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def _setup_device(hass, hass_config, ipaddr, device_config):
|
||||||
|
devices = hass.data[DATA_YEELIGHT][CONF_DEVICES]
|
||||||
|
|
||||||
|
if ipaddr in devices:
|
||||||
|
return
|
||||||
|
|
||||||
|
device = YeelightDevice(hass, ipaddr, device_config)
|
||||||
|
|
||||||
|
devices[ipaddr] = device
|
||||||
|
|
||||||
|
platform_config = device_config.copy()
|
||||||
|
platform_config[CONF_HOST] = ipaddr
|
||||||
|
platform_config[CONF_CUSTOM_EFFECTS] = _parse_custom_effects(
|
||||||
|
hass_config[DATA_YEELIGHT].get(CONF_CUSTOM_EFFECTS, {})
|
||||||
|
)
|
||||||
|
|
||||||
|
load_platform(hass, LIGHT_DOMAIN, DOMAIN, platform_config, hass_config)
|
||||||
|
|
||||||
|
|
||||||
|
class YeelightDevice:
|
||||||
|
"""Represents single Yeelight device."""
|
||||||
|
|
||||||
|
def __init__(self, hass, ipaddr, config):
|
||||||
|
"""Initialize device."""
|
||||||
|
self._hass = hass
|
||||||
|
self._config = config
|
||||||
|
self._ipaddr = ipaddr
|
||||||
|
self._name = config.get(CONF_NAME)
|
||||||
|
self._model = config.get(CONF_MODEL)
|
||||||
|
self._bulb_device = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def bulb(self):
|
||||||
|
"""Return bulb device."""
|
||||||
|
import yeelight
|
||||||
|
if self._bulb_device is None:
|
||||||
|
try:
|
||||||
|
self._bulb_device = yeelight.Bulb(self._ipaddr,
|
||||||
|
model=self._model)
|
||||||
|
# force init for type
|
||||||
|
self._update_properties()
|
||||||
|
|
||||||
|
except yeelight.BulbException as ex:
|
||||||
|
_LOGGER.error("Failed to connect to bulb %s, %s: %s",
|
||||||
|
self._ipaddr, self._name, ex)
|
||||||
|
|
||||||
|
return self._bulb_device
|
||||||
|
|
||||||
|
def _update_properties(self):
|
||||||
|
self._bulb_device.get_properties(UPDATE_REQUEST_PROPERTIES)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
"""Return the name of the device if any."""
|
||||||
|
return self._name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def config(self):
|
||||||
|
"""Return device config."""
|
||||||
|
return self._config
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ipaddr(self):
|
||||||
|
"""Return ip address."""
|
||||||
|
return self._ipaddr
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_nightlight_enabled(self) -> bool:
|
||||||
|
"""Return true / false if nightlight is currently enabled."""
|
||||||
|
if self._bulb_device is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return self.bulb.last_properties.get('active_mode') == '1'
|
||||||
|
|
||||||
|
def turn_on(self, duration=DEFAULT_TRANSITION):
|
||||||
|
"""Turn on device."""
|
||||||
|
import yeelight
|
||||||
|
|
||||||
|
try:
|
||||||
|
self._bulb_device.turn_on(duration=duration)
|
||||||
|
except yeelight.BulbException as ex:
|
||||||
|
_LOGGER.error("Unable to turn the bulb on: %s", ex)
|
||||||
|
return
|
||||||
|
|
||||||
|
self.update()
|
||||||
|
|
||||||
|
def turn_off(self, duration=DEFAULT_TRANSITION):
|
||||||
|
"""Turn off device."""
|
||||||
|
import yeelight
|
||||||
|
|
||||||
|
try:
|
||||||
|
self._bulb_device.turn_off(duration=duration)
|
||||||
|
except yeelight.BulbException as ex:
|
||||||
|
_LOGGER.error("Unable to turn the bulb on: %s", ex)
|
||||||
|
return
|
||||||
|
|
||||||
|
self.update()
|
||||||
|
|
||||||
|
def update(self):
|
||||||
|
"""Read new properties from the device."""
|
||||||
|
if not self.bulb:
|
||||||
|
return
|
||||||
|
|
||||||
|
self._update_properties()
|
||||||
|
dispatcher_send(self._hass, DATA_UPDATED, self._ipaddr)
|
||||||
|
|
||||||
|
def set_mode(self, mode: str):
|
||||||
|
"""Set a power mode."""
|
||||||
|
import yeelight
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.bulb.set_power_mode(yeelight.enums.PowerMode[mode.upper()])
|
||||||
|
except yeelight.BulbException as ex:
|
||||||
|
_LOGGER.error("Unable to set the power mode: %s", ex)
|
||||||
|
|
||||||
|
self.update()
|
||||||
|
|
||||||
|
def start_flow(self, transitions, count=0, action=ACTION_RECOVER):
|
||||||
|
"""Start flow."""
|
||||||
|
import yeelight
|
||||||
|
|
||||||
|
try:
|
||||||
|
flow = yeelight.Flow(
|
||||||
|
count=count,
|
||||||
|
action=yeelight.Flow.actions[action],
|
||||||
|
transitions=transitions)
|
||||||
|
|
||||||
|
self.bulb.start_flow(flow)
|
||||||
|
except yeelight.BulbException as ex:
|
||||||
|
_LOGGER.error("Unable to set effect: %s", ex)
|
||||||
|
@ -1,99 +1,26 @@
|
|||||||
"""
|
"""Light platform support for yeelight."""
|
||||||
Support for Xiaomi Yeelight Wifi color bulb.
|
|
||||||
|
|
||||||
For more details about this platform, please refer to the documentation at
|
|
||||||
https://home-assistant.io/components/light.yeelight/
|
|
||||||
"""
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
import voluptuous as vol
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||||
|
|
||||||
from homeassistant.util.color import (
|
from homeassistant.util.color import (
|
||||||
color_temperature_mired_to_kelvin as mired_to_kelvin,
|
color_temperature_mired_to_kelvin as mired_to_kelvin,
|
||||||
color_temperature_kelvin_to_mired as kelvin_to_mired)
|
color_temperature_kelvin_to_mired as kelvin_to_mired)
|
||||||
from homeassistant.const import CONF_DEVICES, CONF_NAME
|
from homeassistant.const import CONF_HOST, CONF_DEVICES, CONF_LIGHTS
|
||||||
|
from homeassistant.core import callback
|
||||||
from homeassistant.components.light import (
|
from homeassistant.components.light import (
|
||||||
ATTR_BRIGHTNESS, ATTR_HS_COLOR, ATTR_TRANSITION, ATTR_COLOR_TEMP,
|
ATTR_BRIGHTNESS, ATTR_HS_COLOR, ATTR_TRANSITION, ATTR_COLOR_TEMP,
|
||||||
ATTR_FLASH, FLASH_SHORT, FLASH_LONG, ATTR_EFFECT, SUPPORT_BRIGHTNESS,
|
ATTR_FLASH, FLASH_SHORT, FLASH_LONG, ATTR_EFFECT, SUPPORT_BRIGHTNESS,
|
||||||
SUPPORT_COLOR, SUPPORT_TRANSITION, SUPPORT_COLOR_TEMP, SUPPORT_FLASH,
|
SUPPORT_COLOR, SUPPORT_TRANSITION, SUPPORT_COLOR_TEMP, SUPPORT_FLASH,
|
||||||
SUPPORT_EFFECT, Light, PLATFORM_SCHEMA, ATTR_ENTITY_ID, DOMAIN)
|
SUPPORT_EFFECT, Light)
|
||||||
import homeassistant.helpers.config_validation as cv
|
|
||||||
import homeassistant.util.color as color_util
|
import homeassistant.util.color as color_util
|
||||||
|
from homeassistant.components.yeelight import (
|
||||||
|
CONF_TRANSITION, DATA_YEELIGHT, CONF_MODE_MUSIC,
|
||||||
|
CONF_SAVE_ON_CHANGE, CONF_CUSTOM_EFFECTS, DATA_UPDATED)
|
||||||
|
|
||||||
REQUIREMENTS = ['yeelight==0.4.3']
|
DEPENDENCIES = ['yeelight']
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
LEGACY_DEVICE_TYPE_MAP = {
|
|
||||||
'color1': 'rgb',
|
|
||||||
'mono1': 'white',
|
|
||||||
'strip1': 'strip',
|
|
||||||
'bslamp1': 'bedside',
|
|
||||||
'ceiling1': 'ceiling',
|
|
||||||
}
|
|
||||||
|
|
||||||
DEFAULT_NAME = 'Yeelight'
|
|
||||||
DEFAULT_TRANSITION = 350
|
|
||||||
|
|
||||||
CONF_MODEL = 'model'
|
|
||||||
CONF_TRANSITION = 'transition'
|
|
||||||
CONF_SAVE_ON_CHANGE = 'save_on_change'
|
|
||||||
CONF_MODE_MUSIC = 'use_music_mode'
|
|
||||||
CONF_CUSTOM_EFFECTS = 'custom_effects'
|
|
||||||
CONF_FLOW_PARAMS = 'flow_params'
|
|
||||||
|
|
||||||
DATA_KEY = 'light.yeelight'
|
|
||||||
|
|
||||||
ATTR_MODE = 'mode'
|
|
||||||
ATTR_COUNT = 'count'
|
|
||||||
ATTR_ACTION = 'action'
|
|
||||||
ATTR_TRANSITIONS = 'transitions'
|
|
||||||
|
|
||||||
ACTION_RECOVER = 'recover'
|
|
||||||
ACTION_STAY = 'stay'
|
|
||||||
ACTION_OFF = 'off'
|
|
||||||
|
|
||||||
YEELIGHT_RGB_TRANSITION = 'RGBTransition'
|
|
||||||
YEELIGHT_HSV_TRANSACTION = 'HSVTransition'
|
|
||||||
YEELIGHT_TEMPERATURE_TRANSACTION = 'TemperatureTransition'
|
|
||||||
YEELIGHT_SLEEP_TRANSACTION = 'SleepTransition'
|
|
||||||
|
|
||||||
YEELIGHT_SERVICE_SCHEMA = vol.Schema({
|
|
||||||
vol.Required(ATTR_ENTITY_ID): cv.entity_ids,
|
|
||||||
})
|
|
||||||
|
|
||||||
YEELIGHT_FLOW_TRANSITION_SCHEMA = {
|
|
||||||
vol.Optional(ATTR_COUNT, default=0): cv.positive_int,
|
|
||||||
vol.Optional(ATTR_ACTION, default=ACTION_RECOVER):
|
|
||||||
vol.Any(ACTION_RECOVER, ACTION_OFF, ACTION_STAY),
|
|
||||||
vol.Required(ATTR_TRANSITIONS): [{
|
|
||||||
vol.Exclusive(YEELIGHT_RGB_TRANSITION, CONF_TRANSITION):
|
|
||||||
vol.All(cv.ensure_list, [cv.positive_int]),
|
|
||||||
vol.Exclusive(YEELIGHT_HSV_TRANSACTION, CONF_TRANSITION):
|
|
||||||
vol.All(cv.ensure_list, [cv.positive_int]),
|
|
||||||
vol.Exclusive(YEELIGHT_TEMPERATURE_TRANSACTION, CONF_TRANSITION):
|
|
||||||
vol.All(cv.ensure_list, [cv.positive_int]),
|
|
||||||
vol.Exclusive(YEELIGHT_SLEEP_TRANSACTION, CONF_TRANSITION):
|
|
||||||
vol.All(cv.ensure_list, [cv.positive_int]),
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
|
|
||||||
DEVICE_SCHEMA = vol.Schema({
|
|
||||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
|
||||||
vol.Optional(CONF_TRANSITION, default=DEFAULT_TRANSITION): cv.positive_int,
|
|
||||||
vol.Optional(CONF_MODE_MUSIC, default=False): cv.boolean,
|
|
||||||
vol.Optional(CONF_SAVE_ON_CHANGE, default=False): cv.boolean,
|
|
||||||
vol.Optional(CONF_MODEL): cv.string,
|
|
||||||
})
|
|
||||||
|
|
||||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
|
||||||
vol.Optional(CONF_DEVICES, default={}): {cv.string: DEVICE_SCHEMA},
|
|
||||||
vol.Optional(CONF_CUSTOM_EFFECTS): [{
|
|
||||||
vol.Required(CONF_NAME): cv.string,
|
|
||||||
vol.Required(CONF_FLOW_PARAMS): YEELIGHT_FLOW_TRANSITION_SCHEMA
|
|
||||||
}]
|
|
||||||
})
|
|
||||||
|
|
||||||
SUPPORT_YEELIGHT = (SUPPORT_BRIGHTNESS |
|
SUPPORT_YEELIGHT = (SUPPORT_BRIGHTNESS |
|
||||||
SUPPORT_TRANSITION |
|
SUPPORT_TRANSITION |
|
||||||
SUPPORT_FLASH)
|
SUPPORT_FLASH)
|
||||||
@ -143,9 +70,6 @@ YEELIGHT_EFFECT_LIST = [
|
|||||||
EFFECT_TWITTER,
|
EFFECT_TWITTER,
|
||||||
EFFECT_STOP]
|
EFFECT_STOP]
|
||||||
|
|
||||||
SERVICE_SET_MODE = 'yeelight_set_mode'
|
|
||||||
SERVICE_START_FLOW = 'yeelight_start_flow'
|
|
||||||
|
|
||||||
|
|
||||||
def _cmd(func):
|
def _cmd(func):
|
||||||
"""Define a wrapper to catch exceptions from the bulb."""
|
"""Define a wrapper to catch exceptions from the bulb."""
|
||||||
@ -160,117 +84,39 @@ def _cmd(func):
|
|||||||
return _wrap
|
return _wrap
|
||||||
|
|
||||||
|
|
||||||
def _parse_custom_effects(effects_config):
|
|
||||||
import yeelight
|
|
||||||
|
|
||||||
effects = {}
|
|
||||||
for config in effects_config:
|
|
||||||
params = config[CONF_FLOW_PARAMS]
|
|
||||||
action = yeelight.Flow.actions[params[ATTR_ACTION]]
|
|
||||||
transitions = YeelightLight.transitions_config_parser(
|
|
||||||
params[ATTR_TRANSITIONS])
|
|
||||||
|
|
||||||
effects[config[CONF_NAME]] = {
|
|
||||||
ATTR_COUNT: params[ATTR_COUNT],
|
|
||||||
ATTR_ACTION: action,
|
|
||||||
ATTR_TRANSITIONS: transitions
|
|
||||||
}
|
|
||||||
|
|
||||||
return effects
|
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||||
"""Set up the Yeelight bulbs."""
|
"""Set up the Yeelight bulbs."""
|
||||||
from yeelight.enums import PowerMode
|
if not discovery_info:
|
||||||
|
return
|
||||||
|
|
||||||
if DATA_KEY not in hass.data:
|
yeelight_data = hass.data[DATA_YEELIGHT]
|
||||||
hass.data[DATA_KEY] = {}
|
ipaddr = discovery_info[CONF_HOST]
|
||||||
|
device = yeelight_data[CONF_DEVICES][ipaddr]
|
||||||
|
_LOGGER.debug("Adding %s", device.name)
|
||||||
|
|
||||||
lights = []
|
custom_effects = discovery_info[CONF_CUSTOM_EFFECTS]
|
||||||
if discovery_info is not None:
|
light = YeelightLight(device, custom_effects=custom_effects)
|
||||||
_LOGGER.debug("Adding autodetected %s", discovery_info['hostname'])
|
|
||||||
|
|
||||||
device_type = discovery_info['device_type']
|
yeelight_data[CONF_LIGHTS][ipaddr] = light
|
||||||
legacy_device_type = LEGACY_DEVICE_TYPE_MAP.get(device_type,
|
add_entities([light], True)
|
||||||
device_type)
|
|
||||||
|
|
||||||
# Not using hostname, as it seems to vary.
|
|
||||||
name = "yeelight_%s_%s" % (legacy_device_type,
|
|
||||||
discovery_info['properties']['mac'])
|
|
||||||
device = {'name': name, 'ipaddr': discovery_info['host']}
|
|
||||||
|
|
||||||
light = YeelightLight(device, DEVICE_SCHEMA({CONF_MODEL: device_type}))
|
|
||||||
lights.append(light)
|
|
||||||
hass.data[DATA_KEY][name] = light
|
|
||||||
else:
|
|
||||||
for ipaddr, device_config in config[CONF_DEVICES].items():
|
|
||||||
name = device_config[CONF_NAME]
|
|
||||||
_LOGGER.debug("Adding configured %s", name)
|
|
||||||
|
|
||||||
device = {'name': name, 'ipaddr': ipaddr}
|
|
||||||
|
|
||||||
if CONF_CUSTOM_EFFECTS in config:
|
|
||||||
custom_effects = \
|
|
||||||
_parse_custom_effects(config[CONF_CUSTOM_EFFECTS])
|
|
||||||
else:
|
|
||||||
custom_effects = None
|
|
||||||
|
|
||||||
light = YeelightLight(device, device_config,
|
|
||||||
custom_effects=custom_effects)
|
|
||||||
lights.append(light)
|
|
||||||
hass.data[DATA_KEY][name] = light
|
|
||||||
|
|
||||||
add_entities(lights, True)
|
|
||||||
|
|
||||||
def service_handler(service):
|
|
||||||
"""Dispatch service calls to target entities."""
|
|
||||||
params = {key: value for key, value in service.data.items()
|
|
||||||
if key != ATTR_ENTITY_ID}
|
|
||||||
entity_ids = service.data.get(ATTR_ENTITY_ID)
|
|
||||||
target_devices = [dev for dev in hass.data[DATA_KEY].values()
|
|
||||||
if dev.entity_id in entity_ids]
|
|
||||||
|
|
||||||
for target_device in target_devices:
|
|
||||||
if service.service == SERVICE_SET_MODE:
|
|
||||||
target_device.set_mode(**params)
|
|
||||||
elif service.service == SERVICE_START_FLOW:
|
|
||||||
target_device.start_flow(**params)
|
|
||||||
|
|
||||||
service_schema_set_mode = YEELIGHT_SERVICE_SCHEMA.extend({
|
|
||||||
vol.Required(ATTR_MODE):
|
|
||||||
vol.In([mode.name.lower() for mode in PowerMode])
|
|
||||||
})
|
|
||||||
hass.services.register(
|
|
||||||
DOMAIN, SERVICE_SET_MODE, service_handler,
|
|
||||||
schema=service_schema_set_mode)
|
|
||||||
|
|
||||||
service_schema_start_flow = YEELIGHT_SERVICE_SCHEMA.extend(
|
|
||||||
YEELIGHT_FLOW_TRANSITION_SCHEMA
|
|
||||||
)
|
|
||||||
hass.services.register(
|
|
||||||
DOMAIN, SERVICE_START_FLOW, service_handler,
|
|
||||||
schema=service_schema_start_flow)
|
|
||||||
|
|
||||||
|
|
||||||
class YeelightLight(Light):
|
class YeelightLight(Light):
|
||||||
"""Representation of a Yeelight light."""
|
"""Representation of a Yeelight light."""
|
||||||
|
|
||||||
def __init__(self, device, config, custom_effects=None):
|
def __init__(self, device, custom_effects=None):
|
||||||
"""Initialize the Yeelight light."""
|
"""Initialize the Yeelight light."""
|
||||||
self.config = config
|
self.config = device.config
|
||||||
self._name = device['name']
|
self._device = device
|
||||||
self._ipaddr = device['ipaddr']
|
|
||||||
|
|
||||||
self._supported_features = SUPPORT_YEELIGHT
|
self._supported_features = SUPPORT_YEELIGHT
|
||||||
self._available = False
|
self._available = False
|
||||||
self._bulb_device = None
|
|
||||||
|
|
||||||
self._brightness = None
|
self._brightness = None
|
||||||
self._color_temp = None
|
self._color_temp = None
|
||||||
self._is_on = None
|
self._is_on = None
|
||||||
self._hs = None
|
self._hs = None
|
||||||
|
|
||||||
self._model = config.get('model')
|
|
||||||
self._min_mireds = None
|
self._min_mireds = None
|
||||||
self._max_mireds = None
|
self._max_mireds = None
|
||||||
|
|
||||||
@ -279,6 +125,22 @@ class YeelightLight(Light):
|
|||||||
else:
|
else:
|
||||||
self._custom_effects = {}
|
self._custom_effects = {}
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _schedule_immediate_update(self, ipaddr):
|
||||||
|
if ipaddr == self.device.ipaddr:
|
||||||
|
self.async_schedule_update_ha_state(True)
|
||||||
|
|
||||||
|
async def async_added_to_hass(self):
|
||||||
|
"""Handle entity which will be added."""
|
||||||
|
async_dispatcher_connect(
|
||||||
|
self.hass, DATA_UPDATED, self._schedule_immediate_update
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def should_poll(self):
|
||||||
|
"""No polling needed."""
|
||||||
|
return False
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def available(self) -> bool:
|
def available(self) -> bool:
|
||||||
"""Return if bulb is available."""
|
"""Return if bulb is available."""
|
||||||
@ -302,7 +164,7 @@ class YeelightLight(Light):
|
|||||||
@property
|
@property
|
||||||
def name(self) -> str:
|
def name(self) -> str:
|
||||||
"""Return the name of the device if any."""
|
"""Return the name of the device if any."""
|
||||||
return self._name
|
return self.device.name
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_on(self) -> bool:
|
def is_on(self) -> bool:
|
||||||
@ -363,27 +225,26 @@ class YeelightLight(Light):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def _properties(self) -> dict:
|
def _properties(self) -> dict:
|
||||||
if self._bulb_device is None:
|
if self._bulb is None:
|
||||||
return {}
|
return {}
|
||||||
return self._bulb_device.last_properties
|
return self._bulb.last_properties
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device(self):
|
||||||
|
"""Return yeelight device."""
|
||||||
|
return self._device
|
||||||
|
|
||||||
# F821: https://github.com/PyCQA/pyflakes/issues/373
|
# F821: https://github.com/PyCQA/pyflakes/issues/373
|
||||||
@property
|
@property
|
||||||
def _bulb(self) -> 'yeelight.Bulb': # noqa: F821
|
def _bulb(self) -> 'yeelight.Bulb': # noqa: F821
|
||||||
import yeelight
|
bulb = self.device.bulb
|
||||||
if self._bulb_device is None:
|
|
||||||
try:
|
|
||||||
self._bulb_device = yeelight.Bulb(self._ipaddr,
|
|
||||||
model=self._model)
|
|
||||||
self._bulb_device.get_properties() # force init for type
|
|
||||||
|
|
||||||
self._available = True
|
if bulb:
|
||||||
except yeelight.BulbException as ex:
|
self._available = True
|
||||||
self._available = False
|
return bulb
|
||||||
_LOGGER.error("Failed to connect to bulb %s, %s: %s",
|
|
||||||
self._ipaddr, self._name, ex)
|
|
||||||
|
|
||||||
return self._bulb_device
|
self._available = False
|
||||||
|
return None
|
||||||
|
|
||||||
def set_music_mode(self, mode) -> None:
|
def set_music_mode(self, mode) -> None:
|
||||||
"""Set the music mode on or off."""
|
"""Set the music mode on or off."""
|
||||||
@ -396,12 +257,13 @@ class YeelightLight(Light):
|
|||||||
"""Update properties from the bulb."""
|
"""Update properties from the bulb."""
|
||||||
import yeelight
|
import yeelight
|
||||||
try:
|
try:
|
||||||
self._bulb.get_properties()
|
if self._bulb.bulb_type == yeelight.BulbType.Color:
|
||||||
|
|
||||||
if self._bulb_device.bulb_type == yeelight.BulbType.Color:
|
|
||||||
self._supported_features = SUPPORT_YEELIGHT_RGB
|
self._supported_features = SUPPORT_YEELIGHT_RGB
|
||||||
elif self._bulb_device.bulb_type == yeelight.BulbType.WhiteTemp:
|
elif self._bulb.bulb_type == yeelight.BulbType.WhiteTemp:
|
||||||
self._supported_features = SUPPORT_YEELIGHT_WHITE_TEMP
|
if self._device.is_nightlight_enabled:
|
||||||
|
self._supported_features = SUPPORT_YEELIGHT
|
||||||
|
else:
|
||||||
|
self._supported_features = SUPPORT_YEELIGHT_WHITE_TEMP
|
||||||
|
|
||||||
if self._min_mireds is None:
|
if self._min_mireds is None:
|
||||||
model_specs = self._bulb.get_model_specs()
|
model_specs = self._bulb.get_model_specs()
|
||||||
@ -412,7 +274,11 @@ class YeelightLight(Light):
|
|||||||
|
|
||||||
self._is_on = self._properties.get('power') == 'on'
|
self._is_on = self._properties.get('power') == 'on'
|
||||||
|
|
||||||
bright = self._properties.get('bright', None)
|
if self._device.is_nightlight_enabled:
|
||||||
|
bright = self._properties.get('nl_br', None)
|
||||||
|
else:
|
||||||
|
bright = self._properties.get('bright', None)
|
||||||
|
|
||||||
if bright:
|
if bright:
|
||||||
self._brightness = round(255 * (int(bright) / 100))
|
self._brightness = round(255 * (int(bright) / 100))
|
||||||
|
|
||||||
@ -552,11 +418,7 @@ class YeelightLight(Light):
|
|||||||
if ATTR_TRANSITION in kwargs: # passed kwarg overrides config
|
if ATTR_TRANSITION in kwargs: # passed kwarg overrides config
|
||||||
duration = int(kwargs.get(ATTR_TRANSITION) * 1000) # kwarg in s
|
duration = int(kwargs.get(ATTR_TRANSITION) * 1000) # kwarg in s
|
||||||
|
|
||||||
try:
|
self.device.turn_on(duration=duration)
|
||||||
self._bulb.turn_on(duration=duration)
|
|
||||||
except yeelight.BulbException as ex:
|
|
||||||
_LOGGER.error("Unable to turn the bulb on: %s", ex)
|
|
||||||
return
|
|
||||||
|
|
||||||
if self.config[CONF_MODE_MUSIC] and not self._bulb.music_mode:
|
if self.config[CONF_MODE_MUSIC] and not self._bulb.music_mode:
|
||||||
try:
|
try:
|
||||||
@ -588,46 +450,8 @@ class YeelightLight(Light):
|
|||||||
|
|
||||||
def turn_off(self, **kwargs) -> None:
|
def turn_off(self, **kwargs) -> None:
|
||||||
"""Turn off."""
|
"""Turn off."""
|
||||||
import yeelight
|
|
||||||
duration = int(self.config[CONF_TRANSITION]) # in ms
|
duration = int(self.config[CONF_TRANSITION]) # in ms
|
||||||
if ATTR_TRANSITION in kwargs: # passed kwarg overrides config
|
if ATTR_TRANSITION in kwargs: # passed kwarg overrides config
|
||||||
duration = int(kwargs.get(ATTR_TRANSITION) * 1000) # kwarg in s
|
duration = int(kwargs.get(ATTR_TRANSITION) * 1000) # kwarg in s
|
||||||
try:
|
|
||||||
self._bulb.turn_off(duration=duration)
|
|
||||||
except yeelight.BulbException as ex:
|
|
||||||
_LOGGER.error("Unable to turn the bulb off: %s", ex)
|
|
||||||
|
|
||||||
def set_mode(self, mode: str):
|
self.device.turn_off(duration=duration)
|
||||||
"""Set a power mode."""
|
|
||||||
import yeelight
|
|
||||||
try:
|
|
||||||
self._bulb.set_power_mode(yeelight.enums.PowerMode[mode.upper()])
|
|
||||||
self.async_schedule_update_ha_state(True)
|
|
||||||
except yeelight.BulbException as ex:
|
|
||||||
_LOGGER.error("Unable to set the power mode: %s", ex)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def transitions_config_parser(transitions):
|
|
||||||
"""Parse transitions config into initialized objects."""
|
|
||||||
import yeelight
|
|
||||||
|
|
||||||
transition_objects = []
|
|
||||||
for transition_config in transitions:
|
|
||||||
transition, params = list(transition_config.items())[0]
|
|
||||||
transition_objects.append(getattr(yeelight, transition)(*params))
|
|
||||||
|
|
||||||
return transition_objects
|
|
||||||
|
|
||||||
def start_flow(self, transitions, count=0, action=ACTION_RECOVER):
|
|
||||||
"""Start flow."""
|
|
||||||
import yeelight
|
|
||||||
|
|
||||||
try:
|
|
||||||
flow = yeelight.Flow(
|
|
||||||
count=count,
|
|
||||||
action=yeelight.Flow.actions[action],
|
|
||||||
transitions=self.transitions_config_parser(transitions))
|
|
||||||
|
|
||||||
self._bulb.start_flow(flow)
|
|
||||||
except yeelight.BulbException as ex:
|
|
||||||
_LOGGER.error("Unable to set effect: %s", ex)
|
|
||||||
|
25
homeassistant/components/yeelight/services.yaml
Normal file
25
homeassistant/components/yeelight/services.yaml
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
set_mode:
|
||||||
|
description: Set a operation mode.
|
||||||
|
fields:
|
||||||
|
entity_id:
|
||||||
|
description: Name of the light entity.
|
||||||
|
example: 'light.yeelight'
|
||||||
|
mode:
|
||||||
|
description: Operation mode. Valid values are 'last', 'normal', 'rgb', 'hsv', 'color_flow', 'moonlight'.
|
||||||
|
example: 'moonlight'
|
||||||
|
|
||||||
|
start_flow:
|
||||||
|
description: Start a custom flow, using transitions from https://yeelight.readthedocs.io/en/stable/yeelight.html#flow-objects
|
||||||
|
fields:
|
||||||
|
entity_id:
|
||||||
|
description: Name of the light entity.
|
||||||
|
example: 'light.yeelight'
|
||||||
|
count:
|
||||||
|
description: The number of times to run this flow (0 to run forever).
|
||||||
|
example: 0
|
||||||
|
action:
|
||||||
|
description: The action to take after the flow stops. Can be 'recover', 'stay', 'off'. (default 'recover')
|
||||||
|
example: 'stay'
|
||||||
|
transitions:
|
||||||
|
description: Array of transitions, for desired effect. Examples https://yeelight.readthedocs.io/en/stable/flow.html
|
||||||
|
example: '[{ "TemperatureTransition": [1900, 1000, 80] }, { "TemperatureTransition": [1900, 1000, 10] }]'
|
@ -1813,7 +1813,7 @@ yahooweather==0.10
|
|||||||
# homeassistant.components.yale_smart_alarm.alarm_control_panel
|
# homeassistant.components.yale_smart_alarm.alarm_control_panel
|
||||||
yalesmartalarmclient==0.1.6
|
yalesmartalarmclient==0.1.6
|
||||||
|
|
||||||
# homeassistant.components.yeelight.light
|
# homeassistant.components.yeelight
|
||||||
yeelight==0.4.3
|
yeelight==0.4.3
|
||||||
|
|
||||||
# homeassistant.components.yeelightsunflower.light
|
# homeassistant.components.yeelightsunflower.light
|
||||||
|
Loading…
x
Reference in New Issue
Block a user