Merge pull request #65955 from home-assistant/rc

This commit is contained in:
Paulus Schoutsen 2022-02-06 15:30:01 -08:00 committed by GitHub
commit f170aba0cc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
51 changed files with 1122 additions and 341 deletions

View File

@ -17,7 +17,12 @@ from homeassistant.components.weather import (
WeatherEntity, WeatherEntity,
) )
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_NAME, TEMP_CELSIUS, TEMP_FAHRENHEIT from homeassistant.const import (
CONF_NAME,
SPEED_MILES_PER_HOUR,
TEMP_CELSIUS,
TEMP_FAHRENHEIT,
)
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.device_registry import DeviceEntryType
from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity import DeviceInfo
@ -62,9 +67,13 @@ class AccuWeatherEntity(CoordinatorEntity, WeatherEntity):
"""Initialize.""" """Initialize."""
super().__init__(coordinator) super().__init__(coordinator)
self._unit_system = API_METRIC if coordinator.is_metric else API_IMPERIAL self._unit_system = API_METRIC if coordinator.is_metric else API_IMPERIAL
self._attr_wind_speed_unit = self.coordinator.data["Wind"]["Speed"][ wind_speed_unit = self.coordinator.data["Wind"]["Speed"][self._unit_system][
self._unit_system "Unit"
]["Unit"] ]
if wind_speed_unit == "mi/h":
self._attr_wind_speed_unit = SPEED_MILES_PER_HOUR
else:
self._attr_wind_speed_unit = wind_speed_unit
self._attr_name = name self._attr_name = name
self._attr_unique_id = coordinator.location_key self._attr_unique_id = coordinator.location_key
self._attr_temperature_unit = ( self._attr_temperature_unit = (

View File

@ -515,8 +515,8 @@ class AmcrestCam(Camera):
max_tries = 3 max_tries = 3
for tries in range(max_tries, 0, -1): for tries in range(max_tries, 0, -1):
try: try:
await getattr(self, f"_set_{func}")(value) await getattr(self, f"_async_set_{func}")(value)
new_value = await getattr(self, f"_get_{func}")() new_value = await getattr(self, f"_async_get_{func}")()
if new_value != value: if new_value != value:
raise AmcrestCommandFailed raise AmcrestCommandFailed
except (AmcrestError, AmcrestCommandFailed) as error: except (AmcrestError, AmcrestCommandFailed) as error:

View File

@ -18,6 +18,7 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import entity_registry as er from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.device_registry import format_mac
from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.storage import STORAGE_DIR from homeassistant.helpers.storage import STORAGE_DIR
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
@ -33,16 +34,30 @@ from .const import (
DEVICE_ANDROIDTV, DEVICE_ANDROIDTV,
DEVICE_FIRETV, DEVICE_FIRETV,
DOMAIN, DOMAIN,
PROP_ETHMAC,
PROP_SERIALNO, PROP_SERIALNO,
PROP_WIFIMAC,
SIGNAL_CONFIG_ENTITY, SIGNAL_CONFIG_ENTITY,
) )
PLATFORMS = [Platform.MEDIA_PLAYER] PLATFORMS = [Platform.MEDIA_PLAYER]
RELOAD_OPTIONS = [CONF_STATE_DETECTION_RULES] RELOAD_OPTIONS = [CONF_STATE_DETECTION_RULES]
_INVALID_MACS = {"ff:ff:ff:ff:ff:ff"}
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
def get_androidtv_mac(dev_props):
"""Return formatted mac from device properties."""
for prop_mac in (PROP_ETHMAC, PROP_WIFIMAC):
if if_mac := dev_props.get(prop_mac):
mac = format_mac(if_mac)
if mac not in _INVALID_MACS:
return mac
return None
def _setup_androidtv(hass, config): def _setup_androidtv(hass, config):
"""Generate an ADB key (if needed) and load it.""" """Generate an ADB key (if needed) and load it."""
adbkey = config.get(CONF_ADBKEY, hass.config.path(STORAGE_DIR, "androidtv_adbkey")) adbkey = config.get(CONF_ADBKEY, hass.config.path(STORAGE_DIR, "androidtv_adbkey"))

View File

@ -11,9 +11,8 @@ from homeassistant import config_entries
from homeassistant.const import CONF_DEVICE_CLASS, CONF_HOST, CONF_NAME, CONF_PORT from homeassistant.const import CONF_DEVICE_CLASS, CONF_HOST, CONF_NAME, CONF_PORT
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.device_registry import format_mac
from . import async_connect_androidtv from . import async_connect_androidtv, get_androidtv_mac
from .const import ( from .const import (
CONF_ADB_SERVER_IP, CONF_ADB_SERVER_IP,
CONF_ADB_SERVER_PORT, CONF_ADB_SERVER_PORT,
@ -132,9 +131,7 @@ class AndroidTVFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
PROP_WIFIMAC, PROP_WIFIMAC,
dev_prop.get(PROP_WIFIMAC), dev_prop.get(PROP_WIFIMAC),
) )
unique_id = format_mac( unique_id = get_androidtv_mac(dev_prop)
dev_prop.get(PROP_ETHMAC) or dev_prop.get(PROP_WIFIMAC, "")
)
await aftv.adb_close() await aftv.adb_close()
return None, unique_id return None, unique_id

View File

@ -51,12 +51,13 @@ from homeassistant.const import (
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers import config_validation as cv, entity_platform
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, format_mac from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from . import get_androidtv_mac
from .const import ( from .const import (
ANDROID_DEV, ANDROID_DEV,
ANDROID_DEV_OPT, ANDROID_DEV_OPT,
@ -80,8 +81,6 @@ from .const import (
DEVICE_ANDROIDTV, DEVICE_ANDROIDTV,
DEVICE_CLASSES, DEVICE_CLASSES,
DOMAIN, DOMAIN,
PROP_ETHMAC,
PROP_WIFIMAC,
SIGNAL_CONFIG_ENTITY, SIGNAL_CONFIG_ENTITY,
) )
@ -343,7 +342,7 @@ class ADBDevice(MediaPlayerEntity):
self._attr_device_info[ATTR_MANUFACTURER] = manufacturer self._attr_device_info[ATTR_MANUFACTURER] = manufacturer
if sw_version := info.get(ATTR_SW_VERSION): if sw_version := info.get(ATTR_SW_VERSION):
self._attr_device_info[ATTR_SW_VERSION] = sw_version self._attr_device_info[ATTR_SW_VERSION] = sw_version
if mac := format_mac(info.get(PROP_ETHMAC) or info.get(PROP_WIFIMAC, "")): if mac := get_androidtv_mac(info):
self._attr_device_info[ATTR_CONNECTIONS] = {(CONNECTION_NETWORK_MAC, mac)} self._attr_device_info[ATTR_CONNECTIONS] = {(CONNECTION_NETWORK_MAC, mac)}
self._app_id_to_name = {} self._app_id_to_name = {}

View File

@ -62,7 +62,7 @@ async def websocket_update_device(hass, connection, msg):
msg.pop("type") msg.pop("type")
msg_id = msg.pop("id") msg_id = msg.pop("id")
if "disabled_by" in msg: if msg.get("disabled_by") is not None:
msg["disabled_by"] = DeviceEntryDisabler(msg["disabled_by"]) msg["disabled_by"] = DeviceEntryDisabler(msg["disabled_by"])
entry = registry.async_update_device(**msg) entry = registry.async_update_device(**msg)

View File

@ -179,6 +179,7 @@ class WatcherBase:
lowercase_hostname, lowercase_hostname,
) )
matched_domains = set()
for entry in self._integration_matchers: for entry in self._integration_matchers:
if MAC_ADDRESS in entry and not fnmatch.fnmatch( if MAC_ADDRESS in entry and not fnmatch.fnmatch(
uppercase_mac, entry[MAC_ADDRESS] uppercase_mac, entry[MAC_ADDRESS]
@ -191,6 +192,11 @@ class WatcherBase:
continue continue
_LOGGER.debug("Matched %s against %s", data, entry) _LOGGER.debug("Matched %s against %s", data, entry)
if entry["domain"] in matched_domains:
# Only match once per domain
continue
matched_domains.add(entry["domain"])
discovery_flow.async_create_flow( discovery_flow.async_create_flow(
self.hass, self.hass,
entry["domain"], entry["domain"],

View File

@ -2,7 +2,7 @@
"domain": "doods", "domain": "doods",
"name": "DOODS - Dedicated Open Object Detection Service", "name": "DOODS - Dedicated Open Object Detection Service",
"documentation": "https://www.home-assistant.io/integrations/doods", "documentation": "https://www.home-assistant.io/integrations/doods",
"requirements": ["pydoods==1.0.2", "pillow==9.0.0"], "requirements": ["pydoods==1.0.2", "pillow==9.0.1"],
"codeowners": [], "codeowners": [],
"iot_class": "local_polling" "iot_class": "local_polling"
} }

View File

@ -4,7 +4,7 @@
"config_flow": true, "config_flow": true,
"dependencies": ["network"], "dependencies": ["network"],
"documentation": "https://www.home-assistant.io/integrations/flux_led", "documentation": "https://www.home-assistant.io/integrations/flux_led",
"requirements": ["flux_led==0.28.20"], "requirements": ["flux_led==0.28.21"],
"quality_scale": "platinum", "quality_scale": "platinum",
"codeowners": ["@icemanch", "@bdraco"], "codeowners": ["@icemanch", "@bdraco"],
"iot_class": "local_push", "iot_class": "local_push",

View File

@ -567,13 +567,6 @@ class AvmWrapper(FritzBoxTools):
) )
return {} return {}
async def async_get_wan_dsl_interface_config(self) -> dict[str, Any]:
"""Call WANDSLInterfaceConfig service."""
return await self.hass.async_add_executor_job(
partial(self.get_wan_dsl_interface_config)
)
async def async_get_wan_link_properties(self) -> dict[str, Any]: async def async_get_wan_link_properties(self) -> dict[str, Any]:
"""Call WANCommonInterfaceConfig service.""" """Call WANCommonInterfaceConfig service."""
@ -678,11 +671,6 @@ class AvmWrapper(FritzBoxTools):
return self._service_call_action("WLANConfiguration", str(index), "GetInfo") return self._service_call_action("WLANConfiguration", str(index), "GetInfo")
def get_wan_dsl_interface_config(self) -> dict[str, Any]:
"""Call WANDSLInterfaceConfig service."""
return self._service_call_action("WANDSLInterfaceConfig", "1", "GetInfo")
def get_wan_link_properties(self) -> dict[str, Any]: def get_wan_link_properties(self) -> dict[str, Any]:
"""Call WANCommonInterfaceConfig service.""" """Call WANCommonInterfaceConfig service."""

View File

@ -277,10 +277,14 @@ async def async_setup_entry(
_LOGGER.debug("Setting up FRITZ!Box sensors") _LOGGER.debug("Setting up FRITZ!Box sensors")
avm_wrapper: AvmWrapper = hass.data[DOMAIN][entry.entry_id] avm_wrapper: AvmWrapper = hass.data[DOMAIN][entry.entry_id]
dsl: bool = False link_properties = await avm_wrapper.async_get_wan_link_properties()
dslinterface = await avm_wrapper.async_get_wan_dsl_interface_config() dsl: bool = link_properties.get("NewWANAccessType") == "DSL"
if dslinterface:
dsl = dslinterface["NewEnable"] _LOGGER.debug(
"WANAccessType of FritzBox %s is '%s'",
avm_wrapper.host,
link_properties.get("NewWANAccessType"),
)
entities = [ entities = [
FritzBoxSensor(avm_wrapper, entry.title, description) FritzBoxSensor(avm_wrapper, entry.title, description)

View File

@ -0,0 +1,44 @@
"""Diagnostics support for HomeKit."""
from __future__ import annotations
from typing import Any
from pyhap.accessory_driver import AccessoryDriver
from pyhap.state import State
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from . import HomeKit
from .const import DOMAIN, HOMEKIT
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, entry: ConfigEntry
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
homekit: HomeKit = hass.data[DOMAIN][entry.entry_id][HOMEKIT]
driver: AccessoryDriver = homekit.driver
data: dict[str, Any] = {
"status": homekit.status,
"config-entry": {
"title": entry.title,
"version": entry.version,
"data": dict(entry.data),
"options": dict(entry.options),
},
}
if not driver:
return data
data.update(driver.get_accessories())
state: State = driver.state
data.update(
{
"client_properties": {
str(client): props for client, props in state.client_properties.items()
},
"config_version": state.config_version,
"pairing_id": state.mac,
}
)
return data

View File

@ -1,4 +1,6 @@
"""Class to hold all light accessories.""" """Class to hold all light accessories."""
from __future__ import annotations
import logging import logging
import math import math
@ -12,12 +14,13 @@ from homeassistant.components.light import (
ATTR_HS_COLOR, ATTR_HS_COLOR,
ATTR_MAX_MIREDS, ATTR_MAX_MIREDS,
ATTR_MIN_MIREDS, ATTR_MIN_MIREDS,
ATTR_RGB_COLOR,
ATTR_RGBW_COLOR, ATTR_RGBW_COLOR,
ATTR_RGBWW_COLOR, ATTR_RGBWW_COLOR,
ATTR_SUPPORTED_COLOR_MODES, ATTR_SUPPORTED_COLOR_MODES,
ATTR_WHITE,
COLOR_MODE_RGBW, COLOR_MODE_RGBW,
COLOR_MODE_RGBWW, COLOR_MODE_RGBWW,
COLOR_MODE_WHITE,
DOMAIN, DOMAIN,
brightness_supported, brightness_supported,
color_supported, color_supported,
@ -32,9 +35,9 @@ from homeassistant.const import (
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.helpers.event import async_call_later from homeassistant.helpers.event import async_call_later
from homeassistant.util.color import ( from homeassistant.util.color import (
color_hsv_to_RGB,
color_temperature_mired_to_kelvin, color_temperature_mired_to_kelvin,
color_temperature_to_hs, color_temperature_to_hs,
color_temperature_to_rgbww,
) )
from .accessories import TYPES, HomeAccessory from .accessories import TYPES, HomeAccessory
@ -51,12 +54,13 @@ from .const import (
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
RGB_COLOR = "rgb_color"
CHANGE_COALESCE_TIME_WINDOW = 0.01 CHANGE_COALESCE_TIME_WINDOW = 0.01
DEFAULT_MIN_MIREDS = 153
DEFAULT_MAX_MIREDS = 500
COLOR_MODES_WITH_WHITES = {COLOR_MODE_RGBW, COLOR_MODE_RGBWW} COLOR_MODES_WITH_WHITES = {COLOR_MODE_RGBW, COLOR_MODE_RGBWW, COLOR_MODE_WHITE}
@TYPES.register("Light") @TYPES.register("Light")
@ -79,8 +83,12 @@ class Light(HomeAccessory):
self.color_modes = color_modes = ( self.color_modes = color_modes = (
attributes.get(ATTR_SUPPORTED_COLOR_MODES) or [] attributes.get(ATTR_SUPPORTED_COLOR_MODES) or []
) )
self._previous_color_mode = attributes.get(ATTR_COLOR_MODE)
self.color_supported = color_supported(color_modes) self.color_supported = color_supported(color_modes)
self.color_temp_supported = color_temp_supported(color_modes) self.color_temp_supported = color_temp_supported(color_modes)
self.rgbw_supported = COLOR_MODE_RGBW in color_modes
self.rgbww_supported = COLOR_MODE_RGBWW in color_modes
self.white_supported = COLOR_MODE_WHITE in color_modes
self.brightness_supported = brightness_supported(color_modes) self.brightness_supported = brightness_supported(color_modes)
if self.brightness_supported: if self.brightness_supported:
@ -89,7 +97,9 @@ class Light(HomeAccessory):
if self.color_supported: if self.color_supported:
self.chars.extend([CHAR_HUE, CHAR_SATURATION]) self.chars.extend([CHAR_HUE, CHAR_SATURATION])
if self.color_temp_supported: if self.color_temp_supported or COLOR_MODES_WITH_WHITES.intersection(
self.color_modes
):
self.chars.append(CHAR_COLOR_TEMPERATURE) self.chars.append(CHAR_COLOR_TEMPERATURE)
serv_light = self.add_preload_service(SERV_LIGHTBULB, self.chars) serv_light = self.add_preload_service(SERV_LIGHTBULB, self.chars)
@ -101,13 +111,22 @@ class Light(HomeAccessory):
# to set to the correct initial value. # to set to the correct initial value.
self.char_brightness = serv_light.configure_char(CHAR_BRIGHTNESS, value=100) self.char_brightness = serv_light.configure_char(CHAR_BRIGHTNESS, value=100)
if self.color_temp_supported: if CHAR_COLOR_TEMPERATURE in self.chars:
min_mireds = math.floor(attributes.get(ATTR_MIN_MIREDS, 153)) self.min_mireds = math.floor(
max_mireds = math.ceil(attributes.get(ATTR_MAX_MIREDS, 500)) attributes.get(ATTR_MIN_MIREDS, DEFAULT_MIN_MIREDS)
)
self.max_mireds = math.ceil(
attributes.get(ATTR_MAX_MIREDS, DEFAULT_MAX_MIREDS)
)
if not self.color_temp_supported and not self.rgbww_supported:
self.max_mireds = self.min_mireds
self.char_color_temp = serv_light.configure_char( self.char_color_temp = serv_light.configure_char(
CHAR_COLOR_TEMPERATURE, CHAR_COLOR_TEMPERATURE,
value=min_mireds, value=self.min_mireds,
properties={PROP_MIN_VALUE: min_mireds, PROP_MAX_VALUE: max_mireds}, properties={
PROP_MIN_VALUE: self.min_mireds,
PROP_MAX_VALUE: self.max_mireds,
},
) )
if self.color_supported: if self.color_supported:
@ -165,32 +184,31 @@ class Light(HomeAccessory):
) )
return return
# Handle white channels
if CHAR_COLOR_TEMPERATURE in char_values: if CHAR_COLOR_TEMPERATURE in char_values:
params[ATTR_COLOR_TEMP] = char_values[CHAR_COLOR_TEMPERATURE] temp = char_values[CHAR_COLOR_TEMPERATURE]
events.append(f"color temperature at {params[ATTR_COLOR_TEMP]}") events.append(f"color temperature at {temp}")
bright_val = round(
((brightness_pct or self.char_brightness.value) * 255) / 100
)
if self.color_temp_supported:
params[ATTR_COLOR_TEMP] = temp
elif self.rgbww_supported:
params[ATTR_RGBWW_COLOR] = color_temperature_to_rgbww(
temp, bright_val, self.min_mireds, self.max_mireds
)
elif self.rgbw_supported:
params[ATTR_RGBW_COLOR] = (*(0,) * 3, bright_val)
elif self.white_supported:
params[ATTR_WHITE] = bright_val
elif ( elif CHAR_HUE in char_values or CHAR_SATURATION in char_values:
CHAR_HUE in char_values
or CHAR_SATURATION in char_values
# If we are adjusting brightness we need to send the full RGBW/RGBWW values
# since HomeKit does not support RGBW/RGBWW
or brightness_pct
and COLOR_MODES_WITH_WHITES.intersection(self.color_modes)
):
hue_sat = ( hue_sat = (
char_values.get(CHAR_HUE, self.char_hue.value), char_values.get(CHAR_HUE, self.char_hue.value),
char_values.get(CHAR_SATURATION, self.char_saturation.value), char_values.get(CHAR_SATURATION, self.char_saturation.value),
) )
_LOGGER.debug("%s: Set hs_color to %s", self.entity_id, hue_sat) _LOGGER.debug("%s: Set hs_color to %s", self.entity_id, hue_sat)
events.append(f"set color at {hue_sat}") events.append(f"set color at {hue_sat}")
# HomeKit doesn't support RGBW/RGBWW so we need to remove any white values
if COLOR_MODE_RGBWW in self.color_modes:
val = brightness_pct or self.char_brightness.value
params[ATTR_RGBWW_COLOR] = (*color_hsv_to_RGB(*hue_sat, val), 0, 0)
elif COLOR_MODE_RGBW in self.color_modes:
val = brightness_pct or self.char_brightness.value
params[ATTR_RGBW_COLOR] = (*color_hsv_to_RGB(*hue_sat, val), 0)
else:
params[ATTR_HS_COLOR] = hue_sat params[ATTR_HS_COLOR] = hue_sat
if ( if (
@ -200,6 +218,9 @@ class Light(HomeAccessory):
): ):
params[ATTR_BRIGHTNESS_PCT] = brightness_pct params[ATTR_BRIGHTNESS_PCT] = brightness_pct
_LOGGER.debug(
"Calling light service with params: %s -> %s", char_values, params
)
self.async_call_service(DOMAIN, service, params, ", ".join(events)) self.async_call_service(DOMAIN, service, params, ", ".join(events))
@callback @callback
@ -210,20 +231,15 @@ class Light(HomeAccessory):
attributes = new_state.attributes attributes = new_state.attributes
color_mode = attributes.get(ATTR_COLOR_MODE) color_mode = attributes.get(ATTR_COLOR_MODE)
self.char_on.set_value(int(state == STATE_ON)) self.char_on.set_value(int(state == STATE_ON))
color_mode_changed = self._previous_color_mode != color_mode
self._previous_color_mode = color_mode
# Handle Brightness # Handle Brightness
if self.brightness_supported:
if ( if (
color_mode self.brightness_supported
and COLOR_MODES_WITH_WHITES.intersection({color_mode}) and (brightness := attributes.get(ATTR_BRIGHTNESS)) is not None
and (rgb_color := attributes.get(ATTR_RGB_COLOR)) and isinstance(brightness, (int, float))
): ):
# HomeKit doesn't support RGBW/RGBWW so we need to
# give it the color brightness only
brightness = max(rgb_color)
else:
brightness = attributes.get(ATTR_BRIGHTNESS)
if isinstance(brightness, (int, float)):
brightness = round(brightness / 255 * 100, 0) brightness = round(brightness / 255 * 100, 0)
# The homeassistant component might report its brightness as 0 but is # The homeassistant component might report its brightness as 0 but is
# not off. But 0 is a special value in homekit. When you turn on a # not off. But 0 is a special value in homekit. When you turn on a
@ -238,24 +254,36 @@ class Light(HomeAccessory):
if brightness == 0 and state == STATE_ON: if brightness == 0 and state == STATE_ON:
brightness = 1 brightness = 1
self.char_brightness.set_value(brightness) self.char_brightness.set_value(brightness)
if color_mode_changed:
self.char_brightness.notify()
# Handle Color - color must always be set before color temperature # Handle Color - color must always be set before color temperature
# or the iOS UI will not display it correctly. # or the iOS UI will not display it correctly.
if self.color_supported: if self.color_supported:
if ATTR_COLOR_TEMP in attributes: if color_temp := attributes.get(ATTR_COLOR_TEMP):
hue, saturation = color_temperature_to_hs( hue, saturation = color_temperature_to_hs(
color_temperature_mired_to_kelvin( color_temperature_mired_to_kelvin(color_temp)
new_state.attributes[ATTR_COLOR_TEMP]
)
) )
elif color_mode == COLOR_MODE_WHITE:
hue, saturation = 0, 0
else: else:
hue, saturation = attributes.get(ATTR_HS_COLOR, (None, None)) hue, saturation = attributes.get(ATTR_HS_COLOR, (None, None))
if isinstance(hue, (int, float)) and isinstance(saturation, (int, float)): if isinstance(hue, (int, float)) and isinstance(saturation, (int, float)):
self.char_hue.set_value(round(hue, 0)) self.char_hue.set_value(round(hue, 0))
self.char_saturation.set_value(round(saturation, 0)) self.char_saturation.set_value(round(saturation, 0))
if color_mode_changed:
# If the color temp changed, be sure to force the color to update
self.char_hue.notify()
self.char_saturation.notify()
# Handle color temperature # Handle white channels
if CHAR_COLOR_TEMPERATURE in self.chars:
color_temp = None
if self.color_temp_supported: if self.color_temp_supported:
color_temp = attributes.get(ATTR_COLOR_TEMP) color_temp = attributes.get(ATTR_COLOR_TEMP)
elif color_mode == COLOR_MODE_WHITE:
color_temp = self.min_mireds
if isinstance(color_temp, (int, float)): if isinstance(color_temp, (int, float)):
self.char_color_temp.set_value(round(color_temp, 0)) self.char_color_temp.set_value(round(color_temp, 0))
if color_mode_changed:
self.char_color_temp.notify()

View File

@ -3,7 +3,7 @@
"name": "Image", "name": "Image",
"config_flow": false, "config_flow": false,
"documentation": "https://www.home-assistant.io/integrations/image", "documentation": "https://www.home-assistant.io/integrations/image",
"requirements": ["pillow==9.0.0"], "requirements": ["pillow==9.0.1"],
"dependencies": ["http"], "dependencies": ["http"],
"codeowners": ["@home-assistant/core"], "codeowners": ["@home-assistant/core"],
"quality_scale": "internal" "quality_scale": "internal"

View File

@ -3,7 +3,7 @@
"name": "IntelliFire", "name": "IntelliFire",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/intellifire", "documentation": "https://www.home-assistant.io/integrations/intellifire",
"requirements": ["intellifire4py==0.5"], "requirements": ["intellifire4py==0.6"],
"dependencies": [], "dependencies": [],
"codeowners": ["@jeeftor"], "codeowners": ["@jeeftor"],
"iot_class": "local_polling" "iot_class": "local_polling"

View File

@ -4,7 +4,7 @@
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/knx", "documentation": "https://www.home-assistant.io/integrations/knx",
"requirements": [ "requirements": [
"xknx==0.19.1" "xknx==0.19.2"
], ],
"codeowners": [ "codeowners": [
"@Julius2342", "@Julius2342",

View File

@ -12,7 +12,7 @@ from google_nest_sdm.exceptions import ApiException
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from .const import DATA_SUBSCRIBER, DOMAIN from .const import DATA_SDM, DATA_SUBSCRIBER, DOMAIN
REDACT_DEVICE_TRAITS = {InfoTrait.NAME} REDACT_DEVICE_TRAITS = {InfoTrait.NAME}
@ -21,6 +21,9 @@ async def async_get_config_entry_diagnostics(
hass: HomeAssistant, config_entry: ConfigEntry hass: HomeAssistant, config_entry: ConfigEntry
) -> dict: ) -> dict:
"""Return diagnostics for a config entry.""" """Return diagnostics for a config entry."""
if DATA_SDM not in config_entry.data:
return {}
if DATA_SUBSCRIBER not in hass.data[DOMAIN]: if DATA_SUBSCRIBER not in hass.data[DOMAIN]:
return {"error": "No subscriber configured"} return {"error": "No subscriber configured"}

View File

@ -121,14 +121,22 @@ async def async_setup_entry(
if coordinator.data: if coordinator.data:
if coordinator.data.electricity: if coordinator.data.electricity:
for description in SENSOR_TYPES_ELECTRICITY: for description in SENSOR_TYPES_ELECTRICITY:
if description.key == KEY_LAST_ELECTRICITY_COST: if (
description.key == KEY_LAST_ELECTRICITY_COST
and coordinator.data.electricity[-1] is not None
and coordinator.data.electricity[-1].cost is not None
):
description.native_unit_of_measurement = ( description.native_unit_of_measurement = (
coordinator.data.electricity[-1].cost.currency_unit coordinator.data.electricity[-1].cost.currency_unit
) )
entities.append(OVOEnergySensor(coordinator, description, client)) entities.append(OVOEnergySensor(coordinator, description, client))
if coordinator.data.gas: if coordinator.data.gas:
for description in SENSOR_TYPES_GAS: for description in SENSOR_TYPES_GAS:
if description.key == KEY_LAST_GAS_COST: if (
description.key == KEY_LAST_GAS_COST
and coordinator.data.gas[-1] is not None
and coordinator.data.gas[-1].cost is not None
):
description.native_unit_of_measurement = coordinator.data.gas[ description.native_unit_of_measurement = coordinator.data.gas[
-1 -1
].cost.currency_unit ].cost.currency_unit

View File

@ -2,6 +2,6 @@
"domain": "proxy", "domain": "proxy",
"name": "Camera Proxy", "name": "Camera Proxy",
"documentation": "https://www.home-assistant.io/integrations/proxy", "documentation": "https://www.home-assistant.io/integrations/proxy",
"requirements": ["pillow==9.0.0"], "requirements": ["pillow==9.0.1"],
"codeowners": [] "codeowners": []
} }

View File

@ -2,7 +2,7 @@
"domain": "qrcode", "domain": "qrcode",
"name": "QR Code", "name": "QR Code",
"documentation": "https://www.home-assistant.io/integrations/qrcode", "documentation": "https://www.home-assistant.io/integrations/qrcode",
"requirements": ["pillow==9.0.0", "pyzbar==0.1.7"], "requirements": ["pillow==9.0.1", "pyzbar==0.1.7"],
"codeowners": [], "codeowners": [],
"iot_class": "calculated" "iot_class": "calculated"
} }

View File

@ -20,7 +20,6 @@ from homeassistant.const import (
CONF_TYPE, CONF_TYPE,
CONF_USERNAME, CONF_USERNAME,
ENERGY_KILO_WATT_HOUR, ENERGY_KILO_WATT_HOUR,
EVENT_HOMEASSISTANT_START,
EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_STOP,
MASS_KILOGRAMS, MASS_KILOGRAMS,
POWER_WATT, POWER_WATT,
@ -33,6 +32,7 @@ from homeassistant.exceptions import PlatformNotReady
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.event import async_call_later from homeassistant.helpers.event import async_call_later
from homeassistant.helpers.start import async_at_start
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -131,17 +131,19 @@ async def async_setup_platform(
return values return values
@callback
def start_update_interval(event): def start_update_interval(event):
"""Start the update interval scheduling.""" """Start the update interval scheduling."""
nonlocal remove_interval_update nonlocal remove_interval_update
remove_interval_update = async_track_time_interval_backoff(hass, async_saj) remove_interval_update = async_track_time_interval_backoff(hass, async_saj)
@callback
def stop_update_interval(event): def stop_update_interval(event):
"""Properly cancel the scheduled update.""" """Properly cancel the scheduled update."""
remove_interval_update() # pylint: disable=not-callable remove_interval_update() # pylint: disable=not-callable
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, start_update_interval)
hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, stop_update_interval) hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, stop_update_interval)
async_at_start(hass, start_update_interval)
@callback @callback

View File

@ -2,7 +2,7 @@
"domain": "seven_segments", "domain": "seven_segments",
"name": "Seven Segments OCR", "name": "Seven Segments OCR",
"documentation": "https://www.home-assistant.io/integrations/seven_segments", "documentation": "https://www.home-assistant.io/integrations/seven_segments",
"requirements": ["pillow==9.0.0"], "requirements": ["pillow==9.0.1"],
"codeowners": ["@fabaff"], "codeowners": ["@fabaff"],
"iot_class": "local_polling" "iot_class": "local_polling"
} }

