Merge pull request #33862 from home-assistant/rc

0.108.1
This commit is contained in:
Paulus Schoutsen 2020-04-08 22:55:29 -07:00 committed by GitHub
commit eb718bffe0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 341 additions and 153 deletions

View File

@ -68,17 +68,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle the initial step.""" """Handle the initial step."""
errors = {} errors = {}
if user_input is not None: if user_input is not None:
try: info, errors = await self._async_validate_or_error(user_input)
info = await validate_input(self.hass, user_input) if not errors:
except CannotConnect:
errors["base"] = "cannot_connect"
except InvalidAuth:
errors["base"] = "invalid_auth"
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
if "base" not in errors:
await self.async_set_unique_id(info["mac_addr"]) await self.async_set_unique_id(info["mac_addr"])
self._abort_if_unique_id_configured() self._abort_if_unique_id_configured()
return self.async_create_entry(title=info["title"], data=user_input) return self.async_create_entry(title=info["title"], data=user_input)
@ -119,8 +110,31 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
async def async_step_import(self, user_input): async def async_step_import(self, user_input):
"""Handle import.""" """Handle import."""
if user_input:
info, errors = await self._async_validate_or_error(user_input)
if not errors:
await self.async_set_unique_id(
info["mac_addr"], raise_on_progress=False
)
self._abort_if_unique_id_configured()
return self.async_create_entry(title=info["title"], data=user_input)
return await self.async_step_user(user_input) return await self.async_step_user(user_input)
async def _async_validate_or_error(self, user_input):
"""Validate doorbird or error."""
errors = {}
info = {}
try:
info = await validate_input(self.hass, user_input)
except CannotConnect:
errors["base"] = "cannot_connect"
except InvalidAuth:
errors["base"] = "invalid_auth"
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
return info, errors
@staticmethod @staticmethod
@callback @callback
def async_get_options_flow(config_entry): def async_get_options_flow(config_entry):

View File

@ -2,7 +2,7 @@
"domain": "ipp", "domain": "ipp",
"name": "Internet Printing Protocol (IPP)", "name": "Internet Printing Protocol (IPP)",
"documentation": "https://www.home-assistant.io/integrations/ipp", "documentation": "https://www.home-assistant.io/integrations/ipp",
"requirements": ["pyipp==0.9.0"], "requirements": ["pyipp==0.9.1"],
"codeowners": ["@ctalkington"], "codeowners": ["@ctalkington"],
"config_flow": true, "config_flow": true,
"quality_scale": "platinum", "quality_scale": "platinum",

View File

@ -67,7 +67,7 @@ SERVICE_LOW_HZ = "set_low_hz"
SERVICE_SUB_DB = "set_sub_db" SERVICE_SUB_DB = "set_sub_db"
SERVICE_UPDATE_DSP = "update_dsp" SERVICE_UPDATE_DSP = "update_dsp"
DSP_SCAN_INTERVAL = 3600 DSP_SCAN_INTERVAL = timedelta(seconds=3600)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{ {

View File

@ -2,7 +2,6 @@
"domain": "lutron_caseta", "domain": "lutron_caseta",
"name": "Lutron Caseta", "name": "Lutron Caseta",
"documentation": "https://www.home-assistant.io/integrations/lutron_caseta", "documentation": "https://www.home-assistant.io/integrations/lutron_caseta",
"requirements": ["pylutron-caseta==0.6.0"], "requirements": ["pylutron-caseta==0.6.1"],
"dependencies": [],
"codeowners": ["@swails"] "codeowners": ["@swails"]
} }

View File

@ -2,6 +2,7 @@
import asyncio import asyncio
import logging import logging
from async_timeout import timeout
from pymodbus.client.asynchronous import schedulers from pymodbus.client.asynchronous import schedulers
from pymodbus.client.asynchronous.serial import AsyncModbusSerialClient as ClientSerial from pymodbus.client.asynchronous.serial import AsyncModbusSerialClient as ClientSerial
from pymodbus.client.asynchronous.tcp import AsyncModbusTCPClient as ClientTCP from pymodbus.client.asynchronous.tcp import AsyncModbusTCPClient as ClientTCP
@ -246,7 +247,12 @@ class ModbusHub:
await self._connect_delay() await self._connect_delay()
async with self._lock: async with self._lock:
kwargs = {"unit": unit} if unit else {} kwargs = {"unit": unit} if unit else {}
result = await func(address, count, **kwargs) try:
async with timeout(self._config_timeout):
result = await func(address, count, **kwargs)
except asyncio.TimeoutError:
result = None
if isinstance(result, (ModbusException, ExceptionResponse)): if isinstance(result, (ModbusException, ExceptionResponse)):
_LOGGER.error("Hub %s Exception (%s)", self._config_name, result) _LOGGER.error("Hub %s Exception (%s)", self._config_name, result)
return result return result
@ -256,7 +262,11 @@ class ModbusHub:
await self._connect_delay() await self._connect_delay()
async with self._lock: async with self._lock:
kwargs = {"unit": unit} if unit else {} kwargs = {"unit": unit} if unit else {}
await func(address, value, **kwargs) try:
async with timeout(self._config_timeout):
func(address, value, **kwargs)
except asyncio.TimeoutError:
return
async def read_coils(self, unit, address, count): async def read_coils(self, unit, address, count):
"""Read coils.""" """Read coils."""

View File

@ -32,7 +32,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
"If you do not want to have your printer on <br />" "If you do not want to have your printer on <br />"
" at all times, and you would like to monitor <br /> " " at all times, and you would like to monitor <br /> "
"temperatures, please add <br />" "temperatures, please add <br />"
"bed and/or number&#95of&#95tools to your configuration <br />" "bed and/or number&#95;of&#95;tools to your configuration <br />"
"and restart.", "and restart.",
title=NOTIFICATION_TITLE, title=NOTIFICATION_TITLE,
notification_id=NOTIFICATION_ID, notification_id=NOTIFICATION_ID,

View File

@ -1,4 +1,5 @@
"""Support for Recollect Waste curbside collection pickup.""" """Support for Recollect Waste curbside collection pickup."""
from datetime import timedelta
import logging import logging
import recollect_waste import recollect_waste
@ -16,7 +17,7 @@ CONF_PLACE_ID = "place_id"
CONF_SERVICE_ID = "service_id" CONF_SERVICE_ID = "service_id"
DEFAULT_NAME = "recollect_waste" DEFAULT_NAME = "recollect_waste"
ICON = "mdi:trash-can-outline" ICON = "mdi:trash-can-outline"
SCAN_INTERVAL = 86400 SCAN_INTERVAL = timedelta(days=1)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(

View File

@ -15,18 +15,22 @@ from homeassistant.components.light import (
SUPPORT_COLOR_TEMP, SUPPORT_COLOR_TEMP,
Light, Light,
) )
from homeassistant.exceptions import HomeAssistantError
import homeassistant.helpers.device_registry as dr import homeassistant.helpers.device_registry as dr
from homeassistant.helpers.typing import HomeAssistantType from homeassistant.helpers.typing import HomeAssistantType
from homeassistant.util.color import ( from homeassistant.util.color import (
color_temperature_kelvin_to_mired as kelvin_to_mired, color_temperature_kelvin_to_mired as kelvin_to_mired,
color_temperature_mired_to_kelvin as mired_to_kelvin, color_temperature_mired_to_kelvin as mired_to_kelvin,
) )
import homeassistant.util.dt as dt_util
from . import CONF_LIGHT, DOMAIN as TPLINK_DOMAIN from . import CONF_LIGHT, DOMAIN as TPLINK_DOMAIN
from .common import async_add_entities_retry from .common import async_add_entities_retry
PARALLEL_UPDATES = 0 PARALLEL_UPDATES = 0
SCAN_INTERVAL = timedelta(seconds=5) SCAN_INTERVAL = timedelta(seconds=5)
CURRENT_POWER_UPDATE_INTERVAL = timedelta(seconds=60)
HISTORICAL_POWER_UPDATE_INTERVAL = timedelta(minutes=60)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -34,6 +38,21 @@ ATTR_CURRENT_POWER_W = "current_power_w"
ATTR_DAILY_ENERGY_KWH = "daily_energy_kwh" ATTR_DAILY_ENERGY_KWH = "daily_energy_kwh"
ATTR_MONTHLY_ENERGY_KWH = "monthly_energy_kwh" ATTR_MONTHLY_ENERGY_KWH = "monthly_energy_kwh"
LIGHT_STATE_DFT_ON = "dft_on_state"
LIGHT_STATE_ON_OFF = "on_off"
LIGHT_STATE_BRIGHTNESS = "brightness"
LIGHT_STATE_COLOR_TEMP = "color_temp"
LIGHT_STATE_HUE = "hue"
LIGHT_STATE_SATURATION = "saturation"
LIGHT_STATE_ERROR_MSG = "err_msg"
LIGHT_SYSINFO_MAC = "mac"
LIGHT_SYSINFO_ALIAS = "alias"
LIGHT_SYSINFO_MODEL = "model"
LIGHT_SYSINFO_IS_DIMMABLE = "is_dimmable"
LIGHT_SYSINFO_IS_VARIABLE_COLOR_TEMP = "is_variable_color_temp"
LIGHT_SYSINFO_IS_COLOR = "is_color"
async def async_setup_platform(hass, config, add_entities, discovery_info=None): async def async_setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the platform. """Set up the platform.
@ -82,7 +101,21 @@ class LightState(NamedTuple):
brightness: int brightness: int
color_temp: float color_temp: float
hs: Tuple[int, int] hs: Tuple[int, int]
emeter_params: dict
def to_param(self):
"""Return a version that we can send to the bulb."""
if self.color_temp:
color_temp = mired_to_kelvin(self.color_temp)
else:
color_temp = None
return {
LIGHT_STATE_ON_OFF: 1 if self.state else 0,
LIGHT_STATE_BRIGHTNESS: brightness_to_percentage(self.brightness),
LIGHT_STATE_COLOR_TEMP: color_temp,
LIGHT_STATE_HUE: self.hs[0] if self.hs else 0,
LIGHT_STATE_SATURATION: self.hs[1] if self.hs else 0,
}
class LightFeatures(NamedTuple): class LightFeatures(NamedTuple):
@ -107,6 +140,9 @@ class TPLinkSmartBulb(Light):
self._light_state = cast(LightState, None) self._light_state = cast(LightState, None)
self._is_available = True self._is_available = True
self._is_setting_light_state = False self._is_setting_light_state = False
self._last_current_power_update = None
self._last_historical_power_update = None
self._emeter_params = {}
@property @property
def unique_id(self): def unique_id(self):
@ -137,45 +173,42 @@ class TPLinkSmartBulb(Light):
@property @property
def device_state_attributes(self): def device_state_attributes(self):
"""Return the state attributes of the device.""" """Return the state attributes of the device."""
return self._light_state.emeter_params return self._emeter_params
async def async_turn_on(self, **kwargs): async def async_turn_on(self, **kwargs):
"""Turn the light on.""" """Turn the light on."""
brightness = ( if ATTR_BRIGHTNESS in kwargs:
int(kwargs[ATTR_BRIGHTNESS]) brightness = int(kwargs[ATTR_BRIGHTNESS])
if ATTR_BRIGHTNESS in kwargs elif self._light_state.brightness is not None:
else self._light_state.brightness brightness = self._light_state.brightness
if self._light_state.brightness is not None else:
else 255 brightness = 255
)
color_tmp = (
int(kwargs[ATTR_COLOR_TEMP])
if ATTR_COLOR_TEMP in kwargs
else self._light_state.color_temp
)
await self.async_set_light_state_retry( if ATTR_COLOR_TEMP in kwargs:
color_tmp = int(kwargs[ATTR_COLOR_TEMP])
else:
color_tmp = self._light_state.color_temp
if ATTR_HS_COLOR in kwargs:
# TP-Link requires integers.
hue_sat = tuple(int(val) for val in kwargs[ATTR_HS_COLOR])
# TP-Link cannot have both color temp and hue_sat
color_tmp = 0
else:
hue_sat = self._light_state.hs
await self._async_set_light_state_retry(
self._light_state, self._light_state,
LightState( self._light_state._replace(
state=True, state=True, brightness=brightness, color_temp=color_tmp, hs=hue_sat,
brightness=brightness,
color_temp=color_tmp,
hs=tuple(kwargs.get(ATTR_HS_COLOR, self._light_state.hs or ())),
emeter_params=self._light_state.emeter_params,
), ),
) )
async def async_turn_off(self, **kwargs): async def async_turn_off(self, **kwargs):
"""Turn the light off.""" """Turn the light off."""
await self.async_set_light_state_retry( await self._async_set_light_state_retry(
self._light_state, self._light_state, self._light_state._replace(state=False),
LightState(
state=False,
brightness=self._light_state.brightness,
color_temp=self._light_state.color_temp,
hs=self._light_state.hs,
emeter_params=self._light_state.emeter_params,
),
) )
@property @property
@ -214,21 +247,11 @@ class TPLinkSmartBulb(Light):
if self._is_setting_light_state: if self._is_setting_light_state:
return return
# Initial run, perform call blocking.
if not self._light_features:
self.do_update_retry(False)
# Subsequent runs should not block.
else:
self.hass.add_job(self.do_update_retry, True)
def do_update_retry(self, update_state: bool) -> None:
"""Update state data with retry.""" ""
try: try:
# Update light features only once. # Update light features only once.
self._light_features = ( if not self._light_features:
self._light_features or self.get_light_features_retry() self._light_features = self._get_light_features_retry()
) self._light_state = self._get_light_state_retry()
self._light_state = self.get_light_state_retry(self._light_features)
self._is_available = True self._is_available = True
except (SmartDeviceException, OSError) as ex: except (SmartDeviceException, OSError) as ex:
if self._is_available: if self._is_available:
@ -237,45 +260,42 @@ class TPLinkSmartBulb(Light):
) )
self._is_available = False self._is_available = False
# The local variables were updates asyncronousally,
# we need the entity registry to poll this object's properties for
# updated information. Calling schedule_update_ha_state will only
# cause a loop.
if update_state:
self.schedule_update_ha_state()
@property @property
def supported_features(self): def supported_features(self):
"""Flag supported features.""" """Flag supported features."""
return self._light_features.supported_features return self._light_features.supported_features
def get_light_features_retry(self) -> LightFeatures: def _get_light_features_retry(self) -> LightFeatures:
"""Retry the retrieval of the supported features.""" """Retry the retrieval of the supported features."""
try: try:
return self.get_light_features() return self._get_light_features()
except (SmartDeviceException, OSError): except (SmartDeviceException, OSError):
pass pass
_LOGGER.debug("Retrying getting light features") _LOGGER.debug("Retrying getting light features")
return self.get_light_features() return self._get_light_features()
def get_light_features(self): def _get_light_features(self):
"""Determine all supported features in one go.""" """Determine all supported features in one go."""
sysinfo = self.smartbulb.sys_info sysinfo = self.smartbulb.sys_info
supported_features = 0 supported_features = 0
# Calling api here as it reformats
mac = self.smartbulb.mac mac = self.smartbulb.mac
alias = self.smartbulb.alias alias = sysinfo[LIGHT_SYSINFO_ALIAS]
model = self.smartbulb.model model = sysinfo[LIGHT_SYSINFO_MODEL]
min_mireds = None min_mireds = None
max_mireds = None max_mireds = None
if self.smartbulb.is_dimmable: if sysinfo.get(LIGHT_SYSINFO_IS_DIMMABLE):
supported_features += SUPPORT_BRIGHTNESS supported_features += SUPPORT_BRIGHTNESS
if getattr(self.smartbulb, "is_variable_color_temp", False): if sysinfo.get(LIGHT_SYSINFO_IS_VARIABLE_COLOR_TEMP):
supported_features += SUPPORT_COLOR_TEMP supported_features += SUPPORT_COLOR_TEMP
min_mireds = kelvin_to_mired(self.smartbulb.valid_temperature_range[1]) # Have to make another api request here in
max_mireds = kelvin_to_mired(self.smartbulb.valid_temperature_range[0]) # order to not re-implement pyHS100 here
if getattr(self.smartbulb, "is_color", False): max_range, min_range = self.smartbulb.valid_temperature_range
min_mireds = kelvin_to_mired(min_range)
max_mireds = kelvin_to_mired(max_range)
if sysinfo.get(LIGHT_SYSINFO_IS_COLOR):
supported_features += SUPPORT_COLOR supported_features += SUPPORT_COLOR
return LightFeatures( return LightFeatures(
@ -288,110 +308,146 @@ class TPLinkSmartBulb(Light):
max_mireds=max_mireds, max_mireds=max_mireds,
) )
def get_light_state_retry(self, light_features: LightFeatures) -> LightState: def _get_light_state_retry(self) -> LightState:
"""Retry the retrieval of getting light states.""" """Retry the retrieval of getting light states."""
try: try:
return self.get_light_state(light_features) return self._get_light_state()
except (SmartDeviceException, OSError): except (SmartDeviceException, OSError):
pass pass
_LOGGER.debug("Retrying getting light state") _LOGGER.debug("Retrying getting light state")
return self.get_light_state(light_features) return self._get_light_state()
def get_light_state(self, light_features: LightFeatures) -> LightState: def _light_state_from_params(self, light_state_params) -> LightState:
"""Get the light state."""
emeter_params = {}
brightness = None brightness = None
color_temp = None color_temp = None
hue_saturation = None hue_saturation = None
state = self.smartbulb.state == SmartBulb.BULB_STATE_ON light_features = self._light_features
state = bool(light_state_params[LIGHT_STATE_ON_OFF])
if not state and LIGHT_STATE_DFT_ON in light_state_params:
light_state_params = light_state_params[LIGHT_STATE_DFT_ON]
if light_features.supported_features & SUPPORT_BRIGHTNESS: if light_features.supported_features & SUPPORT_BRIGHTNESS:
brightness = brightness_from_percentage(self.smartbulb.brightness) brightness = brightness_from_percentage(
light_state_params[LIGHT_STATE_BRIGHTNESS]
)
if light_features.supported_features & SUPPORT_COLOR_TEMP: if light_features.supported_features & SUPPORT_COLOR_TEMP:
if self.smartbulb.color_temp is not None and self.smartbulb.color_temp != 0: if (
color_temp = kelvin_to_mired(self.smartbulb.color_temp) light_state_params.get(LIGHT_STATE_COLOR_TEMP) is not None
and light_state_params[LIGHT_STATE_COLOR_TEMP] != 0
):
color_temp = kelvin_to_mired(light_state_params[LIGHT_STATE_COLOR_TEMP])
if light_features.supported_features & SUPPORT_COLOR: if light_features.supported_features & SUPPORT_COLOR:
hue, sat, _ = self.smartbulb.hsv hue_saturation = (
hue_saturation = (hue, sat) light_state_params[LIGHT_STATE_HUE],
light_state_params[LIGHT_STATE_SATURATION],
if self.smartbulb.has_emeter:
emeter_params[ATTR_CURRENT_POWER_W] = "{:.1f}".format(
self.smartbulb.current_consumption()
) )
daily_statistics = self.smartbulb.get_emeter_daily()
monthly_statistics = self.smartbulb.get_emeter_monthly()
try:
emeter_params[ATTR_DAILY_ENERGY_KWH] = "{:.3f}".format(
daily_statistics[int(time.strftime("%d"))]
)
emeter_params[ATTR_MONTHLY_ENERGY_KWH] = "{:.3f}".format(
monthly_statistics[int(time.strftime("%m"))]
)
except KeyError:
# device returned no daily/monthly history
pass
return LightState( return LightState(
state=state, state=state,
brightness=brightness, brightness=brightness,
color_temp=color_temp, color_temp=color_temp,
hs=hue_saturation, hs=hue_saturation,
emeter_params=emeter_params,
) )
async def async_set_light_state_retry( def _get_light_state(self) -> LightState:
"""Get the light state."""
self._update_emeter()
return self._light_state_from_params(self.smartbulb.get_light_state())
def _update_emeter(self):
if not self.smartbulb.has_emeter:
return
now = dt_util.utcnow()
if (
not self._last_current_power_update
or self._last_current_power_update + CURRENT_POWER_UPDATE_INTERVAL < now
):
self._last_current_power_update = now
self._emeter_params[ATTR_CURRENT_POWER_W] = "{:.1f}".format(
self.smartbulb.current_consumption()
)
if (
not self._last_historical_power_update
or self._last_historical_power_update + HISTORICAL_POWER_UPDATE_INTERVAL
< now
):
self._last_historical_power_update = now
daily_statistics = self.smartbulb.get_emeter_daily()
monthly_statistics = self.smartbulb.get_emeter_monthly()
try:
self._emeter_params[ATTR_DAILY_ENERGY_KWH] = "{:.3f}".format(
daily_statistics[int(time.strftime("%d"))]
)
self._emeter_params[ATTR_MONTHLY_ENERGY_KWH] = "{:.3f}".format(
monthly_statistics[int(time.strftime("%m"))]
)
except KeyError:
# device returned no daily/monthly history
pass
async def _async_set_light_state_retry(
self, old_light_state: LightState, new_light_state: LightState self, old_light_state: LightState, new_light_state: LightState
) -> None: ) -> None:
"""Set the light state with retry.""" """Set the light state with retry."""
# Optimistically setting the light state.
self._light_state = new_light_state
# Tell the device to set the states. # Tell the device to set the states.
if not _light_state_diff(old_light_state, new_light_state):
# Nothing to do, avoid the executor
return
self._is_setting_light_state = True self._is_setting_light_state = True
try: try:
await self.hass.async_add_executor_job( light_state_params = await self.hass.async_add_executor_job(
self.set_light_state, old_light_state, new_light_state self._set_light_state, old_light_state, new_light_state
) )
self._is_available = True self._is_available = True
self._is_setting_light_state = False self._is_setting_light_state = False
if LIGHT_STATE_ERROR_MSG in light_state_params:
raise HomeAssistantError(light_state_params[LIGHT_STATE_ERROR_MSG])
self._light_state = self._light_state_from_params(light_state_params)
return return
except (SmartDeviceException, OSError): except (SmartDeviceException, OSError):
pass pass
try: try:
_LOGGER.debug("Retrying setting light state") _LOGGER.debug("Retrying setting light state")
await self.hass.async_add_executor_job( light_state_params = await self.hass.async_add_executor_job(
self.set_light_state, old_light_state, new_light_state self._set_light_state, old_light_state, new_light_state
) )
self._is_available = True self._is_available = True
if LIGHT_STATE_ERROR_MSG in light_state_params:
raise HomeAssistantError(light_state_params[LIGHT_STATE_ERROR_MSG])
self._light_state = self._light_state_from_params(light_state_params)
except (SmartDeviceException, OSError) as ex: except (SmartDeviceException, OSError) as ex:
self._is_available = False self._is_available = False
_LOGGER.warning("Could not set data for %s: %s", self.smartbulb.host, ex) _LOGGER.warning("Could not set data for %s: %s", self.smartbulb.host, ex)
self._is_setting_light_state = False self._is_setting_light_state = False
def set_light_state( def _set_light_state(
self, old_light_state: LightState, new_light_state: LightState self, old_light_state: LightState, new_light_state: LightState
) -> None: ) -> None:
"""Set the light state.""" """Set the light state."""
# Calling the API with the new state information. diff = _light_state_diff(old_light_state, new_light_state)
if new_light_state.state != old_light_state.state:
if new_light_state.state:
self.smartbulb.state = SmartBulb.BULB_STATE_ON
else:
self.smartbulb.state = SmartBulb.BULB_STATE_OFF
return
if new_light_state.color_temp != old_light_state.color_temp: if not diff:
self.smartbulb.color_temp = mired_to_kelvin(new_light_state.color_temp) return
brightness_pct = brightness_to_percentage(new_light_state.brightness) return self.smartbulb.set_light_state(diff)
if new_light_state.hs != old_light_state.hs and len(new_light_state.hs) > 1:
hue, sat = new_light_state.hs
hsv = (int(hue), int(sat), brightness_pct) def _light_state_diff(old_light_state: LightState, new_light_state: LightState):
self.smartbulb.hsv = hsv old_state_param = old_light_state.to_param()
elif new_light_state.brightness != old_light_state.brightness: new_state_param = new_light_state.to_param()
self.smartbulb.brightness = brightness_pct
return {
key: value
for key, value in new_state_param.items()
if new_state_param.get(key) != old_state_param.get(key)
}

