mirror of
https://github.com/home-assistant/core.git
synced 2025-07-24 13:47:35 +00:00
WiZ cleanups part 1 (#65746)
Co-authored-by: Franck Nijhof <frenck@frenck.nl>
This commit is contained in:
parent
f3c5f9c972
commit
342f5182b9
@ -203,6 +203,7 @@ homeassistant.components.webostv.*
|
|||||||
homeassistant.components.websocket_api.*
|
homeassistant.components.websocket_api.*
|
||||||
homeassistant.components.wemo.*
|
homeassistant.components.wemo.*
|
||||||
homeassistant.components.whois.*
|
homeassistant.components.whois.*
|
||||||
|
homeassistant.components.wiz.*
|
||||||
homeassistant.components.zodiac.*
|
homeassistant.components.zodiac.*
|
||||||
homeassistant.components.zeroconf.*
|
homeassistant.components.zeroconf.*
|
||||||
homeassistant.components.zone.*
|
homeassistant.components.zone.*
|
||||||
|
@ -1,38 +1,66 @@
|
|||||||
"""WiZ Platform integration."""
|
"""WiZ Platform integration."""
|
||||||
from dataclasses import dataclass
|
from datetime import timedelta
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from pywizlight import wizlight
|
from pywizlight import wizlight
|
||||||
from pywizlight.exceptions import WizLightConnectionError, WizLightTimeOutError
|
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import CONF_HOST
|
from homeassistant.const import CONF_HOST, Platform
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import ConfigEntryNotReady
|
from homeassistant.exceptions import ConfigEntryNotReady
|
||||||
|
from homeassistant.helpers.debounce import Debouncer
|
||||||
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||||
|
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN, WIZ_EXCEPTIONS
|
||||||
|
from .models import WizData
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
PLATFORMS = ["light"]
|
PLATFORMS = [Platform.LIGHT]
|
||||||
|
|
||||||
|
REQUEST_REFRESH_DELAY = 0.35
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
"""Set up the wiz integration from a config entry."""
|
"""Set up the wiz integration from a config entry."""
|
||||||
ip_address = entry.data.get(CONF_HOST)
|
ip_address = entry.data[CONF_HOST]
|
||||||
_LOGGER.debug("Get bulb with IP: %s", ip_address)
|
_LOGGER.debug("Get bulb with IP: %s", ip_address)
|
||||||
|
bulb = wizlight(ip_address)
|
||||||
try:
|
try:
|
||||||
bulb = wizlight(ip_address)
|
|
||||||
scenes = await bulb.getSupportedScenes()
|
|
||||||
await bulb.getMac()
|
await bulb.getMac()
|
||||||
except (
|
scenes = await bulb.getSupportedScenes()
|
||||||
WizLightTimeOutError,
|
# ValueError gets thrown if the bulb type
|
||||||
WizLightConnectionError,
|
# cannot be determined on the first try.
|
||||||
ConnectionRefusedError,
|
# This is likely because way the library
|
||||||
) as err:
|
# processes responses and can be cleaned up
|
||||||
|
# in the future.
|
||||||
|
except (ValueError, *WIZ_EXCEPTIONS) as err:
|
||||||
raise ConfigEntryNotReady from err
|
raise ConfigEntryNotReady from err
|
||||||
|
|
||||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = WizData(bulb=bulb, scenes=scenes)
|
async def _async_update() -> None:
|
||||||
|
"""Update the WiZ device."""
|
||||||
|
try:
|
||||||
|
await bulb.updateState()
|
||||||
|
except WIZ_EXCEPTIONS as ex:
|
||||||
|
raise UpdateFailed(f"Failed to update device at {ip_address}: {ex}") from ex
|
||||||
|
|
||||||
|
coordinator = DataUpdateCoordinator(
|
||||||
|
hass=hass,
|
||||||
|
logger=_LOGGER,
|
||||||
|
name=entry.title,
|
||||||
|
update_interval=timedelta(seconds=15),
|
||||||
|
update_method=_async_update,
|
||||||
|
# We don't want an immediate refresh since the device
|
||||||
|
# takes a moment to reflect the state change
|
||||||
|
request_refresh_debouncer=Debouncer(
|
||||||
|
hass, _LOGGER, cooldown=REQUEST_REFRESH_DELAY, immediate=False
|
||||||
|
),
|
||||||
|
)
|
||||||
|
await coordinator.async_config_entry_first_refresh()
|
||||||
|
|
||||||
|
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = WizData(
|
||||||
|
coordinator=coordinator, bulb=bulb, scenes=scenes
|
||||||
|
)
|
||||||
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
|
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@ -42,11 +70,3 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||||
hass.data[DOMAIN].pop(entry.entry_id)
|
hass.data[DOMAIN].pop(entry.entry_id)
|
||||||
return unload_ok
|
return unload_ok
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class WizData:
|
|
||||||
"""Data for the wiz integration."""
|
|
||||||
|
|
||||||
bulb: wizlight
|
|
||||||
scenes: list
|
|
||||||
|
@ -1,37 +1,38 @@
|
|||||||
"""Config flow for WiZ Platform."""
|
"""Config flow for WiZ Platform."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from pywizlight import wizlight
|
from pywizlight import wizlight
|
||||||
from pywizlight.exceptions import WizLightConnectionError, WizLightTimeOutError
|
from pywizlight.exceptions import WizLightConnectionError, WizLightTimeOutError
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant import config_entries
|
from homeassistant import config_entries
|
||||||
from homeassistant.const import CONF_HOST, CONF_NAME
|
from homeassistant.const import CONF_HOST
|
||||||
|
from homeassistant.data_entry_flow import FlowResult
|
||||||
|
|
||||||
from .const import DEFAULT_NAME, DOMAIN
|
from .const import DEFAULT_NAME, DOMAIN
|
||||||
|
from .utils import _short_mac
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
STEP_USER_DATA_SCHEMA = vol.Schema(
|
|
||||||
{
|
|
||||||
vol.Required(CONF_HOST): str,
|
|
||||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): str,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||||
"""Handle a config flow for WiZ."""
|
"""Handle a config flow for WiZ."""
|
||||||
|
|
||||||
VERSION = 1
|
VERSION = 1
|
||||||
|
|
||||||
async def async_step_user(self, user_input=None):
|
async def async_step_user(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> FlowResult:
|
||||||
"""Handle a flow initialized by the user."""
|
"""Handle a flow initialized by the user."""
|
||||||
errors = {}
|
errors = {}
|
||||||
if user_input is not None:
|
if user_input is not None:
|
||||||
bulb = wizlight(user_input[CONF_HOST])
|
bulb = wizlight(user_input[CONF_HOST])
|
||||||
try:
|
try:
|
||||||
mac = await bulb.getMac()
|
mac = await bulb.getMac()
|
||||||
|
bulbtype = await bulb.get_bulbtype()
|
||||||
except WizLightTimeOutError:
|
except WizLightTimeOutError:
|
||||||
errors["base"] = "bulb_time_out"
|
errors["base"] = "bulb_time_out"
|
||||||
except ConnectionRefusedError:
|
except ConnectionRefusedError:
|
||||||
@ -43,10 +44,18 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
errors["base"] = "unknown"
|
errors["base"] = "unknown"
|
||||||
else:
|
else:
|
||||||
await self.async_set_unique_id(mac)
|
await self.async_set_unique_id(mac)
|
||||||
self._abort_if_unique_id_configured()
|
self._abort_if_unique_id_configured(
|
||||||
return self.async_create_entry(
|
updates={CONF_HOST: user_input[CONF_HOST]}
|
||||||
title=user_input[CONF_NAME], data=user_input
|
|
||||||
)
|
)
|
||||||
|
bulb_type = bulbtype.bulb_type.value if bulbtype else "Unknown"
|
||||||
|
name = f"{DEFAULT_NAME} {bulb_type} {_short_mac(mac)}"
|
||||||
|
return self.async_create_entry(
|
||||||
|
title=name,
|
||||||
|
data=user_input,
|
||||||
|
)
|
||||||
|
|
||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
|
step_id="user",
|
||||||
|
data_schema=vol.Schema({vol.Required(CONF_HOST): str}),
|
||||||
|
errors=errors,
|
||||||
)
|
)
|
||||||
|
@ -1,4 +1,13 @@
|
|||||||
"""Constants for the WiZ Platform integration."""
|
"""Constants for the WiZ Platform integration."""
|
||||||
|
from pywizlight.exceptions import WizLightConnectionError, WizLightTimeOutError
|
||||||
|
|
||||||
DOMAIN = "wiz"
|
DOMAIN = "wiz"
|
||||||
DEFAULT_NAME = "WiZ"
|
DEFAULT_NAME = "WiZ"
|
||||||
|
|
||||||
|
WIZ_EXCEPTIONS = (
|
||||||
|
OSError,
|
||||||
|
WizLightTimeOutError,
|
||||||
|
TimeoutError,
|
||||||
|
WizLightConnectionError,
|
||||||
|
ConnectionRefusedError,
|
||||||
|
)
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
"""WiZ integration."""
|
"""WiZ integration."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from datetime import timedelta
|
import contextlib
|
||||||
import logging
|
import logging
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from pywizlight import PilotBuilder, wizlight
|
from pywizlight import PilotBuilder
|
||||||
from pywizlight.bulblibrary import BulbClass, BulbType
|
from pywizlight.bulblibrary import BulbClass, BulbType
|
||||||
from pywizlight.exceptions import WizLightNotKnownBulb, WizLightTimeOutError
|
from pywizlight.exceptions import WizLightNotKnownBulb
|
||||||
from pywizlight.rgbcw import convertHSfromRGBCW
|
from pywizlight.rgbcw import convertHSfromRGBCW
|
||||||
from pywizlight.scenes import get_id_from_scene_name
|
from pywizlight.scenes import get_id_from_scene_name
|
||||||
|
|
||||||
@ -16,83 +17,177 @@ from homeassistant.components.light import (
|
|||||||
ATTR_EFFECT,
|
ATTR_EFFECT,
|
||||||
ATTR_HS_COLOR,
|
ATTR_HS_COLOR,
|
||||||
ATTR_RGB_COLOR,
|
ATTR_RGB_COLOR,
|
||||||
SUPPORT_BRIGHTNESS,
|
COLOR_MODE_BRIGHTNESS,
|
||||||
SUPPORT_COLOR,
|
COLOR_MODE_COLOR_TEMP,
|
||||||
SUPPORT_COLOR_TEMP,
|
COLOR_MODE_HS,
|
||||||
SUPPORT_EFFECT,
|
SUPPORT_EFFECT,
|
||||||
LightEntity,
|
LightEntity,
|
||||||
)
|
)
|
||||||
from homeassistant.const import CONF_NAME
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
|
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
|
||||||
|
from homeassistant.helpers.entity import DeviceInfo
|
||||||
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
import homeassistant.util.color as color_utils
|
import homeassistant.util.color as color_utils
|
||||||
|
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
|
from .models import WizData
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
SUPPORT_FEATURES_RGB = (
|
DEFAULT_COLOR_MODES = {COLOR_MODE_HS, COLOR_MODE_COLOR_TEMP}
|
||||||
SUPPORT_BRIGHTNESS | SUPPORT_COLOR | SUPPORT_COLOR_TEMP | SUPPORT_EFFECT
|
DEFAULT_MIN_MIREDS = 153
|
||||||
)
|
DEFAULT_MAX_MIREDS = 454
|
||||||
|
|
||||||
|
|
||||||
# set poll interval to 15 sec because of changes from external to the bulb
|
def get_supported_color_modes(bulb_type: BulbType) -> set[str]:
|
||||||
SCAN_INTERVAL = timedelta(seconds=15)
|
"""Flag supported features."""
|
||||||
|
if not bulb_type:
|
||||||
|
# fallback
|
||||||
|
return DEFAULT_COLOR_MODES
|
||||||
|
color_modes = set()
|
||||||
|
try:
|
||||||
|
features = bulb_type.features
|
||||||
|
if features.color:
|
||||||
|
color_modes.add(COLOR_MODE_HS)
|
||||||
|
if features.color_tmp:
|
||||||
|
color_modes.add(COLOR_MODE_COLOR_TEMP)
|
||||||
|
if not color_modes and features.brightness:
|
||||||
|
color_modes.add(COLOR_MODE_BRIGHTNESS)
|
||||||
|
return color_modes
|
||||||
|
except WizLightNotKnownBulb:
|
||||||
|
_LOGGER.warning("Bulb is not present in the library. Fallback to full feature")
|
||||||
|
return DEFAULT_COLOR_MODES
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(hass, entry, async_add_entities):
|
def supports_effects(bulb_type: BulbType) -> bool:
|
||||||
|
"""Check if a bulb supports effects."""
|
||||||
|
with contextlib.suppress(WizLightNotKnownBulb):
|
||||||
|
return bool(bulb_type.features.effect)
|
||||||
|
return True # default is true
|
||||||
|
|
||||||
|
|
||||||
|
def get_min_max_mireds(bulb_type: BulbType) -> tuple[int, int]:
|
||||||
|
"""Return the coldest and warmest color_temp that this light supports."""
|
||||||
|
if bulb_type is None:
|
||||||
|
return DEFAULT_MIN_MIREDS, DEFAULT_MAX_MIREDS
|
||||||
|
# DW bulbs have no kelvin
|
||||||
|
if bulb_type.bulb_type == BulbClass.DW:
|
||||||
|
return 0, 0
|
||||||
|
# If bulbtype is TW or RGB then return the kelvin value
|
||||||
|
try:
|
||||||
|
return color_utils.color_temperature_kelvin_to_mired(
|
||||||
|
bulb_type.kelvin_range.max
|
||||||
|
), color_utils.color_temperature_kelvin_to_mired(bulb_type.kelvin_range.min)
|
||||||
|
except WizLightNotKnownBulb:
|
||||||
|
_LOGGER.debug("Kelvin is not present in the library. Fallback to 6500")
|
||||||
|
return DEFAULT_MIN_MIREDS, DEFAULT_MAX_MIREDS
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entry: ConfigEntry,
|
||||||
|
async_add_entities: AddEntitiesCallback,
|
||||||
|
) -> None:
|
||||||
"""Set up the WiZ Platform from config_flow."""
|
"""Set up the WiZ Platform from config_flow."""
|
||||||
# Assign configuration variables.
|
wiz_data: WizData = hass.data[DOMAIN][entry.entry_id]
|
||||||
wiz_data = hass.data[DOMAIN][entry.entry_id]
|
async_add_entities([WizBulbEntity(wiz_data, entry.title)])
|
||||||
wizbulb = WizBulbEntity(wiz_data.bulb, entry.data.get(CONF_NAME), wiz_data.scenes)
|
|
||||||
# Add devices with defined name
|
|
||||||
async_add_entities([wizbulb], update_before_add=True)
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
class WizBulbEntity(LightEntity):
|
class WizBulbEntity(CoordinatorEntity, LightEntity):
|
||||||
"""Representation of WiZ Light bulb."""
|
"""Representation of WiZ Light bulb."""
|
||||||
|
|
||||||
def __init__(self, light: wizlight, name, scenes):
|
def __init__(self, wiz_data: WizData, name: str) -> None:
|
||||||
"""Initialize an WiZLight."""
|
"""Initialize an WiZLight."""
|
||||||
self._light = light
|
super().__init__(wiz_data.coordinator)
|
||||||
self._state = None
|
self._light = wiz_data.bulb
|
||||||
self._brightness = None
|
bulb_type: BulbType = self._light.bulbtype
|
||||||
|
self._attr_unique_id = self._light.mac
|
||||||
self._attr_name = name
|
self._attr_name = name
|
||||||
self._rgb_color = None
|
self._attr_effect_list = wiz_data.scenes
|
||||||
self._temperature = None
|
self._attr_min_mireds, self._attr_max_mireds = get_min_max_mireds(bulb_type)
|
||||||
self._hscolor = None
|
self._attr_supported_color_modes = get_supported_color_modes(bulb_type)
|
||||||
self._available = None
|
if supports_effects(bulb_type):
|
||||||
self._effect = None
|
self._attr_supported_features = SUPPORT_EFFECT
|
||||||
self._scenes: list[str] = scenes
|
self._attr_device_info = DeviceInfo(
|
||||||
self._bulbtype: BulbType = light.bulbtype
|
connections={(CONNECTION_NETWORK_MAC, self._light.mac)},
|
||||||
self._mac = light.mac
|
name=name,
|
||||||
self._attr_unique_id = light.mac
|
manufacturer="WiZ",
|
||||||
# new init states
|
model=bulb_type.name,
|
||||||
self._attr_min_mireds = self.get_min_mireds()
|
)
|
||||||
self._attr_max_mireds = self.get_max_mireds()
|
|
||||||
self._attr_supported_features = self.get_supported_features()
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def brightness(self):
|
def is_on(self) -> bool | None:
|
||||||
"""Return the brightness of the light."""
|
|
||||||
return self._brightness
|
|
||||||
|
|
||||||
@property
|
|
||||||
def rgb_color(self):
|
|
||||||
"""Return the color property."""
|
|
||||||
return self._rgb_color
|
|
||||||
|
|
||||||
@property
|
|
||||||
def hs_color(self):
|
|
||||||
"""Return the hs color value."""
|
|
||||||
return self._hscolor
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_on(self):
|
|
||||||
"""Return true if light is on."""
|
"""Return true if light is on."""
|
||||||
return self._state
|
is_on: bool | None = self._light.status
|
||||||
|
return is_on
|
||||||
|
|
||||||
async def async_turn_on(self, **kwargs):
|
@property
|
||||||
|
def brightness(self) -> int | None:
|
||||||
|
"""Return the brightness of the light."""
|
||||||
|
if (brightness := self._light.state.get_brightness()) is None:
|
||||||
|
return None
|
||||||
|
if 0 <= int(brightness) <= 255:
|
||||||
|
return int(brightness)
|
||||||
|
_LOGGER.error("Received invalid brightness : %s. Expected: 0-255", brightness)
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def color_mode(self) -> str:
|
||||||
|
"""Return the current color mode."""
|
||||||
|
color_modes = self.supported_color_modes
|
||||||
|
assert color_modes is not None
|
||||||
|
if (
|
||||||
|
COLOR_MODE_COLOR_TEMP in color_modes
|
||||||
|
and self._light.state.get_colortemp() is not None
|
||||||
|
):
|
||||||
|
return COLOR_MODE_COLOR_TEMP
|
||||||
|
if (
|
||||||
|
COLOR_MODE_HS in color_modes
|
||||||
|
and (rgb := self._light.state.get_rgb()) is not None
|
||||||
|
and rgb[0] is not None
|
||||||
|
):
|
||||||
|
return COLOR_MODE_HS
|
||||||
|
return COLOR_MODE_BRIGHTNESS
|
||||||
|
|
||||||
|
@property
|
||||||
|
def hs_color(self) -> tuple[float, float] | None:
|
||||||
|
"""Return the hs color value."""
|
||||||
|
colortemp = self._light.state.get_colortemp()
|
||||||
|
if colortemp is not None and colortemp != 0:
|
||||||
|
return None
|
||||||
|
if (rgb := self._light.state.get_rgb()) is None:
|
||||||
|
return None
|
||||||
|
if rgb[0] is None:
|
||||||
|
# this is the case if the temperature was changed
|
||||||
|
# do nothing until the RGB color was changed
|
||||||
|
return None
|
||||||
|
if (warmwhite := self._light.state.get_warm_white()) is None:
|
||||||
|
return None
|
||||||
|
hue_sat = convertHSfromRGBCW(rgb, warmwhite)
|
||||||
|
hue: float = hue_sat[0]
|
||||||
|
sat: float = hue_sat[1]
|
||||||
|
return hue, sat
|
||||||
|
|
||||||
|
@property
|
||||||
|
def color_temp(self) -> int | None:
|
||||||
|
"""Return the CT color value in mireds."""
|
||||||
|
colortemp = self._light.state.get_colortemp()
|
||||||
|
if colortemp is None or colortemp == 0:
|
||||||
|
return None
|
||||||
|
_LOGGER.debug(
|
||||||
|
"[wizlight %s] kelvin from the bulb: %s", self._light.ip, colortemp
|
||||||
|
)
|
||||||
|
return color_utils.color_temperature_kelvin_to_mired(colortemp)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def effect(self) -> str | None:
|
||||||
|
"""Return the current effect."""
|
||||||
|
effect: str | None = self._light.state.get_scene()
|
||||||
|
return effect
|
||||||
|
|
||||||
|
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||||
"""Instruct the light to turn on."""
|
"""Instruct the light to turn on."""
|
||||||
brightness = None
|
brightness = None
|
||||||
|
|
||||||
@ -150,199 +245,9 @@ class WizBulbEntity(LightEntity):
|
|||||||
)
|
)
|
||||||
|
|
||||||
await self._light.turn_on(pilot)
|
await self._light.turn_on(pilot)
|
||||||
|
await self.coordinator.async_request_refresh()
|
||||||
|
|
||||||
async def async_turn_off(self, **kwargs):
|
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||||
"""Instruct the light to turn off."""
|
"""Instruct the light to turn off."""
|
||||||
await self._light.turn_off()
|
await self._light.turn_off()
|
||||||
|
await self.coordinator.async_request_refresh()
|
||||||
@property
|
|
||||||
def color_temp(self):
|
|
||||||
"""Return the CT color value in mireds."""
|
|
||||||
return self._temperature
|
|
||||||
|
|
||||||
def get_min_mireds(self) -> int:
|
|
||||||
"""Return the coldest color_temp that this light supports."""
|
|
||||||
if self._bulbtype is None:
|
|
||||||
return color_utils.color_temperature_kelvin_to_mired(6500)
|
|
||||||
# DW bulbs have no kelvin
|
|
||||||
if self._bulbtype.bulb_type == BulbClass.DW:
|
|
||||||
return 0
|
|
||||||
# If bulbtype is TW or RGB then return the kelvin value
|
|
||||||
try:
|
|
||||||
return color_utils.color_temperature_kelvin_to_mired(
|
|
||||||
self._bulbtype.kelvin_range.max
|
|
||||||
)
|
|
||||||
except WizLightNotKnownBulb:
|
|
||||||
_LOGGER.debug("Kelvin is not present in the library. Fallback to 6500")
|
|
||||||
return color_utils.color_temperature_kelvin_to_mired(6500)
|
|
||||||
|
|
||||||
def get_max_mireds(self) -> int:
|
|
||||||
"""Return the warmest color_temp that this light supports."""
|
|
||||||
if self._bulbtype is None:
|
|
||||||
return color_utils.color_temperature_kelvin_to_mired(2200)
|
|
||||||
# DW bulbs have no kelvin
|
|
||||||
if self._bulbtype.bulb_type == BulbClass.DW:
|
|
||||||
return 0
|
|
||||||
# If bulbtype is TW or RGB then return the kelvin value
|
|
||||||
try:
|
|
||||||
return color_utils.color_temperature_kelvin_to_mired(
|
|
||||||
self._bulbtype.kelvin_range.min
|
|
||||||
)
|
|
||||||
except WizLightNotKnownBulb:
|
|
||||||
_LOGGER.debug("Kelvin is not present in the library. Fallback to 2200")
|
|
||||||
return color_utils.color_temperature_kelvin_to_mired(2200)
|
|
||||||
|
|
||||||
def get_supported_features(self) -> int:
|
|
||||||
"""Flag supported features."""
|
|
||||||
if not self._bulbtype:
|
|
||||||
# fallback
|
|
||||||
return SUPPORT_FEATURES_RGB
|
|
||||||
features = 0
|
|
||||||
try:
|
|
||||||
# Map features for better reading
|
|
||||||
if self._bulbtype.features.brightness:
|
|
||||||
features = features | SUPPORT_BRIGHTNESS
|
|
||||||
if self._bulbtype.features.color:
|
|
||||||
features = features | SUPPORT_COLOR
|
|
||||||
if self._bulbtype.features.effect:
|
|
||||||
features = features | SUPPORT_EFFECT
|
|
||||||
if self._bulbtype.features.color_tmp:
|
|
||||||
features = features | SUPPORT_COLOR_TEMP
|
|
||||||
return features
|
|
||||||
except WizLightNotKnownBulb:
|
|
||||||
_LOGGER.warning(
|
|
||||||
"Bulb is not present in the library. Fallback to full feature"
|
|
||||||
)
|
|
||||||
return SUPPORT_FEATURES_RGB
|
|
||||||
|
|
||||||
@property
|
|
||||||
def effect(self):
|
|
||||||
"""Return the current effect."""
|
|
||||||
return self._effect
|
|
||||||
|
|
||||||
@property
|
|
||||||
def effect_list(self):
|
|
||||||
"""Return the list of supported effects.
|
|
||||||
|
|
||||||
URL: https://docs.pro.wizconnected.com/#light-modes
|
|
||||||
"""
|
|
||||||
return self._scenes
|
|
||||||
|
|
||||||
@property
|
|
||||||
def available(self):
|
|
||||||
"""Return if light is available."""
|
|
||||||
return self._available
|
|
||||||
|
|
||||||
async def async_update(self):
|
|
||||||
"""Fetch new state data for this light."""
|
|
||||||
await self.update_state()
|
|
||||||
|
|
||||||
if self._state is not None and self._state is not False:
|
|
||||||
self.update_brightness()
|
|
||||||
self.update_temperature()
|
|
||||||
self.update_color()
|
|
||||||
self.update_effect()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def device_info(self):
|
|
||||||
"""Get device specific attributes."""
|
|
||||||
return {
|
|
||||||
"connections": {(CONNECTION_NETWORK_MAC, self._mac)},
|
|
||||||
"name": self._attr_name,
|
|
||||||
"manufacturer": "WiZ Light Platform",
|
|
||||||
"model": self._bulbtype.name,
|
|
||||||
}
|
|
||||||
|
|
||||||
def update_state_available(self):
|
|
||||||
"""Update the state if bulb is available."""
|
|
||||||
self._state = self._light.status
|
|
||||||
self._available = True
|
|
||||||
|
|
||||||
def update_state_unavailable(self):
|
|
||||||
"""Update the state if bulb is unavailable."""
|
|
||||||
self._state = False
|
|
||||||
self._available = False
|
|
||||||
|
|
||||||
async def update_state(self):
|
|
||||||
"""Update the state."""
|
|
||||||
try:
|
|
||||||
await self._light.updateState()
|
|
||||||
except (ConnectionRefusedError, TimeoutError, WizLightTimeOutError) as ex:
|
|
||||||
_LOGGER.debug(ex)
|
|
||||||
self.update_state_unavailable()
|
|
||||||
else:
|
|
||||||
if self._light.state is None:
|
|
||||||
self.update_state_unavailable()
|
|
||||||
else:
|
|
||||||
self.update_state_available()
|
|
||||||
_LOGGER.debug(
|
|
||||||
"[wizlight %s] updated state: %s and available: %s",
|
|
||||||
self._light.ip,
|
|
||||||
self._state,
|
|
||||||
self._available,
|
|
||||||
)
|
|
||||||
|
|
||||||
def update_brightness(self):
|
|
||||||
"""Update the brightness."""
|
|
||||||
if self._light.state.get_brightness() is None:
|
|
||||||
return
|
|
||||||
brightness = self._light.state.get_brightness()
|
|
||||||
if 0 <= int(brightness) <= 255:
|
|
||||||
self._brightness = int(brightness)
|
|
||||||
else:
|
|
||||||
_LOGGER.error(
|
|
||||||
"Received invalid brightness : %s. Expected: 0-255", brightness
|
|
||||||
)
|
|
||||||
self._brightness = None
|
|
||||||
|
|
||||||
def update_temperature(self):
|
|
||||||
"""Update the temperature."""
|
|
||||||
colortemp = self._light.state.get_colortemp()
|
|
||||||
if colortemp is None or colortemp == 0:
|
|
||||||
self._temperature = None
|
|
||||||
return
|
|
||||||
|
|
||||||
_LOGGER.debug(
|
|
||||||
"[wizlight %s] kelvin from the bulb: %s", self._light.ip, colortemp
|
|
||||||
)
|
|
||||||
temperature = color_utils.color_temperature_kelvin_to_mired(colortemp)
|
|
||||||
self._temperature = temperature
|
|
||||||
|
|
||||||
def update_color(self):
|
|
||||||
"""Update the hs color."""
|
|
||||||
colortemp = self._light.state.get_colortemp()
|
|
||||||
if colortemp is not None and colortemp != 0:
|
|
||||||
self._hscolor = None
|
|
||||||
return
|
|
||||||
if self._light.state.get_rgb() is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
rgb = self._light.state.get_rgb()
|
|
||||||
if rgb[0] is None:
|
|
||||||
# this is the case if the temperature was changed
|
|
||||||
# do nothing until the RGB color was changed
|
|
||||||
return
|
|
||||||
warmwhite = self._light.state.get_warm_white()
|
|
||||||
if warmwhite is None:
|
|
||||||
return
|
|
||||||
self._hscolor = convertHSfromRGBCW(rgb, warmwhite)
|
|
||||||
|
|
||||||
def update_effect(self):
|
|
||||||
"""Update the bulb scene."""
|
|
||||||
self._effect = self._light.state.get_scene()
|
|
||||||
|
|
||||||
async def get_bulb_type(self):
|
|
||||||
"""Get the bulb type."""
|
|
||||||
if self._bulbtype is not None:
|
|
||||||
return self._bulbtype
|
|
||||||
try:
|
|
||||||
self._bulbtype = await self._light.get_bulbtype()
|
|
||||||
_LOGGER.info(
|
|
||||||
"[wizlight %s] Initiate the WiZ bulb as %s",
|
|
||||||
self._light.ip,
|
|
||||||
self._bulbtype.name,
|
|
||||||
)
|
|
||||||
except WizLightTimeOutError:
|
|
||||||
_LOGGER.debug(
|
|
||||||
"[wizlight %s] Bulbtype update failed - Timeout", self._light.ip
|
|
||||||
)
|
|
||||||
|
@ -3,11 +3,7 @@
|
|||||||
"name": "WiZ",
|
"name": "WiZ",
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/wiz",
|
"documentation": "https://www.home-assistant.io/integrations/wiz",
|
||||||
"requirements": [
|
"requirements": ["pywizlight==0.4.16"],
|
||||||
"pywizlight==0.4.15"
|
|
||||||
],
|
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"codeowners": [
|
"codeowners": ["@sbidy"]
|
||||||
"@sbidy"
|
|
||||||
]
|
|
||||||
}
|
}
|
15
homeassistant/components/wiz/models.py
Normal file
15
homeassistant/components/wiz/models.py
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
"""WiZ integration models."""
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from pywizlight import wizlight
|
||||||
|
|
||||||
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class WizData:
|
||||||
|
"""Data for the wiz integration."""
|
||||||
|
|
||||||
|
coordinator: DataUpdateCoordinator
|
||||||
|
bulb: wizlight
|
||||||
|
scenes: list
|
@ -3,13 +3,9 @@
|
|||||||
"step": {
|
"step": {
|
||||||
"user": {
|
"user": {
|
||||||
"data": {
|
"data": {
|
||||||
"host": "[%key:common::config_flow::data::host%]",
|
"host": "[%key:common::config_flow::data::host%]"
|
||||||
"name": "[%key:common::config_flow::data::name%]"
|
|
||||||
},
|
},
|
||||||
"description": "Please enter a hostname or IP address and name to add a new bulb:"
|
"description": "Enter the IP address of the device."
|
||||||
},
|
|
||||||
"confirm": {
|
|
||||||
"description": "[%key:common::config_flow::description::confirm_setup%]"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
|
@ -11,15 +11,11 @@
|
|||||||
"unknown": "Unexpected error"
|
"unknown": "Unexpected error"
|
||||||
},
|
},
|
||||||
"step": {
|
"step": {
|
||||||
"confirm": {
|
|
||||||
"description": "Do you want to add a new Bulb?"
|
|
||||||
},
|
|
||||||
"user": {
|
"user": {
|
||||||
"data": {
|
"data": {
|
||||||
"host": "Hostname or IP",
|
"host": "Host"
|
||||||
"name": "Name"
|
|
||||||
},
|
},
|
||||||
"description": "Please enter a hostname or IP address and name to add a new bulb:"
|
"description": "Enter the IP address of the device."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
7
homeassistant/components/wiz/utils.py
Normal file
7
homeassistant/components/wiz/utils.py
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
"""WiZ utils."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
|
||||||
|
def _short_mac(mac: str) -> str:
|
||||||
|
"""Get the short mac address from the full mac."""
|
||||||
|
return mac.replace(":", "").upper()[-6:]
|
11
mypy.ini
11
mypy.ini
@ -2050,6 +2050,17 @@ no_implicit_optional = true
|
|||||||
warn_return_any = true
|
warn_return_any = true
|
||||||
warn_unreachable = true
|
warn_unreachable = true
|
||||||
|
|
||||||
|
[mypy-homeassistant.components.wiz.*]
|
||||||
|
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.zodiac.*]
|
[mypy-homeassistant.components.zodiac.*]
|
||||||
check_untyped_defs = true
|
check_untyped_defs = true
|
||||||
disallow_incomplete_defs = true
|
disallow_incomplete_defs = true
|
||||||
|
@ -2051,7 +2051,7 @@ pywemo==0.7.0
|
|||||||
pywilight==0.0.70
|
pywilight==0.0.70
|
||||||
|
|
||||||
# homeassistant.components.wiz
|
# homeassistant.components.wiz
|
||||||
pywizlight==0.4.15
|
pywizlight==0.4.16
|
||||||
|
|
||||||
# homeassistant.components.xeoma
|
# homeassistant.components.xeoma
|
||||||
pyxeoma==1.4.1
|
pyxeoma==1.4.1
|
||||||
|
@ -1276,7 +1276,7 @@ pywemo==0.7.0
|
|||||||
pywilight==0.0.70
|
pywilight==0.0.70
|
||||||
|
|
||||||
# homeassistant.components.wiz
|
# homeassistant.components.wiz
|
||||||
pywizlight==0.4.15
|
pywizlight==0.4.16
|
||||||
|
|
||||||
# homeassistant.components.zerproc
|
# homeassistant.components.zerproc
|
||||||
pyzerproc==0.4.8
|
pyzerproc==0.4.8
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
"""Test the WiZ Platform config flow."""
|
"""Test the WiZ Platform config flow."""
|
||||||
|
from contextlib import contextmanager
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
@ -9,27 +10,49 @@ from homeassistant.components.wiz.config_flow import (
|
|||||||
WizLightTimeOutError,
|
WizLightTimeOutError,
|
||||||
)
|
)
|
||||||
from homeassistant.components.wiz.const import DOMAIN
|
from homeassistant.components.wiz.const import DOMAIN
|
||||||
from homeassistant.const import CONF_HOST, CONF_NAME
|
from homeassistant.const import CONF_HOST
|
||||||
|
|
||||||
from tests.common import MockConfigEntry
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
FAKE_BULB_CONFIG = '{"method":"getSystemConfig","env":"pro","result":\
|
FAKE_MAC = "ABCABCABCABC"
|
||||||
{"mac":"ABCABCABCABC",\
|
FAKE_BULB_CONFIG = {
|
||||||
"homeId":653906,\
|
"method": "getSystemConfig",
|
||||||
"roomId":989983,\
|
"env": "pro",
|
||||||
"moduleName":"ESP_0711_STR",\
|
"result": {
|
||||||
"fwVersion":"1.21.0",\
|
"mac": FAKE_MAC,
|
||||||
"groupId":0,"drvConf":[20,2],\
|
"homeId": 653906,
|
||||||
"ewf":[255,0,255,255,0,0,0],\
|
"roomId": 989983,
|
||||||
"ewfHex":"ff00ffff000000",\
|
"moduleName": "ESP_0711_STR",
|
||||||
"ping":0}}'
|
"fwVersion": "1.21.0",
|
||||||
|
"groupId": 0,
|
||||||
TEST_SYSTEM_INFO = {"id": "ABCABCABCABC", "name": "Test Bulb"}
|
"drvConf": [20, 2],
|
||||||
|
"ewf": [255, 0, 255, 255, 0, 0, 0],
|
||||||
|
"ewfHex": "ff00ffff000000",
|
||||||
|
"ping": 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
FAKE_EXTENDED_WHITE_RANGE = [2200, 2700, 6500, 6500]
|
||||||
|
TEST_SYSTEM_INFO = {"id": FAKE_MAC, "name": "Test Bulb"}
|
||||||
|
TEST_CONNECTION = {CONF_HOST: "1.1.1.1"}
|
||||||
|
TEST_NO_IP = {CONF_HOST: "this is no IP input"}
|
||||||
|
|
||||||
|
|
||||||
TEST_CONNECTION = {CONF_HOST: "1.1.1.1", CONF_NAME: "Test Bulb"}
|
def _patch_wizlight():
|
||||||
|
@contextmanager
|
||||||
|
def _patcher():
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.wiz.wizlight.getBulbConfig",
|
||||||
|
return_value=FAKE_BULB_CONFIG,
|
||||||
|
), patch(
|
||||||
|
"homeassistant.components.wiz.wizlight.getExtendedWhiteRange",
|
||||||
|
return_value=FAKE_EXTENDED_WHITE_RANGE,
|
||||||
|
), patch(
|
||||||
|
"homeassistant.components.wiz.wizlight.getMac",
|
||||||
|
return_value=FAKE_MAC,
|
||||||
|
):
|
||||||
|
yield
|
||||||
|
|
||||||
TEST_NO_IP = {CONF_HOST: "this is no IP input", CONF_NAME: "Test Bulb"}
|
return _patcher()
|
||||||
|
|
||||||
|
|
||||||
async def test_form(hass):
|
async def test_form(hass):
|
||||||
@ -40,13 +63,7 @@ async def test_form(hass):
|
|||||||
assert result["type"] == "form"
|
assert result["type"] == "form"
|
||||||
assert result["errors"] == {}
|
assert result["errors"] == {}
|
||||||
# Patch functions
|
# Patch functions
|
||||||
with patch(
|
with _patch_wizlight(), patch(
|
||||||
"homeassistant.components.wiz.wizlight.getBulbConfig",
|
|
||||||
return_value=FAKE_BULB_CONFIG,
|
|
||||||
), patch(
|
|
||||||
"homeassistant.components.wiz.wizlight.getMac",
|
|
||||||
return_value="ABCABCABCABC",
|
|
||||||
) as mock_setup, patch(
|
|
||||||
"homeassistant.components.wiz.async_setup_entry",
|
"homeassistant.components.wiz.async_setup_entry",
|
||||||
return_value=True,
|
return_value=True,
|
||||||
) as mock_setup_entry:
|
) as mock_setup_entry:
|
||||||
@ -57,9 +74,10 @@ async def test_form(hass):
|
|||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert result2["type"] == "create_entry"
|
assert result2["type"] == "create_entry"
|
||||||
assert result2["title"] == "Test Bulb"
|
assert result2["title"] == "WiZ Dimmable White ABCABC"
|
||||||
assert result2["data"] == TEST_CONNECTION
|
assert result2["data"] == {
|
||||||
assert len(mock_setup.mock_calls) == 1
|
CONF_HOST: "1.1.1.1",
|
||||||
|
}
|
||||||
assert len(mock_setup_entry.mock_calls) == 1
|
assert len(mock_setup_entry.mock_calls) == 1
|
||||||
|
|
||||||
|
|
||||||
@ -98,8 +116,6 @@ async def test_form_updates_unique_id(hass):
|
|||||||
unique_id=TEST_SYSTEM_INFO["id"],
|
unique_id=TEST_SYSTEM_INFO["id"],
|
||||||
data={
|
data={
|
||||||
CONF_HOST: "dummy",
|
CONF_HOST: "dummy",
|
||||||
CONF_NAME: TEST_SYSTEM_INFO["name"],
|
|
||||||
"id": TEST_SYSTEM_INFO["id"],
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -108,13 +124,7 @@ async def test_form_updates_unique_id(hass):
|
|||||||
result = await hass.config_entries.flow.async_init(
|
result = await hass.config_entries.flow.async_init(
|
||||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||||
)
|
)
|
||||||
with patch(
|
with _patch_wizlight(), patch(
|
||||||
"homeassistant.components.wiz.wizlight.getBulbConfig",
|
|
||||||
return_value=FAKE_BULB_CONFIG,
|
|
||||||
), patch(
|
|
||||||
"homeassistant.components.wiz.wizlight.getMac",
|
|
||||||
return_value="ABCABCABCABC",
|
|
||||||
), patch(
|
|
||||||
"homeassistant.components.wiz.async_setup_entry",
|
"homeassistant.components.wiz.async_setup_entry",
|
||||||
return_value=True,
|
return_value=True,
|
||||||
):
|
):
|
||||||
|
Loading…
x
Reference in New Issue
Block a user