View File

@ -3,7 +3,7 @@
"name": "Shelly", "name": "Shelly",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/shelly", "documentation": "https://www.home-assistant.io/integrations/shelly",
"requirements": ["aioshelly==1.0.8"], "requirements": ["aioshelly==1.0.9"],
"zeroconf": [ "zeroconf": [
{ {
"type": "_http._tcp.local.", "type": "_http._tcp.local.",

View File

@ -2,7 +2,7 @@
"domain": "sighthound", "domain": "sighthound",
"name": "Sighthound", "name": "Sighthound",
"documentation": "https://www.home-assistant.io/integrations/sighthound", "documentation": "https://www.home-assistant.io/integrations/sighthound",
"requirements": ["pillow==9.0.0", "simplehound==0.3"], "requirements": ["pillow==9.0.1", "simplehound==0.3"],
"codeowners": ["@robmarkcole"], "codeowners": ["@robmarkcole"],
"iot_class": "cloud_polling" "iot_class": "cloud_polling"
} }

View File

@ -482,6 +482,7 @@ class SimpliSafe:
self._websocket_reconnect_task: asyncio.Task | None = None self._websocket_reconnect_task: asyncio.Task | None = None
self.entry = entry self.entry = entry
self.initial_event_to_use: dict[int, dict[str, Any]] = {} self.initial_event_to_use: dict[int, dict[str, Any]] = {}
self.subscription_data: dict[int, Any] = api.subscription_data
self.systems: dict[int, SystemType] = {} self.systems: dict[int, SystemType] = {}
# This will get filled in by async_init: # This will get filled in by async_init:

View File

@ -11,14 +11,28 @@ from homeassistant.core import HomeAssistant
from . import SimpliSafe from . import SimpliSafe
from .const import DOMAIN from .const import DOMAIN
CONF_CREDIT_CARD = "creditCard"
CONF_EXPIRES = "expires"
CONF_LOCATION = "location"
CONF_LOCATION_NAME = "locationName"
CONF_PAYMENT_PROFILE_ID = "paymentProfileId"
CONF_SERIAL = "serial" CONF_SERIAL = "serial"
CONF_SID = "sid"
CONF_SYSTEM_ID = "system_id" CONF_SYSTEM_ID = "system_id"
CONF_UID = "uid"
CONF_WIFI_SSID = "wifi_ssid" CONF_WIFI_SSID = "wifi_ssid"
TO_REDACT = { TO_REDACT = {
CONF_ADDRESS, CONF_ADDRESS,
CONF_CREDIT_CARD,
CONF_EXPIRES,
CONF_LOCATION,
CONF_LOCATION_NAME,
CONF_PAYMENT_PROFILE_ID,
CONF_SERIAL, CONF_SERIAL,
CONF_SID,
CONF_SYSTEM_ID, CONF_SYSTEM_ID,
CONF_UID,
CONF_WIFI_SSID, CONF_WIFI_SSID,
} }
@ -34,6 +48,7 @@ async def async_get_config_entry_diagnostics(
"entry": { "entry": {
"options": dict(entry.options), "options": dict(entry.options),
}, },
"subscription_data": simplisafe.subscription_data,
"systems": [system.as_dict() for system in simplisafe.systems.values()], "systems": [system.as_dict() for system in simplisafe.systems.values()],
}, },
TO_REDACT, TO_REDACT,

View File

@ -3,7 +3,7 @@
"name": "SimpliSafe", "name": "SimpliSafe",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/simplisafe", "documentation": "https://www.home-assistant.io/integrations/simplisafe",
"requirements": ["simplisafe-python==2022.01.0"], "requirements": ["simplisafe-python==2022.02.0"],
"codeowners": ["@bachya"], "codeowners": ["@bachya"],
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"dhcp": [ "dhcp": [

View File

@ -3,7 +3,7 @@
"name": "Sonos", "name": "Sonos",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/sonos", "documentation": "https://www.home-assistant.io/integrations/sonos",
"requirements": ["soco==0.26.0"], "requirements": ["soco==0.26.2"],
"dependencies": ["ssdp"], "dependencies": ["ssdp"],
"after_dependencies": ["plex", "spotify", "zeroconf", "media_source"], "after_dependencies": ["plex", "spotify", "zeroconf", "media_source"],
"zeroconf": ["_sonos._tcp.local."], "zeroconf": ["_sonos._tcp.local."],

View File

@ -558,7 +558,7 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity):
plex_plugin.play_now(media) plex_plugin.play_now(media)
return return
share_link = self.speaker.share_link share_link = self.coordinator.share_link
if share_link.is_share_link(media_id): if share_link.is_share_link(media_id):
if kwargs.get(ATTR_MEDIA_ENQUEUE): if kwargs.get(ATTR_MEDIA_ENQUEUE):
share_link.add_share_link_to_queue(media_id) share_link.add_share_link_to_queue(media_id)

View File

@ -16,6 +16,7 @@ from synology_dsm.api.storage.storage import SynoStorage
from synology_dsm.api.surveillance_station import SynoSurveillanceStation from synology_dsm.api.surveillance_station import SynoSurveillanceStation
from synology_dsm.exceptions import ( from synology_dsm.exceptions import (
SynologyDSMAPIErrorException, SynologyDSMAPIErrorException,
SynologyDSMException,
SynologyDSMLoginFailedException, SynologyDSMLoginFailedException,
SynologyDSMRequestException, SynologyDSMRequestException,
) )
@ -237,7 +238,11 @@ class SynoApi:
async def async_unload(self) -> None: async def async_unload(self) -> None:
"""Stop interacting with the NAS and prepare for removal from hass.""" """Stop interacting with the NAS and prepare for removal from hass."""
try:
await self._syno_api_executer(self.dsm.logout) await self._syno_api_executer(self.dsm.logout)
except SynologyDSMException:
# ignore API errors during logout
pass
async def async_update(self, now: timedelta | None = None) -> None: async def async_update(self, now: timedelta | None = None) -> None:
"""Update function for updating API information.""" """Update function for updating API information."""

View File

@ -267,7 +267,7 @@ class SynologyDSMFlowHandler(ConfigFlow, domain=DOMAIN):
and existing_entry.data[CONF_HOST] != parsed_url.hostname and existing_entry.data[CONF_HOST] != parsed_url.hostname
and not fqdn_with_ssl_verification and not fqdn_with_ssl_verification
): ):
_LOGGER.debug( _LOGGER.info(
"Update host from '%s' to '%s' for NAS '%s' via SSDP discovery", "Update host from '%s' to '%s' for NAS '%s' via SSDP discovery",
existing_entry.data[CONF_HOST], existing_entry.data[CONF_HOST],
parsed_url.hostname, parsed_url.hostname,

View File

@ -7,7 +7,7 @@
"tf-models-official==2.5.0", "tf-models-official==2.5.0",
"pycocotools==2.0.1", "pycocotools==2.0.1",
"numpy==1.21.4", "numpy==1.21.4",
"pillow==9.0.0" "pillow==9.0.1"
], ],
"codeowners": [], "codeowners": [],
"iot_class": "local_polling" "iot_class": "local_polling"