View File

@ -125,8 +125,8 @@ class VizioOptionsConfigFlow(config_entries.OptionsFlow):
default_include_or_exclude = ( default_include_or_exclude = (
CONF_EXCLUDE CONF_EXCLUDE
if self.config_entry.options if self.config_entry.options
and CONF_EXCLUDE in self.config_entry.options.get(CONF_APPS) and CONF_EXCLUDE in self.config_entry.options.get(CONF_APPS, {})
else CONF_EXCLUDE else CONF_INCLUDE
) )
options.update( options.update(
{ {

View File

@ -8,7 +8,7 @@
"zha-quirks==0.0.38", "zha-quirks==0.0.38",
"zigpy-cc==0.3.1", "zigpy-cc==0.3.1",
"zigpy-deconz==0.8.0", "zigpy-deconz==0.8.0",
"zigpy-homeassistant==0.18.1", "zigpy-homeassistant==0.18.2",
"zigpy-xbee-homeassistant==0.11.0", "zigpy-xbee-homeassistant==0.11.0",
"zigpy-zigate==0.5.1" "zigpy-zigate==0.5.1"
], ],

View File

@ -1,7 +1,7 @@
"""Constants used by Home Assistant components.""" """Constants used by Home Assistant components."""
MAJOR_VERSION = 0 MAJOR_VERSION = 0
MINOR_VERSION = 108 MINOR_VERSION = 108
PATCH_VERSION = "0" PATCH_VERSION = "1"
__short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__ = f"{__short_version__}.{PATCH_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER = (3, 7, 0) REQUIRED_PYTHON_VER = (3, 7, 0)

View File

@ -1336,7 +1336,7 @@ pyintesishome==1.7.1
pyipma==2.0.5 pyipma==2.0.5
# homeassistant.components.ipp # homeassistant.components.ipp
pyipp==0.9.0 pyipp==0.9.1
# homeassistant.components.iqvia # homeassistant.components.iqvia
pyiqvia==0.2.1 pyiqvia==0.2.1
@ -1378,7 +1378,7 @@ pylitejet==0.1
pyloopenergy==0.1.3 pyloopenergy==0.1.3
# homeassistant.components.lutron_caseta # homeassistant.components.lutron_caseta
pylutron-caseta==0.6.0 pylutron-caseta==0.6.1
# homeassistant.components.lutron # homeassistant.components.lutron
pylutron==0.2.5 pylutron==0.2.5
@ -2191,7 +2191,7 @@ zigpy-cc==0.3.1
zigpy-deconz==0.8.0 zigpy-deconz==0.8.0
# homeassistant.components.zha # homeassistant.components.zha
zigpy-homeassistant==0.18.1 zigpy-homeassistant==0.18.2
# homeassistant.components.zha # homeassistant.components.zha
zigpy-xbee-homeassistant==0.11.0 zigpy-xbee-homeassistant==0.11.0

View File

@ -519,7 +519,7 @@ pyicloud==0.9.6.1
pyipma==2.0.5 pyipma==2.0.5
# homeassistant.components.ipp # homeassistant.components.ipp
pyipp==0.9.0 pyipp==0.9.1
# homeassistant.components.iqvia # homeassistant.components.iqvia
pyiqvia==0.2.1 pyiqvia==0.2.1
@ -807,7 +807,7 @@ zigpy-cc==0.3.1
zigpy-deconz==0.8.0 zigpy-deconz==0.8.0
# homeassistant.components.zha # homeassistant.components.zha
zigpy-homeassistant==0.18.1 zigpy-homeassistant==0.18.2
# homeassistant.components.zha # homeassistant.components.zha
zigpy-xbee-homeassistant==0.11.0 zigpy-xbee-homeassistant==0.11.0

View File

@ -127,6 +127,73 @@ async def test_form_import(hass):
assert len(mock_setup_entry.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1
async def test_form_import_with_zeroconf_already_discovered(hass):
"""Test we get the form with import source."""
await hass.async_add_executor_job(
init_recorder_component, hass
) # force in memory db
await setup.async_setup_component(hass, "persistent_notification", {})
# Running the zeroconf init will make the unique id
# in progress
zero_conf = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_ZEROCONF},
data={
"properties": {"macaddress": "1CCAE3DOORBIRD"},
"name": "Doorstation - abc123._axis-video._tcp.local.",
"host": "192.168.1.5",
},
)
assert zero_conf["type"] == data_entry_flow.RESULT_TYPE_FORM
assert zero_conf["step_id"] == "user"
assert zero_conf["errors"] == {}
import_config = VALID_CONFIG.copy()
import_config[CONF_EVENTS] = ["event1", "event2", "event3"]
import_config[CONF_TOKEN] = "imported_token"
import_config[
CONF_CUSTOM_URL
] = "http://legacy.custom.url/should/only/come/in/from/yaml"
doorbirdapi = _get_mock_doorbirdapi_return_values(
ready=[True], info={"WIFI_MAC_ADDR": "1CCAE3DOORBIRD"}
)
with patch(
"homeassistant.components.doorbird.config_flow.DoorBird",
return_value=doorbirdapi,
), patch("homeassistant.components.logbook.async_setup", return_value=True), patch(
"homeassistant.components.doorbird.async_setup", return_value=True
) as mock_setup, patch(
"homeassistant.components.doorbird.async_setup_entry", return_value=True,
) as mock_setup_entry:
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_IMPORT},
data=import_config,
)
assert result["type"] == "create_entry"
assert result["title"] == "1.2.3.4"
assert result["data"] == {
"host": "1.2.3.4",
"name": "mydoorbird",
"password": "password",
"username": "friend",
"events": ["event1", "event2", "event3"],
"token": "imported_token",
# This will go away once we convert to cloud hooks
"hass_url_override": "http://legacy.custom.url/should/only/come/in/from/yaml",
}
# It is not possible to import options at this time
# so they end up in the config entry data and are
# used a fallback when they are not in options
await hass.async_block_till_done()
assert len(mock_setup.mock_calls) == 1
assert len(mock_setup_entry.mock_calls) == 1
async def test_form_zeroconf_wrong_oui(hass): async def test_form_zeroconf_wrong_oui(hass):
"""Test we abort when we get the wrong OUI via zeroconf.""" """Test we abort when we get the wrong OUI via zeroconf."""
await hass.async_add_executor_job( await hass.async_add_executor_job(

View File

@ -85,6 +85,7 @@ def light_mock_data_fixture() -> None:
light_state.update(state) light_state.update(state)
light_state["dft_on_state"] = drt_on_state light_state["dft_on_state"] = drt_on_state
return light_state
set_light_state_patch = patch( set_light_state_patch = patch(
"homeassistant.components.tplink.common.SmartBulb.set_light_state", "homeassistant.components.tplink.common.SmartBulb.set_light_state",
@ -310,7 +311,7 @@ async def test_get_light_state_retry(
if set_state_call_count == 1: if set_state_call_count == 1:
raise SmartDeviceException() raise SmartDeviceException()
light_mock_data.set_light_state(state_data) return light_mock_data.set_light_state(state_data)
light_mock_data.set_light_state_mock.side_effect = set_light_state_side_effect light_mock_data.set_light_state_mock.side_effect = set_light_state_side_effect

View File

@ -1,6 +1,5 @@
"""The tests for the TTS component.""" """The tests for the TTS component."""
import ctypes import ctypes
import os
from unittest.mock import PropertyMock, patch from unittest.mock import PropertyMock, patch
import pytest import pytest
@ -121,6 +120,7 @@ async def test_setup_component_and_test_service(hass, empty_cache_dir):
] == "{}/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3".format( ] == "{}/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3".format(
hass.config.api.base_url hass.config.api.base_url
) )
await hass.async_block_till_done()
assert ( assert (
empty_cache_dir / "42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3" empty_cache_dir / "42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3"
).is_file() ).is_file()
@ -153,6 +153,7 @@ async def test_setup_component_and_test_service_with_config_language(
] == "{}/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491_de_-_demo.mp3".format( ] == "{}/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491_de_-_demo.mp3".format(
hass.config.api.base_url hass.config.api.base_url
) )
await hass.async_block_till_done()
assert ( assert (
empty_cache_dir / "42f18378fd4393d18c8dd11d03fa9563c1e54491_de_-_demo.mp3" empty_cache_dir / "42f18378fd4393d18c8dd11d03fa9563c1e54491_de_-_demo.mp3"
).is_file() ).is_file()
@ -194,6 +195,7 @@ async def test_setup_component_and_test_service_with_service_language(
] == "{}/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491_de_-_demo.mp3".format( ] == "{}/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491_de_-_demo.mp3".format(
hass.config.api.base_url hass.config.api.base_url
) )
await hass.async_block_till_done()
assert ( assert (
empty_cache_dir / "42f18378fd4393d18c8dd11d03fa9563c1e54491_de_-_demo.mp3" empty_cache_dir / "42f18378fd4393d18c8dd11d03fa9563c1e54491_de_-_demo.mp3"
).is_file() ).is_file()
@ -221,6 +223,7 @@ async def test_setup_component_test_service_with_wrong_service_language(
blocking=True, blocking=True,
) )
assert len(calls) == 0 assert len(calls) == 0
await hass.async_block_till_done()
assert not ( assert not (
empty_cache_dir / "42f18378fd4393d18c8dd11d03fa9563c1e54491_lang_-_demo.mp3" empty_cache_dir / "42f18378fd4393d18c8dd11d03fa9563c1e54491_lang_-_demo.mp3"
).is_file() ).is_file()
@ -257,6 +260,7 @@ async def test_setup_component_and_test_service_with_service_options(
] == "{}/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491_de_{}_demo.mp3".format( ] == "{}/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491_de_{}_demo.mp3".format(
hass.config.api.base_url, opt_hash hass.config.api.base_url, opt_hash
) )
await hass.async_block_till_done()
assert ( assert (
empty_cache_dir empty_cache_dir
/ f"42f18378fd4393d18c8dd11d03fa9563c1e54491_de_{opt_hash}_demo.mp3" / f"42f18378fd4393d18c8dd11d03fa9563c1e54491_de_{opt_hash}_demo.mp3"
@ -294,14 +298,11 @@ async def test_setup_component_and_test_with_service_options_def(hass, empty_cac
] == "{}/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491_de_{}_demo.mp3".format( ] == "{}/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491_de_{}_demo.mp3".format(
hass.config.api.base_url, opt_hash hass.config.api.base_url, opt_hash
) )
assert os.path.isfile( await hass.async_block_till_done()
os.path.join( assert (
empty_cache_dir, empty_cache_dir
"42f18378fd4393d18c8dd11d03fa9563c1e54491_de_{0}_demo.mp3".format( / f"42f18378fd4393d18c8dd11d03fa9563c1e54491_de_{opt_hash}_demo.mp3"
opt_hash ).is_file()
),
)
)
async def test_setup_component_and_test_service_with_service_options_wrong( async def test_setup_component_and_test_service_with_service_options_wrong(
@ -329,6 +330,7 @@ async def test_setup_component_and_test_service_with_service_options_wrong(
opt_hash = ctypes.c_size_t(hash(frozenset({"speed": 1}))).value opt_hash = ctypes.c_size_t(hash(frozenset({"speed": 1}))).value
assert len(calls) == 0 assert len(calls) == 0
await hass.async_block_till_done()
assert not ( assert not (
empty_cache_dir empty_cache_dir
/ f"42f18378fd4393d18c8dd11d03fa9563c1e54491_de_{opt_hash}_demo.mp3" / f"42f18378fd4393d18c8dd11d03fa9563c1e54491_de_{opt_hash}_demo.mp3"
@ -383,6 +385,7 @@ async def test_setup_component_and_test_service_clear_cache(hass, empty_cache_di
# To make sure the file is persisted # To make sure the file is persisted
await hass.async_block_till_done() await hass.async_block_till_done()
assert len(calls) == 1 assert len(calls) == 1
await hass.async_block_till_done()
assert ( assert (
empty_cache_dir / "42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3" empty_cache_dir / "42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3"
).is_file() ).is_file()
@ -391,6 +394,7 @@ async def test_setup_component_and_test_service_clear_cache(hass, empty_cache_di
tts.DOMAIN, tts.SERVICE_CLEAR_CACHE, {}, blocking=True tts.DOMAIN, tts.SERVICE_CLEAR_CACHE, {}, blocking=True
) )
await hass.async_block_till_done()
assert not ( assert not (
empty_cache_dir / "42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3" empty_cache_dir / "42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3"
).is_file() ).is_file()
@ -520,6 +524,7 @@ async def test_setup_component_test_without_cache(hass, empty_cache_dir):
blocking=True, blocking=True,
) )
assert len(calls) == 1 assert len(calls) == 1
await hass.async_block_till_done()
assert not ( assert not (
empty_cache_dir / "42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3" empty_cache_dir / "42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3"
).is_file() ).is_file()
@ -547,6 +552,7 @@ async def test_setup_component_test_with_cache_call_service_without_cache(
blocking=True, blocking=True,
) )
assert len(calls) == 1 assert len(calls) == 1
await hass.async_block_till_done()
assert not ( assert not (
empty_cache_dir / "42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3" empty_cache_dir / "42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3"
).is_file() ).is_file()

View File

@ -9,6 +9,7 @@ from homeassistant.components.media_player import DEVICE_CLASS_SPEAKER, DEVICE_C
from homeassistant.components.vizio.config_flow import _get_config_schema from homeassistant.components.vizio.config_flow import _get_config_schema
from homeassistant.components.vizio.const import ( from homeassistant.components.vizio.const import (
CONF_APPS, CONF_APPS,
CONF_APPS_TO_INCLUDE_OR_EXCLUDE,
CONF_INCLUDE, CONF_INCLUDE,
CONF_VOLUME_STEP, CONF_VOLUME_STEP,
DEFAULT_NAME, DEFAULT_NAME,
@ -176,6 +177,39 @@ async def test_tv_options_flow_with_apps(hass: HomeAssistantType) -> None:
assert result["data"][CONF_APPS] == {CONF_INCLUDE: [CURRENT_APP]} assert result["data"][CONF_APPS] == {CONF_INCLUDE: [CURRENT_APP]}
async def test_tv_options_flow_start_with_volume(hass: HomeAssistantType) -> None:
"""Test options config flow for TV with providing apps option after providing volume step in initial config."""
entry = MockConfigEntry(
domain=DOMAIN,
data=MOCK_USER_VALID_TV_CONFIG,
options={CONF_VOLUME_STEP: VOLUME_STEP},
)
entry.add_to_hass(hass)
assert entry.options
assert entry.options == {CONF_VOLUME_STEP: VOLUME_STEP}
assert CONF_APPS not in entry.options
assert CONF_APPS_TO_INCLUDE_OR_EXCLUDE not in entry.options
result = await hass.config_entries.options.async_init(entry.entry_id, data=None)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "init"
options = {CONF_VOLUME_STEP: VOLUME_STEP}
options.update(MOCK_INCLUDE_APPS)
result = await hass.config_entries.options.async_configure(
result["flow_id"], user_input=options
)
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result["title"] == ""
assert result["data"][CONF_VOLUME_STEP] == VOLUME_STEP
assert CONF_APPS in result["data"]
assert result["data"][CONF_APPS] == {CONF_INCLUDE: [CURRENT_APP]}
async def test_user_host_already_configured( async def test_user_host_already_configured(
hass: HomeAssistantType, hass: HomeAssistantType,
vizio_connect: pytest.fixture, vizio_connect: pytest.fixture,