View File

@ -7,7 +7,7 @@ from .backports.enum import StrEnum
MAJOR_VERSION: Final = 2022 MAJOR_VERSION: Final = 2022
MINOR_VERSION: Final = 2 MINOR_VERSION: Final = 2
PATCH_VERSION: Final = "2" PATCH_VERSION: Final = "3"
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__: Final = f"{__short_version__}.{PATCH_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0)

View File

@ -20,7 +20,7 @@ httpx==0.21.3
ifaddr==0.1.7 ifaddr==0.1.7
jinja2==3.0.3 jinja2==3.0.3
paho-mqtt==1.6.1 paho-mqtt==1.6.1
pillow==9.0.0 pillow==9.0.1
pip>=8.0.3,<20.3 pip>=8.0.3,<20.3
pyserial==3.5 pyserial==3.5
python-slugify==4.0.1 python-slugify==4.0.1

View File

@ -472,7 +472,10 @@ def color_rgbww_to_rgb(
except ZeroDivisionError: except ZeroDivisionError:
ct_ratio = 0.5 ct_ratio = 0.5
color_temp_mired = min_mireds + ct_ratio * mired_range color_temp_mired = min_mireds + ct_ratio * mired_range
if color_temp_mired:
color_temp_kelvin = color_temperature_mired_to_kelvin(color_temp_mired) color_temp_kelvin = color_temperature_mired_to_kelvin(color_temp_mired)
else:
color_temp_kelvin = 0
w_r, w_g, w_b = color_temperature_to_rgb(color_temp_kelvin) w_r, w_g, w_b = color_temperature_to_rgb(color_temp_kelvin)
white_level = max(cw, ww) / 255 white_level = max(cw, ww) / 255

View File

@ -254,7 +254,7 @@ aioridwell==2021.12.2
aiosenseme==0.6.1 aiosenseme==0.6.1
# homeassistant.components.shelly # homeassistant.components.shelly
aioshelly==1.0.8 aioshelly==1.0.9
# homeassistant.components.steamist # homeassistant.components.steamist
aiosteamist==0.3.1 aiosteamist==0.3.1
@ -681,7 +681,7 @@ fjaraskupan==1.0.2
flipr-api==1.4.1 flipr-api==1.4.1
# homeassistant.components.flux_led # homeassistant.components.flux_led
flux_led==0.28.20 flux_led==0.28.21
# homeassistant.components.homekit # homeassistant.components.homekit
fnvhash==0.1.0 fnvhash==0.1.0
@ -914,7 +914,7 @@ influxdb-client==1.24.0
influxdb==5.3.1 influxdb==5.3.1
# homeassistant.components.intellifire # homeassistant.components.intellifire
intellifire4py==0.5 intellifire4py==0.6
# homeassistant.components.iotawatt # homeassistant.components.iotawatt
iotawattpy==0.1.0 iotawattpy==0.1.0
@ -1249,7 +1249,7 @@ pilight==0.1.1
# homeassistant.components.seven_segments # homeassistant.components.seven_segments
# homeassistant.components.sighthound # homeassistant.components.sighthound
# homeassistant.components.tensorflow # homeassistant.components.tensorflow
pillow==9.0.0 pillow==9.0.1
# homeassistant.components.dominos # homeassistant.components.dominos
pizzapi==0.0.3 pizzapi==0.0.3
@ -2190,7 +2190,7 @@ simplehound==0.3
simplepush==1.1.4 simplepush==1.1.4
# homeassistant.components.simplisafe # homeassistant.components.simplisafe
simplisafe-python==2022.01.0 simplisafe-python==2022.02.0
# homeassistant.components.sisyphus # homeassistant.components.sisyphus
sisyphus-control==3.1.2 sisyphus-control==3.1.2
@ -2228,7 +2228,7 @@ smhi-pkg==1.0.15
snapcast==2.1.3 snapcast==2.1.3
# homeassistant.components.sonos # homeassistant.components.sonos
soco==0.26.0 soco==0.26.2
# homeassistant.components.solaredge_local # homeassistant.components.solaredge_local
solaredge-local==0.2.0 solaredge-local==0.2.0
@ -2496,7 +2496,7 @@ xbox-webapi==2.0.11
xboxapi==2.0.1 xboxapi==2.0.1
# homeassistant.components.knx # homeassistant.components.knx
xknx==0.19.1 xknx==0.19.2
# homeassistant.components.bluesound # homeassistant.components.bluesound
# homeassistant.components.fritz # homeassistant.components.fritz

View File

@ -189,7 +189,7 @@ aioridwell==2021.12.2
aiosenseme==0.6.1 aiosenseme==0.6.1
# homeassistant.components.shelly # homeassistant.components.shelly
aioshelly==1.0.8 aioshelly==1.0.9
# homeassistant.components.steamist # homeassistant.components.steamist
aiosteamist==0.3.1 aiosteamist==0.3.1
@ -427,7 +427,7 @@ fjaraskupan==1.0.2
flipr-api==1.4.1 flipr-api==1.4.1
# homeassistant.components.flux_led # homeassistant.components.flux_led
flux_led==0.28.20 flux_led==0.28.21
# homeassistant.components.homekit # homeassistant.components.homekit
fnvhash==0.1.0 fnvhash==0.1.0
@ -586,7 +586,7 @@ influxdb-client==1.24.0
influxdb==5.3.1 influxdb==5.3.1
# homeassistant.components.intellifire # homeassistant.components.intellifire
intellifire4py==0.5 intellifire4py==0.6
# homeassistant.components.iotawatt # homeassistant.components.iotawatt
iotawattpy==0.1.0 iotawattpy==0.1.0
@ -771,7 +771,7 @@ pilight==0.1.1
# homeassistant.components.seven_segments # homeassistant.components.seven_segments
# homeassistant.components.sighthound # homeassistant.components.sighthound
# homeassistant.components.tensorflow # homeassistant.components.tensorflow
pillow==9.0.0 pillow==9.0.1
# homeassistant.components.plex # homeassistant.components.plex
plexapi==4.9.1 plexapi==4.9.1
@ -1337,7 +1337,7 @@ sharkiqpy==0.1.8
simplehound==0.3 simplehound==0.3
# homeassistant.components.simplisafe # homeassistant.components.simplisafe
simplisafe-python==2022.01.0 simplisafe-python==2022.02.0
# homeassistant.components.slack # homeassistant.components.slack
slackclient==2.5.0 slackclient==2.5.0
@ -1355,7 +1355,7 @@ smarthab==0.21
smhi-pkg==1.0.15 smhi-pkg==1.0.15
# homeassistant.components.sonos # homeassistant.components.sonos
soco==0.26.0 soco==0.26.2
# homeassistant.components.solaredge # homeassistant.components.solaredge
solaredge==0.0.2 solaredge==0.0.2
@ -1527,7 +1527,7 @@ wolf_smartset==0.1.11
xbox-webapi==2.0.11 xbox-webapi==2.0.11
# homeassistant.components.knx # homeassistant.components.knx
xknx==0.19.1 xknx==0.19.2
# homeassistant.components.bluesound # homeassistant.components.bluesound
# homeassistant.components.fritz # homeassistant.components.fritz

View File

@ -1,6 +1,6 @@
[metadata] [metadata]
name = homeassistant name = homeassistant
version = 2022.2.2 version = 2022.2.3
author = The Home Assistant Authors author = The Home Assistant Authors
author_email = hello@home-assistant.io author_email = hello@home-assistant.io
license = Apache-2.0 license = Apache-2.0

View File

@ -1,13 +1,17 @@
"""Define patches used for androidtv tests.""" """Define patches used for androidtv tests."""
from unittest.mock import mock_open, patch from unittest.mock import mock_open, patch
from androidtv.constants import CMD_DEVICE_PROPERTIES, CMD_MAC_ETH0, CMD_MAC_WLAN0
KEY_PYTHON = "python" KEY_PYTHON = "python"
KEY_SERVER = "server" KEY_SERVER = "server"
ADB_DEVICE_TCP_ASYNC_FAKE = "AdbDeviceTcpAsyncFake" ADB_DEVICE_TCP_ASYNC_FAKE = "AdbDeviceTcpAsyncFake"
DEVICE_ASYNC_FAKE = "DeviceAsyncFake" DEVICE_ASYNC_FAKE = "DeviceAsyncFake"
PROPS_DEV_INFO = "fake\nfake\n0123456\nfake"
PROPS_DEV_MAC = "ether ab:cd:ef:gh:ij:kl brd"
class AdbDeviceTcpAsyncFake: class AdbDeviceTcpAsyncFake:
"""A fake of the `adb_shell.adb_device_async.AdbDeviceTcpAsync` class.""" """A fake of the `adb_shell.adb_device_async.AdbDeviceTcpAsync` class."""
@ -100,12 +104,18 @@ def patch_connect(success):
} }
def patch_shell(response=None, error=False): def patch_shell(response=None, error=False, mac_eth=False):
"""Mock the `AdbDeviceTcpAsyncFake.shell` and `DeviceAsyncFake.shell` methods.""" """Mock the `AdbDeviceTcpAsyncFake.shell` and `DeviceAsyncFake.shell` methods."""
async def shell_success(self, cmd, *args, **kwargs): async def shell_success(self, cmd, *args, **kwargs):
"""Mock the `AdbDeviceTcpAsyncFake.shell` and `DeviceAsyncFake.shell` methods when they are successful.""" """Mock the `AdbDeviceTcpAsyncFake.shell` and `DeviceAsyncFake.shell` methods when they are successful."""
self.shell_cmd = cmd self.shell_cmd = cmd
if cmd == CMD_DEVICE_PROPERTIES:
return PROPS_DEV_INFO
if cmd == CMD_MAC_WLAN0:
return PROPS_DEV_MAC
if cmd == CMD_MAC_ETH0:
return PROPS_DEV_MAC if mac_eth else None
return response return response
async def shell_fail_python(self, cmd, *args, **kwargs): async def shell_fail_python(self, cmd, *args, **kwargs):
@ -185,15 +195,3 @@ PATCH_ANDROIDTV_UPDATE_EXCEPTION = patch(
"androidtv.androidtv.androidtv_async.AndroidTVAsync.update", "androidtv.androidtv.androidtv_async.AndroidTVAsync.update",
side_effect=ZeroDivisionError, side_effect=ZeroDivisionError,
) )
PATCH_DEVICE_PROPERTIES = patch(
"androidtv.basetv.basetv_async.BaseTVAsync.get_device_properties",
return_value={
"manufacturer": "a",
"model": "b",
"serialno": "c",
"sw_version": "d",
"wifimac": "ab:cd:ef:gh:ij:kl",
"ethmac": None,
},
)

View File

@ -31,6 +31,7 @@ from homeassistant.components.androidtv.const import (
DEFAULT_PORT, DEFAULT_PORT,
DOMAIN, DOMAIN,
PROP_ETHMAC, PROP_ETHMAC,
PROP_WIFIMAC,
) )
from homeassistant.components.media_player import DOMAIN as MP_DOMAIN from homeassistant.components.media_player import DOMAIN as MP_DOMAIN
from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER
@ -42,6 +43,7 @@ from tests.components.androidtv.patchers import isfile
ADBKEY = "adbkey" ADBKEY = "adbkey"
ETH_MAC = "a1:b1:c1:d1:e1:f1" ETH_MAC = "a1:b1:c1:d1:e1:f1"
WIFI_MAC = "a2:b2:c2:d2:e2:f2"
HOST = "127.0.0.1" HOST = "127.0.0.1"
VALID_DETECT_RULE = [{"paused": {"media_session_state": 3}}] VALID_DETECT_RULE = [{"paused": {"media_session_state": 3}}]
@ -84,18 +86,28 @@ PATCH_SETUP_ENTRY = patch(
class MockConfigDevice: class MockConfigDevice:
"""Mock class to emulate Android TV device.""" """Mock class to emulate Android TV device."""
def __init__(self, eth_mac=ETH_MAC): def __init__(self, eth_mac=ETH_MAC, wifi_mac=None):
"""Initialize a fake device to test config flow.""" """Initialize a fake device to test config flow."""
self.available = True self.available = True
self.device_properties = {PROP_ETHMAC: eth_mac} self.device_properties = {PROP_ETHMAC: eth_mac, PROP_WIFIMAC: wifi_mac}
async def adb_close(self): async def adb_close(self):
"""Fake method to close connection.""" """Fake method to close connection."""
self.available = False self.available = False
@pytest.mark.parametrize("config", [CONFIG_PYTHON_ADB, CONFIG_ADB_SERVER]) @pytest.mark.parametrize(
async def test_user(hass, config): ["config", "eth_mac", "wifi_mac"],
[
(CONFIG_PYTHON_ADB, ETH_MAC, None),
(CONFIG_ADB_SERVER, ETH_MAC, None),
(CONFIG_PYTHON_ADB, None, WIFI_MAC),
(CONFIG_ADB_SERVER, None, WIFI_MAC),
(CONFIG_PYTHON_ADB, ETH_MAC, WIFI_MAC),
(CONFIG_ADB_SERVER, ETH_MAC, WIFI_MAC),
],
)
async def test_user(hass, config, eth_mac, wifi_mac):
"""Test user config.""" """Test user config."""
flow_result = await hass.config_entries.flow.async_init( flow_result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER, "show_advanced_options": True} DOMAIN, context={"source": SOURCE_USER, "show_advanced_options": True}
@ -106,7 +118,7 @@ async def test_user(hass, config):
# test with all provided # test with all provided
with patch( with patch(
CONNECT_METHOD, CONNECT_METHOD,
return_value=(MockConfigDevice(), None), return_value=(MockConfigDevice(eth_mac, wifi_mac), None),
), PATCH_SETUP_ENTRY as mock_setup_entry, PATCH_GET_HOST_IP: ), PATCH_SETUP_ENTRY as mock_setup_entry, PATCH_GET_HOST_IP:
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
flow_result["flow_id"], user_input=config flow_result["flow_id"], user_input=config
@ -273,7 +285,7 @@ async def test_invalid_serial(hass):
"""Test for invalid serialno.""" """Test for invalid serialno."""
with patch( with patch(
CONNECT_METHOD, CONNECT_METHOD,
return_value=(MockConfigDevice(eth_mac=""), None), return_value=(MockConfigDevice(eth_mac=None), None),
), PATCH_GET_HOST_IP: ), PATCH_GET_HOST_IP:
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, DOMAIN,

View File

@ -142,29 +142,6 @@ def _setup(config):
return patch_key, entity_id, config_entry return patch_key, entity_id, config_entry
async def test_setup_with_properties(hass):
"""Test that setup succeeds with device properties.
the response must be a string with the following info separated with line break:
"manufacturer, model, serialno, version, mac_wlan0_output, mac_eth0_output"
"""
patch_key, entity_id, config_entry = _setup(CONFIG_ANDROIDTV_ADB_SERVER)
config_entry.add_to_hass(hass)
response = "fake\nfake\n0123456\nfake\nether a1:b1:c1:d1:e1:f1 brd\nnone"
with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[
patch_key
], patchers.patch_shell(response)[patch_key]:
with patchers.PATCH_DEVICE_PROPERTIES:
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
state = hass.states.get(entity_id)
assert state is not None
@pytest.mark.parametrize( @pytest.mark.parametrize(
"config", "config",
[ [
@ -190,7 +167,6 @@ async def test_reconnect(hass, caplog, config):
], patchers.patch_shell(SHELL_RESPONSE_OFF)[ ], patchers.patch_shell(SHELL_RESPONSE_OFF)[
patch_key patch_key
], patchers.PATCH_KEYGEN, patchers.PATCH_ANDROIDTV_OPEN, patchers.PATCH_SIGNER: ], patchers.PATCH_KEYGEN, patchers.PATCH_ANDROIDTV_OPEN, patchers.PATCH_SIGNER:
with patchers.PATCH_DEVICE_PROPERTIES:
assert await hass.config_entries.async_setup(config_entry.entry_id) assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()
@ -259,7 +235,6 @@ async def test_adb_shell_returns_none(hass, config):
], patchers.patch_shell(SHELL_RESPONSE_OFF)[ ], patchers.patch_shell(SHELL_RESPONSE_OFF)[
patch_key patch_key
], patchers.PATCH_KEYGEN, patchers.PATCH_ANDROIDTV_OPEN, patchers.PATCH_SIGNER: ], patchers.PATCH_KEYGEN, patchers.PATCH_ANDROIDTV_OPEN, patchers.PATCH_SIGNER:
with patchers.PATCH_DEVICE_PROPERTIES:
assert await hass.config_entries.async_setup(config_entry.entry_id) assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()
@ -289,7 +264,6 @@ async def test_setup_with_adbkey(hass):
], patchers.patch_shell(SHELL_RESPONSE_OFF)[ ], patchers.patch_shell(SHELL_RESPONSE_OFF)[
patch_key patch_key
], patchers.PATCH_ANDROIDTV_OPEN, patchers.PATCH_SIGNER, PATCH_ISFILE, PATCH_ACCESS: ], patchers.PATCH_ANDROIDTV_OPEN, patchers.PATCH_SIGNER, PATCH_ISFILE, PATCH_ACCESS:
with patchers.PATCH_DEVICE_PROPERTIES:
assert await hass.config_entries.async_setup(config_entry.entry_id) assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()
@ -324,7 +298,6 @@ async def test_sources(hass, config0):
with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[
patch_key patch_key
], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]: ], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]:
with patchers.PATCH_DEVICE_PROPERTIES:
assert await hass.config_entries.async_setup(config_entry.entry_id) assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()
@ -404,7 +377,6 @@ async def _test_exclude_sources(hass, config0, expected_sources):
with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[
patch_key patch_key
], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]: ], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]:
with patchers.PATCH_DEVICE_PROPERTIES:
assert await hass.config_entries.async_setup(config_entry.entry_id) assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()
@ -486,7 +458,6 @@ async def _test_select_source(hass, config0, source, expected_arg, method_patch)
with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[
patch_key patch_key
], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]: ], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]:
with patchers.PATCH_DEVICE_PROPERTIES:
assert await hass.config_entries.async_setup(config_entry.entry_id) assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()
@ -714,7 +685,6 @@ async def test_setup_fail(hass, config):
], patchers.patch_shell(SHELL_RESPONSE_OFF)[ ], patchers.patch_shell(SHELL_RESPONSE_OFF)[
patch_key patch_key
], patchers.PATCH_KEYGEN, patchers.PATCH_ANDROIDTV_OPEN, patchers.PATCH_SIGNER: ], patchers.PATCH_KEYGEN, patchers.PATCH_ANDROIDTV_OPEN, patchers.PATCH_SIGNER:
with patchers.PATCH_DEVICE_PROPERTIES:
assert await hass.config_entries.async_setup(config_entry.entry_id) is False assert await hass.config_entries.async_setup(config_entry.entry_id) is False
await hass.async_block_till_done() await hass.async_block_till_done()
@ -733,7 +703,6 @@ async def test_adb_command(hass):
with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[
patch_key patch_key
], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]: ], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]:
with patchers.PATCH_DEVICE_PROPERTIES:
assert await hass.config_entries.async_setup(config_entry.entry_id) assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()
@ -763,7 +732,6 @@ async def test_adb_command_unicode_decode_error(hass):
with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[
patch_key patch_key
], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]: ], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]:
with patchers.PATCH_DEVICE_PROPERTIES:
assert await hass.config_entries.async_setup(config_entry.entry_id) assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()
@ -793,7 +761,6 @@ async def test_adb_command_key(hass):
with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[
patch_key patch_key
], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]: ], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]:
with patchers.PATCH_DEVICE_PROPERTIES:
assert await hass.config_entries.async_setup(config_entry.entry_id) assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()
@ -823,7 +790,6 @@ async def test_adb_command_get_properties(hass):
with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[
patch_key patch_key
], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]: ], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]:
with patchers.PATCH_DEVICE_PROPERTIES:
assert await hass.config_entries.async_setup(config_entry.entry_id) assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()
@ -853,7 +819,6 @@ async def test_learn_sendevent(hass):
with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[
patch_key patch_key
], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]: ], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]:
with patchers.PATCH_DEVICE_PROPERTIES:
assert await hass.config_entries.async_setup(config_entry.entry_id) assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()
@ -882,7 +847,6 @@ async def test_update_lock_not_acquired(hass):
with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[
patch_key patch_key
], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]: ], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]:
with patchers.PATCH_DEVICE_PROPERTIES:
assert await hass.config_entries.async_setup(config_entry.entry_id) assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()
@ -918,7 +882,6 @@ async def test_download(hass):
with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[
patch_key patch_key
], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]: ], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]:
with patchers.PATCH_DEVICE_PROPERTIES:
assert await hass.config_entries.async_setup(config_entry.entry_id) assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()
@ -965,7 +928,6 @@ async def test_upload(hass):
with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[
patch_key patch_key
], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]: ], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]:
with patchers.PATCH_DEVICE_PROPERTIES:
assert await hass.config_entries.async_setup(config_entry.entry_id) assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()
@ -1010,7 +972,6 @@ async def test_androidtv_volume_set(hass):
with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[
patch_key patch_key
], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]: ], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]:
with patchers.PATCH_DEVICE_PROPERTIES:
assert await hass.config_entries.async_setup(config_entry.entry_id) assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()
@ -1038,7 +999,6 @@ async def test_get_image(hass, hass_ws_client):
with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[
patch_key patch_key
], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]: ], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]:
with patchers.PATCH_DEVICE_PROPERTIES:
assert await hass.config_entries.async_setup(config_entry.entry_id) assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()
@ -1115,7 +1075,6 @@ async def test_services_androidtv(hass):
with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[patch_key]: with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[patch_key]:
with patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]: with patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]:
with patchers.PATCH_DEVICE_PROPERTIES:
assert await hass.config_entries.async_setup(config_entry.entry_id) assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()
@ -1162,7 +1121,6 @@ async def test_services_firetv(hass):
with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[patch_key]: with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[patch_key]:
with patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]: with patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]:
with patchers.PATCH_DEVICE_PROPERTIES:
assert await hass.config_entries.async_setup(config_entry.entry_id) assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()
@ -1179,7 +1137,6 @@ async def test_volume_mute(hass):
with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[patch_key]: with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[patch_key]:
with patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]: with patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]:
with patchers.PATCH_DEVICE_PROPERTIES:
assert await hass.config_entries.async_setup(config_entry.entry_id) assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()
@ -1224,7 +1181,6 @@ async def test_connection_closed_on_ha_stop(hass):
with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[
patch_key patch_key
], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]: ], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]:
with patchers.PATCH_DEVICE_PROPERTIES:
assert await hass.config_entries.async_setup(config_entry.entry_id) assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()
@ -1249,7 +1205,6 @@ async def test_exception(hass):
], patchers.patch_shell(SHELL_RESPONSE_OFF)[ ], patchers.patch_shell(SHELL_RESPONSE_OFF)[
patch_key patch_key
], patchers.PATCH_KEYGEN, patchers.PATCH_ANDROIDTV_OPEN, patchers.PATCH_SIGNER: ], patchers.PATCH_KEYGEN, patchers.PATCH_ANDROIDTV_OPEN, patchers.PATCH_SIGNER:
with patchers.PATCH_DEVICE_PROPERTIES:
assert await hass.config_entries.async_setup(config_entry.entry_id) assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()

View File

@ -255,6 +255,33 @@ async def test_dhcp_match_macaddress(hass):
) )
async def test_dhcp_multiple_match_only_one_flow(hass):
"""Test matching the domain multiple times only generates one flow."""
integration_matchers = [
{"domain": "mock-domain", "macaddress": "B8B7F1*"},
{"domain": "mock-domain", "hostname": "connect"},
]
packet = Ether(RAW_DHCP_REQUEST)
async_handle_dhcp_packet = await _async_get_handle_dhcp_packet(
hass, integration_matchers
)
with patch.object(hass.config_entries.flow, "async_init") as mock_init:
await async_handle_dhcp_packet(packet)
assert len(mock_init.mock_calls) == 1
assert mock_init.mock_calls[0][1][0] == "mock-domain"
assert mock_init.mock_calls[0][2]["context"] == {
"source": config_entries.SOURCE_DHCP
}
assert mock_init.mock_calls[0][2]["data"] == dhcp.DhcpServiceInfo(
ip="192.168.210.56",
hostname="connect",
macaddress="b8b7f16db533",
)
async def test_dhcp_match_macaddress_without_hostname(hass): async def test_dhcp_match_macaddress_without_hostname(hass):
"""Test matching based on macaddress only.""" """Test matching based on macaddress only."""
integration_matchers = [{"domain": "mock-domain", "macaddress": "606BBD*"}] integration_matchers = [{"domain": "mock-domain", "macaddress": "606BBD*"}]

View File

@ -0,0 +1,119 @@
"""Test homekit diagnostics."""
from unittest.mock import ANY, patch
from homeassistant.components.homekit.const import DOMAIN
from homeassistant.const import CONF_NAME, CONF_PORT, EVENT_HOMEASSISTANT_STARTED
from .util import async_init_integration
from tests.common import MockConfigEntry
from tests.components.diagnostics import get_diagnostics_for_config_entry
async def test_config_entry_not_running(
hass, hass_client, hk_driver, mock_async_zeroconf
):
"""Test generating diagnostics for a config entry."""
entry = await async_init_integration(hass)
diag = await get_diagnostics_for_config_entry(hass, hass_client, entry)
assert diag == {
"config-entry": {
"data": {"name": "mock_name", "port": 12345},
"options": {},
"title": "Mock Title",
"version": 1,
},
"status": 0,
}
async def test_config_entry_running(hass, hass_client, hk_driver, mock_async_zeroconf):
"""Test generating diagnostics for a config entry."""
entry = MockConfigEntry(
domain=DOMAIN, data={CONF_NAME: "mock_name", CONF_PORT: 12345}
)
entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(entry.entry_id)
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
await hass.async_block_till_done()
diag = await get_diagnostics_for_config_entry(hass, hass_client, entry)
assert diag == {
"accessories": [
{
"aid": 1,
"services": [
{
"characteristics": [
{"format": "bool", "iid": 2, "perms": ["pw"], "type": "14"},
{
"format": "string",
"iid": 3,
"perms": ["pr"],
"type": "20",
"value": "Home Assistant",
},
{
"format": "string",
"iid": 4,
"perms": ["pr"],
"type": "21",
"value": "Bridge",
},
{
"format": "string",
"iid": 5,
"perms": ["pr"],
"type": "23",
"value": "mock_name",
},
{
"format": "string",
"iid": 6,
"perms": ["pr"],
"type": "30",
"value": "homekit.bridge",
},
{
"format": "string",
"iid": 7,
"perms": ["pr"],
"type": "52",
"value": ANY,
},
],
"iid": 1,
"type": "3E",
},
{
"characteristics": [
{
"format": "string",
"iid": 9,
"perms": ["pr", "ev"],
"type": "37",
"value": "01.01.00",
}
],
"iid": 8,
"type": "A2",
},
],
}
],
"client_properties": {},
"config-entry": {
"data": {"name": "mock_name", "port": 12345},
"options": {},
"title": "Mock Title",
"version": 1,
},
"config_version": 2,
"pairing_id": ANY,
"status": 1,
}
with patch("pyhap.accessory_driver.AccessoryDriver.async_start"), patch(
"homeassistant.components.homekit.HomeKit.async_stop"
), patch("homeassistant.components.homekit.async_port_is_available"):
assert await hass.config_entries.async_unload(entry.entry_id)
await hass.async_block_till_done()

View File

@ -5,7 +5,11 @@ from datetime import timedelta
from pyhap.const import HAP_REPR_AID, HAP_REPR_CHARS, HAP_REPR_IID, HAP_REPR_VALUE from pyhap.const import HAP_REPR_AID, HAP_REPR_CHARS, HAP_REPR_IID, HAP_REPR_VALUE
import pytest import pytest
from homeassistant.components.homekit.const import ATTR_VALUE from homeassistant.components.homekit.const import (
ATTR_VALUE,
PROP_MAX_VALUE,
PROP_MIN_VALUE,
)
from homeassistant.components.homekit.type_lights import ( from homeassistant.components.homekit.type_lights import (
CHANGE_COALESCE_TIME_WINDOW, CHANGE_COALESCE_TIME_WINDOW,
Light, Light,
@ -22,9 +26,12 @@ from homeassistant.components.light import (
ATTR_RGBW_COLOR, ATTR_RGBW_COLOR,
ATTR_RGBWW_COLOR, ATTR_RGBWW_COLOR,
ATTR_SUPPORTED_COLOR_MODES, ATTR_SUPPORTED_COLOR_MODES,
ATTR_WHITE,
COLOR_MODE_COLOR_TEMP, COLOR_MODE_COLOR_TEMP,
COLOR_MODE_RGB,
COLOR_MODE_RGBW, COLOR_MODE_RGBW,
COLOR_MODE_RGBWW, COLOR_MODE_RGBWW,
COLOR_MODE_WHITE,
DOMAIN, DOMAIN,
) )
from homeassistant.const import ( from homeassistant.const import (
@ -573,7 +580,7 @@ async def test_light_restore(hass, hk_driver, events):
@pytest.mark.parametrize( @pytest.mark.parametrize(
"supported_color_modes, state_props, turn_on_props, turn_on_props_with_brightness", "supported_color_modes, state_props, turn_on_props_with_brightness",
[ [
[ [
[COLOR_MODE_COLOR_TEMP, COLOR_MODE_RGBW], [COLOR_MODE_COLOR_TEMP, COLOR_MODE_RGBW],
@ -584,8 +591,7 @@ async def test_light_restore(hass, hk_driver, events):
ATTR_BRIGHTNESS: 255, ATTR_BRIGHTNESS: 255,
ATTR_COLOR_MODE: COLOR_MODE_RGBW, ATTR_COLOR_MODE: COLOR_MODE_RGBW,
}, },
{ATTR_RGBW_COLOR: (31, 127, 71, 0)}, {ATTR_HS_COLOR: (145, 75), ATTR_BRIGHTNESS_PCT: 25},
{ATTR_RGBW_COLOR: (15, 63, 35, 0)},
], ],
[ [
[COLOR_MODE_COLOR_TEMP, COLOR_MODE_RGBWW], [COLOR_MODE_COLOR_TEMP, COLOR_MODE_RGBWW],
@ -596,21 +602,19 @@ async def test_light_restore(hass, hk_driver, events):
ATTR_BRIGHTNESS: 255, ATTR_BRIGHTNESS: 255,
ATTR_COLOR_MODE: COLOR_MODE_RGBWW, ATTR_COLOR_MODE: COLOR_MODE_RGBWW,
}, },
{ATTR_RGBWW_COLOR: (31, 127, 71, 0, 0)}, {ATTR_HS_COLOR: (145, 75), ATTR_BRIGHTNESS_PCT: 25},
{ATTR_RGBWW_COLOR: (15, 63, 35, 0, 0)},
], ],
], ],
) )
async def test_light_rgb_with_white( async def test_light_rgb_with_color_temp(
hass, hass,
hk_driver, hk_driver,
events, events,
supported_color_modes, supported_color_modes,
state_props, state_props,
turn_on_props,
turn_on_props_with_brightness, turn_on_props_with_brightness,
): ):
"""Test lights with RGBW/RGBWW.""" """Test lights with RGBW/RGBWW with color temp support."""
entity_id = "light.demo" entity_id = "light.demo"
hass.states.async_set( hass.states.async_set(
@ -629,7 +633,7 @@ async def test_light_rgb_with_white(
await hass.async_block_till_done() await hass.async_block_till_done()
assert acc.char_hue.value == 23 assert acc.char_hue.value == 23
assert acc.char_saturation.value == 100 assert acc.char_saturation.value == 100
assert acc.char_brightness.value == 50 assert acc.char_brightness.value == 100
# Set from HomeKit # Set from HomeKit
call_turn_on = async_mock_service(hass, DOMAIN, "turn_on") call_turn_on = async_mock_service(hass, DOMAIN, "turn_on")
@ -658,11 +662,10 @@ async def test_light_rgb_with_white(
await _wait_for_light_coalesce(hass) await _wait_for_light_coalesce(hass)
assert call_turn_on assert call_turn_on
assert call_turn_on[-1].data[ATTR_ENTITY_ID] == entity_id assert call_turn_on[-1].data[ATTR_ENTITY_ID] == entity_id
for k, v in turn_on_props.items(): assert call_turn_on[-1].data[ATTR_HS_COLOR] == (145, 75)
assert call_turn_on[-1].data[k] == v
assert len(events) == 1 assert len(events) == 1
assert events[-1].data[ATTR_VALUE] == "set color at (145, 75)" assert events[-1].data[ATTR_VALUE] == "set color at (145, 75)"
assert acc.char_brightness.value == 50 assert acc.char_brightness.value == 100
hk_driver.set_characteristics( hk_driver.set_characteristics(
{ {
@ -697,7 +700,204 @@ async def test_light_rgb_with_white(
@pytest.mark.parametrize( @pytest.mark.parametrize(
"supported_color_modes, state_props, turn_on_props, turn_on_props_with_brightness", "supported_color_modes, state_props, turn_on_props_with_brightness",
[
[
[COLOR_MODE_RGBW],
{
ATTR_RGBW_COLOR: (128, 50, 0, 255),
ATTR_RGB_COLOR: (128, 50, 0),
ATTR_HS_COLOR: (23.438, 100.0),
ATTR_BRIGHTNESS: 255,
ATTR_COLOR_MODE: COLOR_MODE_RGBW,
},
{ATTR_RGBW_COLOR: (0, 0, 0, 191)},
],
[
[COLOR_MODE_RGBWW],
{
ATTR_RGBWW_COLOR: (128, 50, 0, 255, 255),
ATTR_RGB_COLOR: (128, 50, 0),
ATTR_HS_COLOR: (23.438, 100.0),
ATTR_BRIGHTNESS: 255,
ATTR_COLOR_MODE: COLOR_MODE_RGBWW,
},
{ATTR_RGBWW_COLOR: (0, 0, 0, 165, 26)},
],
],
)
async def test_light_rgbwx_with_color_temp_and_brightness(
hass,
hk_driver,
events,
supported_color_modes,
state_props,
turn_on_props_with_brightness,
):
"""Test lights with RGBW/RGBWW with color temp support and setting brightness."""
entity_id = "light.demo"
hass.states.async_set(
entity_id,
STATE_ON,
{ATTR_SUPPORTED_COLOR_MODES: supported_color_modes, **state_props},
)
await hass.async_block_till_done()
acc = Light(hass, hk_driver, "Light", entity_id, 1, None)
hk_driver.add_accessory(acc)
assert acc.char_hue.value == 23
assert acc.char_saturation.value == 100
await acc.run()
await hass.async_block_till_done()
assert acc.char_hue.value == 23
assert acc.char_saturation.value == 100
assert acc.char_brightness.value == 100
# Set from HomeKit
call_turn_on = async_mock_service(hass, DOMAIN, "turn_on")
char_color_temp_iid = acc.char_color_temp.to_HAP()[HAP_REPR_IID]
char_brightness_iid = acc.char_brightness.to_HAP()[HAP_REPR_IID]
hk_driver.set_characteristics(
{
HAP_REPR_CHARS: [
{
HAP_REPR_AID: acc.aid,
HAP_REPR_IID: char_color_temp_iid,
HAP_REPR_VALUE: 200,
},
{
HAP_REPR_AID: acc.aid,
HAP_REPR_IID: char_brightness_iid,
HAP_REPR_VALUE: 75,
},
]
},
"mock_addr",
)
await _wait_for_light_coalesce(hass)
assert call_turn_on
assert call_turn_on[-1].data[ATTR_ENTITY_ID] == entity_id
for k, v in turn_on_props_with_brightness.items():
assert call_turn_on[-1].data[k] == v
assert len(events) == 1
assert events[-1].data[ATTR_VALUE] == "brightness at 75%, color temperature at 200"
assert acc.char_brightness.value == 75
async def test_light_rgb_or_w_lights(
hass,
hk_driver,
events,
):
"""Test lights with RGB or W lights."""
entity_id = "light.demo"
hass.states.async_set(
entity_id,
STATE_ON,
{
ATTR_SUPPORTED_COLOR_MODES: [COLOR_MODE_RGB, COLOR_MODE_WHITE],
ATTR_RGBW_COLOR: (128, 50, 0, 255),
ATTR_RGB_COLOR: (128, 50, 0),
ATTR_HS_COLOR: (23.438, 100.0),
ATTR_BRIGHTNESS: 255,
ATTR_COLOR_MODE: COLOR_MODE_RGB,
},
)
await hass.async_block_till_done()
acc = Light(hass, hk_driver, "Light", entity_id, 1, None)
hk_driver.add_accessory(acc)
assert acc.char_hue.value == 23
assert acc.char_saturation.value == 100
await acc.run()
await hass.async_block_till_done()
assert acc.char_hue.value == 23
assert acc.char_saturation.value == 100
assert acc.char_brightness.value == 100
assert acc.char_color_temp.value == 153
# Set from HomeKit
call_turn_on = async_mock_service(hass, DOMAIN, "turn_on")
char_hue_iid = acc.char_hue.to_HAP()[HAP_REPR_IID]
char_saturation_iid = acc.char_saturation.to_HAP()[HAP_REPR_IID]
char_brightness_iid = acc.char_brightness.to_HAP()[HAP_REPR_IID]
char_color_temp_iid = acc.char_color_temp.to_HAP()[HAP_REPR_IID]
hk_driver.set_characteristics(
{
HAP_REPR_CHARS: [
{
HAP_REPR_AID: acc.aid,
HAP_REPR_IID: char_hue_iid,
HAP_REPR_VALUE: 145,
},
{
HAP_REPR_AID: acc.aid,
HAP_REPR_IID: char_saturation_iid,
HAP_REPR_VALUE: 75,
},
]
},
"mock_addr",
)
await _wait_for_light_coalesce(hass)
assert call_turn_on
assert call_turn_on[-1].data[ATTR_ENTITY_ID] == entity_id
assert call_turn_on[-1].data[ATTR_HS_COLOR] == (145, 75)
assert len(events) == 1
assert events[-1].data[ATTR_VALUE] == "set color at (145, 75)"
assert acc.char_brightness.value == 100
hk_driver.set_characteristics(
{
HAP_REPR_CHARS: [
{
HAP_REPR_AID: acc.aid,
HAP_REPR_IID: char_color_temp_iid,
HAP_REPR_VALUE: acc.min_mireds,
},
{
HAP_REPR_AID: acc.aid,
HAP_REPR_IID: char_brightness_iid,
HAP_REPR_VALUE: 25,
},
]
},
"mock_addr",
)
await _wait_for_light_coalesce(hass)
assert call_turn_on
assert call_turn_on[-1].data[ATTR_ENTITY_ID] == entity_id
assert call_turn_on[-1].data[ATTR_WHITE] == round(25 * 255 / 100)
assert len(events) == 2
assert events[-1].data[ATTR_VALUE] == "brightness at 25%, color temperature at 153"
assert acc.char_brightness.value == 25
hass.states.async_set(
entity_id,
STATE_ON,
{
ATTR_SUPPORTED_COLOR_MODES: [COLOR_MODE_RGB, COLOR_MODE_WHITE],
ATTR_BRIGHTNESS: 255,
ATTR_COLOR_MODE: COLOR_MODE_WHITE,
},
)
await hass.async_block_till_done()
assert acc.char_hue.value == 0
assert acc.char_saturation.value == 0
assert acc.char_brightness.value == 100
assert acc.char_color_temp.value == 153
@pytest.mark.parametrize(
"supported_color_modes, state_props",
[ [
[ [
[COLOR_MODE_COLOR_TEMP, COLOR_MODE_RGBW], [COLOR_MODE_COLOR_TEMP, COLOR_MODE_RGBW],
@ -708,8 +908,6 @@ async def test_light_rgb_with_white(
ATTR_BRIGHTNESS: 255, ATTR_BRIGHTNESS: 255,
ATTR_COLOR_MODE: COLOR_MODE_RGBW, ATTR_COLOR_MODE: COLOR_MODE_RGBW,
}, },
{ATTR_RGBW_COLOR: (31, 127, 71, 0)},
{ATTR_COLOR_TEMP: 2700},
], ],
[ [
[COLOR_MODE_COLOR_TEMP, COLOR_MODE_RGBWW], [COLOR_MODE_COLOR_TEMP, COLOR_MODE_RGBWW],
@ -720,8 +918,6 @@ async def test_light_rgb_with_white(
ATTR_BRIGHTNESS: 255, ATTR_BRIGHTNESS: 255,
ATTR_COLOR_MODE: COLOR_MODE_RGBWW, ATTR_COLOR_MODE: COLOR_MODE_RGBWW,
}, },
{ATTR_RGBWW_COLOR: (31, 127, 71, 0, 0)},
{ATTR_COLOR_TEMP: 2700},
], ],
], ],
) )
@ -731,8 +927,6 @@ async def test_light_rgb_with_white_switch_to_temp(
events, events,
supported_color_modes, supported_color_modes,
state_props, state_props,
turn_on_props,
turn_on_props_with_brightness,
): ):
"""Test lights with RGBW/RGBWW that preserves brightness when switching to color temp.""" """Test lights with RGBW/RGBWW that preserves brightness when switching to color temp."""
entity_id = "light.demo" entity_id = "light.demo"
@ -753,7 +947,7 @@ async def test_light_rgb_with_white_switch_to_temp(
await hass.async_block_till_done() await hass.async_block_till_done()
assert acc.char_hue.value == 23 assert acc.char_hue.value == 23
assert acc.char_saturation.value == 100 assert acc.char_saturation.value == 100
assert acc.char_brightness.value == 50 assert acc.char_brightness.value == 100
# Set from HomeKit # Set from HomeKit
call_turn_on = async_mock_service(hass, DOMAIN, "turn_on") call_turn_on = async_mock_service(hass, DOMAIN, "turn_on")
@ -782,19 +976,17 @@ async def test_light_rgb_with_white_switch_to_temp(
await _wait_for_light_coalesce(hass) await _wait_for_light_coalesce(hass)
assert call_turn_on assert call_turn_on
assert call_turn_on[-1].data[ATTR_ENTITY_ID] == entity_id assert call_turn_on[-1].data[ATTR_ENTITY_ID] == entity_id
for k, v in turn_on_props.items(): assert call_turn_on[-1].data[ATTR_HS_COLOR] == (145, 75)
assert call_turn_on[-1].data[k] == v
assert len(events) == 1 assert len(events) == 1
assert events[-1].data[ATTR_VALUE] == "set color at (145, 75)" assert events[-1].data[ATTR_VALUE] == "set color at (145, 75)"
assert acc.char_brightness.value == 50 assert acc.char_brightness.value == 100
hk_driver.set_characteristics( hk_driver.set_characteristics(
{ {
HAP_REPR_CHARS: [ HAP_REPR_CHARS: [
{ {
HAP_REPR_AID: acc.aid, HAP_REPR_AID: acc.aid,
HAP_REPR_IID: char_color_temp_iid, HAP_REPR_IID: char_color_temp_iid,
HAP_REPR_VALUE: 2700, HAP_REPR_VALUE: 500,
}, },
] ]
}, },
@ -803,11 +995,221 @@ async def test_light_rgb_with_white_switch_to_temp(
await _wait_for_light_coalesce(hass) await _wait_for_light_coalesce(hass)
assert call_turn_on assert call_turn_on
assert call_turn_on[-1].data[ATTR_ENTITY_ID] == entity_id assert call_turn_on[-1].data[ATTR_ENTITY_ID] == entity_id
for k, v in turn_on_props_with_brightness.items(): assert call_turn_on[-1].data[ATTR_COLOR_TEMP] == 500
assert call_turn_on[-1].data[k] == v
assert len(events) == 2 assert len(events) == 2
assert events[-1].data[ATTR_VALUE] == "color temperature at 2700" assert events[-1].data[ATTR_VALUE] == "color temperature at 500"
assert acc.char_brightness.value == 50 assert acc.char_brightness.value == 100
async def test_light_rgbww_with_color_temp_conversion(
hass,
hk_driver,
events,
):
"""Test lights with RGBWW convert color temp as expected."""
entity_id = "light.demo"
hass.states.async_set(
entity_id,
STATE_ON,
{
ATTR_SUPPORTED_COLOR_MODES: [COLOR_MODE_RGBWW],
ATTR_RGBWW_COLOR: (128, 50, 0, 255, 255),
ATTR_RGB_COLOR: (128, 50, 0),
ATTR_HS_COLOR: (23.438, 100.0),
ATTR_BRIGHTNESS: 255,
ATTR_COLOR_MODE: COLOR_MODE_RGBWW,
},
)
await hass.async_block_till_done()
acc = Light(hass, hk_driver, "Light", entity_id, 1, None)
hk_driver.add_accessory(acc)
assert acc.char_hue.value == 23
assert acc.char_saturation.value == 100
await acc.run()
await hass.async_block_till_done()
assert acc.char_hue.value == 23
assert acc.char_saturation.value == 100
assert acc.char_brightness.value == 100
# Set from HomeKit
call_turn_on = async_mock_service(hass, DOMAIN, "turn_on")
char_hue_iid = acc.char_hue.to_HAP()[HAP_REPR_IID]
char_saturation_iid = acc.char_saturation.to_HAP()[HAP_REPR_IID]
char_color_temp_iid = acc.char_color_temp.to_HAP()[HAP_REPR_IID]
char_brightness_iid = acc.char_brightness.to_HAP()[HAP_REPR_IID]
hk_driver.set_characteristics(
{
HAP_REPR_CHARS: [
{
HAP_REPR_AID: acc.aid,
HAP_REPR_IID: char_hue_iid,
HAP_REPR_VALUE: 145,
},
{
HAP_REPR_AID: acc.aid,
HAP_REPR_IID: char_saturation_iid,
HAP_REPR_VALUE: 75,
},
]
},
"mock_addr",
)
await _wait_for_light_coalesce(hass)
assert call_turn_on
assert call_turn_on[-1].data[ATTR_ENTITY_ID] == entity_id
assert call_turn_on[-1].data[ATTR_HS_COLOR] == (145, 75)
assert len(events) == 1
assert events[-1].data[ATTR_VALUE] == "set color at (145, 75)"
assert acc.char_brightness.value == 100
hk_driver.set_characteristics(
{
HAP_REPR_CHARS: [
{
HAP_REPR_AID: acc.aid,
HAP_REPR_IID: char_color_temp_iid,
HAP_REPR_VALUE: 200,
},
]
},
"mock_addr",
)
await _wait_for_light_coalesce(hass)
assert call_turn_on
assert call_turn_on[-1].data[ATTR_ENTITY_ID] == entity_id
assert call_turn_on[-1].data[ATTR_RGBWW_COLOR] == (0, 0, 0, 220, 35)
assert len(events) == 2
assert events[-1].data[ATTR_VALUE] == "color temperature at 200"
assert acc.char_brightness.value == 100
hass.states.async_set(
entity_id,
STATE_ON,
{
ATTR_SUPPORTED_COLOR_MODES: [COLOR_MODE_RGBWW],
ATTR_RGBWW_COLOR: (0, 0, 0, 128, 255),
ATTR_RGB_COLOR: (255, 163, 79),
ATTR_HS_COLOR: (28.636, 69.02),
ATTR_BRIGHTNESS: 180,
ATTR_COLOR_MODE: COLOR_MODE_RGBWW,
},
)
await hass.async_block_till_done()
hk_driver.set_characteristics(
{
HAP_REPR_CHARS: [
{
HAP_REPR_AID: acc.aid,
HAP_REPR_IID: char_brightness_iid,
HAP_REPR_VALUE: 100,
},
]
},
"mock_addr",
)
await _wait_for_light_coalesce(hass)
assert call_turn_on
assert call_turn_on[-1].data[ATTR_ENTITY_ID] == entity_id
assert call_turn_on[-1].data[ATTR_BRIGHTNESS_PCT] == 100
assert len(events) == 3
assert events[-1].data[ATTR_VALUE] == "brightness at 100%"
assert acc.char_brightness.value == 100
async def test_light_rgbw_with_color_temp_conversion(
hass,
hk_driver,
events,
):
"""Test lights with RGBW convert color temp as expected."""
entity_id = "light.demo"
hass.states.async_set(
entity_id,
STATE_ON,
{
ATTR_SUPPORTED_COLOR_MODES: [COLOR_MODE_RGBW],
ATTR_RGBWW_COLOR: (128, 50, 0, 255, 255),
ATTR_RGB_COLOR: (128, 50, 0),
ATTR_HS_COLOR: (23.438, 100.0),
ATTR_BRIGHTNESS: 255,
ATTR_COLOR_MODE: COLOR_MODE_RGBW,
},
)
await hass.async_block_till_done()
acc = Light(hass, hk_driver, "Light", entity_id, 1, None)
hk_driver.add_accessory(acc)
assert acc.char_hue.value == 23
assert acc.char_saturation.value == 100
await acc.run()
await hass.async_block_till_done()
assert acc.char_hue.value == 23
assert acc.char_saturation.value == 100
assert acc.char_brightness.value == 100
# Set from HomeKit
call_turn_on = async_mock_service(hass, DOMAIN, "turn_on")
char_hue_iid = acc.char_hue.to_HAP()[HAP_REPR_IID]
char_saturation_iid = acc.char_saturation.to_HAP()[HAP_REPR_IID]
char_color_temp_iid = acc.char_color_temp.to_HAP()[HAP_REPR_IID]
assert (
acc.char_color_temp.properties[PROP_MIN_VALUE]
== acc.char_color_temp.properties[PROP_MAX_VALUE]
)
hk_driver.set_characteristics(
{
HAP_REPR_CHARS: [
{
HAP_REPR_AID: acc.aid,
HAP_REPR_IID: char_hue_iid,
HAP_REPR_VALUE: 145,
},
{
HAP_REPR_AID: acc.aid,
HAP_REPR_IID: char_saturation_iid,
HAP_REPR_VALUE: 75,
},
]
},
"mock_addr",
)
await _wait_for_light_coalesce(hass)
assert call_turn_on
assert call_turn_on[-1].data[ATTR_ENTITY_ID] == entity_id
assert call_turn_on[-1].data[ATTR_HS_COLOR] == (145, 75)
assert len(events) == 1
assert events[-1].data[ATTR_VALUE] == "set color at (145, 75)"
assert acc.char_brightness.value == 100
hk_driver.set_characteristics(
{
HAP_REPR_CHARS: [
{
HAP_REPR_AID: acc.aid,
HAP_REPR_IID: char_color_temp_iid,
HAP_REPR_VALUE: 153,
},
]
},
"mock_addr",
)
await _wait_for_light_coalesce(hass)
assert call_turn_on
assert call_turn_on[-1].data[ATTR_ENTITY_ID] == entity_id
assert call_turn_on[-1].data[ATTR_RGBW_COLOR] == (0, 0, 0, 255)
assert len(events) == 2
assert events[-1].data[ATTR_VALUE] == "color temperature at 153"
assert acc.char_brightness.value == 100
async def test_light_set_brightness_and_color(hass, hk_driver, events): async def test_light_set_brightness_and_color(hass, hk_driver, events):

View File

@ -102,6 +102,24 @@ TEST_CONFIG_HYBRID = NestTestConfig(
}, },
) )
TEST_CONFIG_LEGACY = NestTestConfig(
config={
"nest": {
"client_id": "some-client-id",
"client_secret": "some-client-secret",
},
},
config_entry_data={
"auth_implementation": "local",
"tokens": {
"expires_at": time.time() + 86400,
"access_token": {
"token": "some-token",
},
},
},
)
class FakeSubscriber(GoogleNestSubscriber): class FakeSubscriber(GoogleNestSubscriber):
"""Fake subscriber that supplies a FakeDeviceManager.""" """Fake subscriber that supplies a FakeDeviceManager."""

View File

@ -6,9 +6,11 @@ from homeassistant import config_entries, data_entry_flow
from homeassistant.components.nest import DOMAIN, config_flow from homeassistant.components.nest import DOMAIN, config_flow
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
from .common import TEST_CONFIG_LEGACY
from tests.common import MockConfigEntry from tests.common import MockConfigEntry
CONFIG = {DOMAIN: {"client_id": "bla", "client_secret": "bla"}} CONFIG = TEST_CONFIG_LEGACY.config
async def test_abort_if_no_implementation_registered(hass): async def test_abort_if_no_implementation_registered(hass):
@ -59,7 +61,7 @@ async def test_full_flow_implementation(hass):
assert ( assert (
result["description_placeholders"] result["description_placeholders"]
.get("url") .get("url")
.startswith("https://home.nest.com/login/oauth2?client_id=bla") .startswith("https://home.nest.com/login/oauth2?client_id=some-client-id")
) )
def mock_login(auth): def mock_login(auth):

View File

@ -2,25 +2,22 @@
from unittest.mock import patch from unittest.mock import patch
from google_nest_sdm.device import Device
from google_nest_sdm.exceptions import SubscriberException from google_nest_sdm.exceptions import SubscriberException
import pytest
from homeassistant.components.nest import DOMAIN
from homeassistant.config_entries import ConfigEntryState from homeassistant.config_entries import ConfigEntryState
from homeassistant.setup import async_setup_component
from .common import CONFIG, async_setup_sdm_platform, create_config_entry from .common import TEST_CONFIG_LEGACY
from tests.components.diagnostics import get_diagnostics_for_config_entry from tests.components.diagnostics import get_diagnostics_for_config_entry
THERMOSTAT_TYPE = "sdm.devices.types.THERMOSTAT"
async def test_entry_diagnostics(
async def test_entry_diagnostics(hass, hass_client): hass, hass_client, create_device, setup_platform, config_entry
):
"""Test config entry diagnostics.""" """Test config entry diagnostics."""
devices = { create_device.create(
"some-device-id": Device.MakeDevice( raw_data={
{
"name": "enterprises/project-id/devices/device-id", "name": "enterprises/project-id/devices/device-id",
"type": "sdm.devices.types.THERMOSTAT", "type": "sdm.devices.types.THERMOSTAT",
"assignee": "enterprises/project-id/structures/structure-id/rooms/room-id", "assignee": "enterprises/project-id/structures/structure-id/rooms/room-id",
@ -41,15 +38,9 @@ async def test_entry_diagnostics(hass, hass_client):
"displayName": "Lobby", "displayName": "Lobby",
} }
], ],
},
auth=None,
)
} }
assert await async_setup_sdm_platform(hass, platform=None, devices=devices) )
await setup_platform()
entries = hass.config_entries.async_entries(DOMAIN)
assert len(entries) == 1
config_entry = entries[0]
assert config_entry.state is ConfigEntryState.LOADED assert config_entry.state is ConfigEntryState.LOADED
# Test that only non identifiable device information is returned # Test that only non identifiable device information is returned
@ -76,20 +67,32 @@ async def test_entry_diagnostics(hass, hass_client):
} }
async def test_setup_susbcriber_failure(hass, hass_client): async def test_setup_susbcriber_failure(
hass, hass_client, config_entry, setup_base_platform
):
"""Test configuration error.""" """Test configuration error."""
config_entry = create_config_entry()
config_entry.add_to_hass(hass)
with patch( with patch(
"homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation" "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation"
), patch( ), patch(
"homeassistant.components.nest.api.GoogleNestSubscriber.start_async", "homeassistant.components.nest.api.GoogleNestSubscriber.start_async",
side_effect=SubscriberException(), side_effect=SubscriberException(),
): ):
assert await async_setup_component(hass, DOMAIN, CONFIG) await setup_base_platform()
assert config_entry.state is ConfigEntryState.SETUP_RETRY assert config_entry.state is ConfigEntryState.SETUP_RETRY
assert await get_diagnostics_for_config_entry(hass, hass_client, config_entry) == { assert await get_diagnostics_for_config_entry(hass, hass_client, config_entry) == {
"error": "No subscriber configured" "error": "No subscriber configured"
} }
@pytest.mark.parametrize("nest_test_config", [TEST_CONFIG_LEGACY])
async def test_legacy_config_entry_diagnostics(
hass, hass_client, config_entry, setup_base_platform
):
"""Test config entry diagnostics for legacy integration doesn't fail."""
with patch("homeassistant.components.nest.legacy.Nest"):
await setup_base_platform()
assert await get_diagnostics_for_config_entry(hass, hass_client, config_entry) == {}

View File

@ -1,30 +1,18 @@
"""Test basic initialization for the Legacy Nest API using mocks for the Nest python library.""" """Test basic initialization for the Legacy Nest API using mocks for the Nest python library."""
import time
from unittest.mock import MagicMock, PropertyMock, patch from unittest.mock import MagicMock, PropertyMock, patch
from homeassistant.setup import async_setup_component import pytest
from tests.common import MockConfigEntry from .common import TEST_CONFIG_LEGACY
DOMAIN = "nest" DOMAIN = "nest"
CONFIG = {
"nest": {
"client_id": "some-client-id",
"client_secret": "some-client-secret",
},
}
CONFIG_ENTRY_DATA = { @pytest.fixture
"auth_implementation": "local", def nest_test_config():
"tokens": { """Fixture to specify the overall test fixture configuration."""
"expires_at": time.time() + 86400, return TEST_CONFIG_LEGACY
"access_token": {
"token": "some-token",
},
},
}
def make_thermostat(): def make_thermostat():
@ -45,7 +33,7 @@ def make_thermostat():
return device return device
async def test_thermostat(hass): async def test_thermostat(hass, setup_base_platform):
"""Test simple initialization for thermostat entities.""" """Test simple initialization for thermostat entities."""
thermostat = make_thermostat() thermostat = make_thermostat()
@ -58,8 +46,6 @@ async def test_thermostat(hass):
nest = MagicMock() nest = MagicMock()
type(nest).structures = PropertyMock(return_value=[structure]) type(nest).structures = PropertyMock(return_value=[structure])
config_entry = MockConfigEntry(domain=DOMAIN, data=CONFIG_ENTRY_DATA)
config_entry.add_to_hass(hass)
with patch("homeassistant.components.nest.legacy.Nest", return_value=nest), patch( with patch("homeassistant.components.nest.legacy.Nest", return_value=nest), patch(
"homeassistant.components.nest.legacy.sensor._VALID_SENSOR_TYPES", "homeassistant.components.nest.legacy.sensor._VALID_SENSOR_TYPES",
["humidity", "temperature"], ["humidity", "temperature"],
@ -67,8 +53,7 @@ async def test_thermostat(hass):
"homeassistant.components.nest.legacy.binary_sensor._VALID_BINARY_SENSOR_TYPES", "homeassistant.components.nest.legacy.binary_sensor._VALID_BINARY_SENSOR_TYPES",
{"fan": None}, {"fan": None},
): ):
assert await async_setup_component(hass, DOMAIN, CONFIG) await setup_base_platform()
await hass.async_block_till_done()
climate = hass.states.get("climate.my_thermostat") climate = hass.states.get("climate.my_thermostat")
assert climate is not None assert climate is not None

View File

@ -18,11 +18,12 @@ USER_ID = "12345"
@pytest.fixture(name="api") @pytest.fixture(name="api")
def api_fixture(system_v3, websocket): def api_fixture(data_subscription, system_v3, websocket):
"""Define a fixture for a simplisafe-python API object.""" """Define a fixture for a simplisafe-python API object."""
return Mock( return Mock(
async_get_systems=AsyncMock(return_value={SYSTEM_ID: system_v3}), async_get_systems=AsyncMock(return_value={SYSTEM_ID: system_v3}),
refresh_token=REFRESH_TOKEN, refresh_token=REFRESH_TOKEN,
subscription_data=data_subscription,
user_id=USER_ID, user_id=USER_ID,
websocket=websocket, websocket=websocket,
) )

View File

@ -7,7 +7,96 @@ from tests.components.diagnostics import get_diagnostics_for_config_entry
async def test_entry_diagnostics(hass, config_entry, hass_client, setup_simplisafe): async def test_entry_diagnostics(hass, config_entry, hass_client, setup_simplisafe):
"""Test config entry diagnostics.""" """Test config entry diagnostics."""
assert await get_diagnostics_for_config_entry(hass, hass_client, config_entry) == { assert await get_diagnostics_for_config_entry(hass, hass_client, config_entry) == {
"entry": {"options": {}}, "entry": {
"options": {},
},
"subscription_data": {
"system_123": {
"uid": REDACTED,
"sid": REDACTED,
"sStatus": 20,
"activated": 1445034752,
"planSku": "SSEDSM2",
"planName": "Interactive Monitoring",
"price": 24.99,
"currency": "USD",
"country": "US",
"expires": REDACTED,
"canceled": 0,
"extraTime": 0,
"creditCard": REDACTED,
"time": 2628000,
"paymentProfileId": REDACTED,
"features": {
"monitoring": True,
"alerts": True,
"online": True,
"hazard": True,
"video": True,
"cameras": 10,
"dispatch": True,
"proInstall": False,
"discount": 0,
"vipCS": False,
"medical": True,
"careVisit": False,
"storageDays": 30,
},
"status": {
"hasBaseStation": True,
"isActive": True,
"monitoring": "Active",
},
"subscriptionFeatures": {
"monitoredSensorsTypes": [
"Entry",
"Motion",
"GlassBreak",
"Smoke",
"CO",
"Freeze",
"Water",
],
"monitoredPanicConditions": ["Fire", "Medical", "Duress"],
"dispatchTypes": ["Police", "Fire", "Medical", "Guard"],
"remoteControl": [
"ArmDisarm",
"LockUnlock",
"ViewSettings",
"ConfigureSettings",
],
"cameraFeatures": {
"liveView": True,
"maxRecordingCameras": 10,
"recordingStorageDays": 30,
"videoVerification": True,
},
"support": {
"level": "Basic",
"annualVisit": False,
"professionalInstall": False,
},
"cellCommunicationBackup": True,
"alertChannels": ["Push", "SMS", "Email"],
"alertTypes": ["Alarm", "Error", "Activity", "Camera"],
"alarmModes": ["Alarm", "SecretAlert", "Disabled"],
"supportedIntegrations": [
"GoogleAssistant",
"AmazonAlexa",
"AugustLock",
],
"timeline": {},
},
"dispatcher": "cops",
"dcid": 0,
"location": REDACTED,
"pinUnlocked": True,
"billDate": 1602887552,
"billInterval": 2628000,
"pinUnlockedBy": "pin",
"autoActivation": None,
}
},
"systems": [ "systems": [
{ {
"address": REDACTED, "address": REDACTED,
@ -183,7 +272,7 @@ async def test_entry_diagnostics(hass, config_entry, hass_client, setup_simplisa
"shutter_open_when_off": False, "shutter_open_when_off": False,
"status": "online", "status": "online",
"subscription_enabled": True, "subscription_enabled": True,
}, }
], ],
"chime_volume": 2, "chime_volume": 2,
"entry_delay_away": 30, "entry_delay_away": 30,

View File

@ -405,6 +405,49 @@ def test_color_rgb_to_rgbww():
assert color_util.color_rgb_to_rgbww(255, 255, 255, 1, 5) == (103, 69, 0, 255, 255) assert color_util.color_rgb_to_rgbww(255, 255, 255, 1, 5) == (103, 69, 0, 255, 255)
def test_color_rgbww_to_rgb():
"""Test color_rgbww_to_rgb conversions."""
assert color_util.color_rgbww_to_rgb(0, 54, 98, 255, 255, 154, 370) == (
255,
255,
255,
)
assert color_util.color_rgbww_to_rgb(255, 255, 255, 0, 0, 154, 370) == (
255,
255,
255,
)
assert color_util.color_rgbww_to_rgb(0, 118, 241, 255, 255, 154, 370) == (
163,
204,
255,
)
assert color_util.color_rgbww_to_rgb(0, 27, 49, 128, 128, 154, 370) == (
128,
128,
128,
)
assert color_util.color_rgbww_to_rgb(0, 14, 25, 64, 64, 154, 370) == (64, 64, 64)
assert color_util.color_rgbww_to_rgb(9, 64, 0, 38, 38, 154, 370) == (32, 64, 16)
assert color_util.color_rgbww_to_rgb(0, 0, 0, 0, 0, 154, 370) == (0, 0, 0)
assert color_util.color_rgbww_to_rgb(103, 69, 0, 255, 255, 153, 370) == (
255,
193,
112,
)
assert color_util.color_rgbww_to_rgb(255, 255, 255, 0, 0, 0, 0) == (255, 255, 255)
assert color_util.color_rgbww_to_rgb(255, 255, 255, 255, 255, 0, 0) == (
255,
161,
128,
)
assert color_util.color_rgbww_to_rgb(255, 255, 255, 255, 255, 0, 370) == (
255,
245,
237,
)
def test_color_temperature_to_rgbww(): def test_color_temperature_to_rgbww():
"""Test color temp to warm, cold conversion. """Test color temp to warm, cold conversion.