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,
)
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.helpers.device_registry import DeviceEntryType
from homeassistant.helpers.entity import DeviceInfo
@ -62,9 +67,13 @@ class AccuWeatherEntity(CoordinatorEntity, WeatherEntity):
"""Initialize."""
super().__init__(coordinator)
self._unit_system = API_METRIC if coordinator.is_metric else API_IMPERIAL
self._attr_wind_speed_unit = self.coordinator.data["Wind"]["Speed"][
self._unit_system
]["Unit"]
wind_speed_unit = self.coordinator.data["Wind"]["Speed"][self._unit_system][
"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_unique_id = coordinator.location_key
self._attr_temperature_unit = (

View File

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

View File

@ -18,6 +18,7 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
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.storage import STORAGE_DIR
from homeassistant.helpers.typing import ConfigType
@ -33,16 +34,30 @@ from .const import (
DEVICE_ANDROIDTV,
DEVICE_FIRETV,
DOMAIN,
PROP_ETHMAC,
PROP_SERIALNO,
PROP_WIFIMAC,
SIGNAL_CONFIG_ENTITY,
)
PLATFORMS = [Platform.MEDIA_PLAYER]
RELOAD_OPTIONS = [CONF_STATE_DETECTION_RULES]
_INVALID_MACS = {"ff:ff:ff:ff:ff:ff"}
_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):
"""Generate an ADB key (if needed) and load it."""
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.core import callback
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 (
CONF_ADB_SERVER_IP,
CONF_ADB_SERVER_PORT,
@ -132,9 +131,7 @@ class AndroidTVFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
PROP_WIFIMAC,
dev_prop.get(PROP_WIFIMAC),
)
unique_id = format_mac(
dev_prop.get(PROP_ETHMAC) or dev_prop.get(PROP_WIFIMAC, "")
)
unique_id = get_androidtv_mac(dev_prop)
await aftv.adb_close()
return None, unique_id

View File

@ -51,12 +51,13 @@ from homeassistant.const import (
)
from homeassistant.core import HomeAssistant
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.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from . import get_androidtv_mac
from .const import (
ANDROID_DEV,
ANDROID_DEV_OPT,
@ -80,8 +81,6 @@ from .const import (
DEVICE_ANDROIDTV,
DEVICE_CLASSES,
DOMAIN,
PROP_ETHMAC,
PROP_WIFIMAC,
SIGNAL_CONFIG_ENTITY,
)
@ -343,7 +342,7 @@ class ADBDevice(MediaPlayerEntity):
self._attr_device_info[ATTR_MANUFACTURER] = manufacturer
if sw_version := info.get(ATTR_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._app_id_to_name = {}

View File

@ -62,7 +62,7 @@ async def websocket_update_device(hass, connection, msg):
msg.pop("type")
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"])
entry = registry.async_update_device(**msg)

View File

@ -179,6 +179,7 @@ class WatcherBase:
lowercase_hostname,
)
matched_domains = set()
for entry in self._integration_matchers:
if MAC_ADDRESS in entry and not fnmatch.fnmatch(
uppercase_mac, entry[MAC_ADDRESS]
@ -191,6 +192,11 @@ class WatcherBase:
continue
_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(
self.hass,
entry["domain"],

View File

@ -2,7 +2,7 @@
"domain": "doods",
"name": "DOODS - Dedicated Open Object Detection Service",
"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": [],
"iot_class": "local_polling"
}

View File

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

View File

@ -567,13 +567,6 @@ class AvmWrapper(FritzBoxTools):
)
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]:
"""Call WANCommonInterfaceConfig service."""
@ -678,11 +671,6 @@ class AvmWrapper(FritzBoxTools):
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]:
"""Call WANCommonInterfaceConfig service."""

View File

@ -277,10 +277,14 @@ async def async_setup_entry(
_LOGGER.debug("Setting up FRITZ!Box sensors")
avm_wrapper: AvmWrapper = hass.data[DOMAIN][entry.entry_id]
dsl: bool = False
dslinterface = await avm_wrapper.async_get_wan_dsl_interface_config()
if dslinterface:
dsl = dslinterface["NewEnable"]
link_properties = await avm_wrapper.async_get_wan_link_properties()
dsl: bool = link_properties.get("NewWANAccessType") == "DSL"
_LOGGER.debug(
"WANAccessType of FritzBox %s is '%s'",
avm_wrapper.host,
link_properties.get("NewWANAccessType"),
)
entities = [
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."""
from __future__ import annotations
import logging
import math
@ -12,12 +14,13 @@ from homeassistant.components.light import (
ATTR_HS_COLOR,
ATTR_MAX_MIREDS,
ATTR_MIN_MIREDS,
ATTR_RGB_COLOR,
ATTR_RGBW_COLOR,
ATTR_RGBWW_COLOR,
ATTR_SUPPORTED_COLOR_MODES,
ATTR_WHITE,
COLOR_MODE_RGBW,
COLOR_MODE_RGBWW,
COLOR_MODE_WHITE,
DOMAIN,
brightness_supported,
color_supported,
@ -32,9 +35,9 @@ from homeassistant.const import (
from homeassistant.core import callback
from homeassistant.helpers.event import async_call_later
from homeassistant.util.color import (
color_hsv_to_RGB,
color_temperature_mired_to_kelvin,
color_temperature_to_hs,
color_temperature_to_rgbww,
)
from .accessories import TYPES, HomeAccessory
@ -51,12 +54,13 @@ from .const import (
_LOGGER = logging.getLogger(__name__)
RGB_COLOR = "rgb_color"
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")
@ -79,8 +83,12 @@ class Light(HomeAccessory):
self.color_modes = color_modes = (
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_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)
if self.brightness_supported:
@ -89,7 +97,9 @@ class Light(HomeAccessory):
if self.color_supported:
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)
serv_light = self.add_preload_service(SERV_LIGHTBULB, self.chars)
@ -101,13 +111,22 @@ class Light(HomeAccessory):
# to set to the correct initial value.
self.char_brightness = serv_light.configure_char(CHAR_BRIGHTNESS, value=100)
if self.color_temp_supported:
min_mireds = math.floor(attributes.get(ATTR_MIN_MIREDS, 153))
max_mireds = math.ceil(attributes.get(ATTR_MAX_MIREDS, 500))
if CHAR_COLOR_TEMPERATURE in self.chars:
self.min_mireds = math.floor(
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(
CHAR_COLOR_TEMPERATURE,
value=min_mireds,
properties={PROP_MIN_VALUE: min_mireds, PROP_MAX_VALUE: max_mireds},
value=self.min_mireds,
properties={
PROP_MIN_VALUE: self.min_mireds,
PROP_MAX_VALUE: self.max_mireds,
},
)
if self.color_supported:
@ -165,32 +184,31 @@ class Light(HomeAccessory):
)
return
# Handle white channels
if CHAR_COLOR_TEMPERATURE in char_values:
params[ATTR_COLOR_TEMP] = char_values[CHAR_COLOR_TEMPERATURE]
events.append(f"color temperature at {params[ATTR_COLOR_TEMP]}")
temp = char_values[CHAR_COLOR_TEMPERATURE]
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 (
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)
):
elif CHAR_HUE in char_values or CHAR_SATURATION in char_values:
hue_sat = (
char_values.get(CHAR_HUE, self.char_hue.value),
char_values.get(CHAR_SATURATION, self.char_saturation.value),
)
_LOGGER.debug("%s: Set hs_color to %s", self.entity_id, 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
if (
@ -200,6 +218,9 @@ class Light(HomeAccessory):
):
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))
@callback
@ -210,20 +231,15 @@ class Light(HomeAccessory):
attributes = new_state.attributes
color_mode = attributes.get(ATTR_COLOR_MODE)
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
if self.brightness_supported:
if (
color_mode
and COLOR_MODES_WITH_WHITES.intersection({color_mode})
and (rgb_color := attributes.get(ATTR_RGB_COLOR))
self.brightness_supported
and (brightness := attributes.get(ATTR_BRIGHTNESS)) is not None
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)
# 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
@ -238,24 +254,36 @@ class Light(HomeAccessory):
if brightness == 0 and state == STATE_ON:
brightness = 1
self.char_brightness.set_value(brightness)
if color_mode_changed:
self.char_brightness.notify()
# Handle Color - color must always be set before color temperature
# or the iOS UI will not display it correctly.
if self.color_supported:
if ATTR_COLOR_TEMP in attributes:
if color_temp := attributes.get(ATTR_COLOR_TEMP):
hue, saturation = color_temperature_to_hs(
color_temperature_mired_to_kelvin(
new_state.attributes[ATTR_COLOR_TEMP]
)
color_temperature_mired_to_kelvin(color_temp)
)
elif color_mode == COLOR_MODE_WHITE:
hue, saturation = 0, 0
else:
hue, saturation = attributes.get(ATTR_HS_COLOR, (None, None))
if isinstance(hue, (int, float)) and isinstance(saturation, (int, float)):
self.char_hue.set_value(round(hue, 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:
color_temp = attributes.get(ATTR_COLOR_TEMP)
elif color_mode == COLOR_MODE_WHITE:
color_temp = self.min_mireds
if isinstance(color_temp, (int, float)):
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",
"config_flow": false,
"documentation": "https://www.home-assistant.io/integrations/image",
"requirements": ["pillow==9.0.0"],
"requirements": ["pillow==9.0.1"],
"dependencies": ["http"],
"codeowners": ["@home-assistant/core"],
"quality_scale": "internal"

View File

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

View File

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

View File

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

View File

@ -121,14 +121,22 @@ async def async_setup_entry(
if coordinator.data:
if coordinator.data.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 = (
coordinator.data.electricity[-1].cost.currency_unit
)
entities.append(OVOEnergySensor(coordinator, description, client))
if coordinator.data.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[
-1
].cost.currency_unit

View File

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

View File

@ -2,7 +2,7 @@
"domain": "qrcode",
"name": "QR Code",
"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": [],
"iot_class": "calculated"
}

View File

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

View File

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

View File

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

View File

@ -2,7 +2,7 @@
"domain": "sighthound",
"name": "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"],
"iot_class": "cloud_polling"
}

View File

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

View File

@ -11,14 +11,28 @@ from homeassistant.core import HomeAssistant
from . import SimpliSafe
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_SID = "sid"
CONF_SYSTEM_ID = "system_id"
CONF_UID = "uid"
CONF_WIFI_SSID = "wifi_ssid"
TO_REDACT = {
CONF_ADDRESS,
CONF_CREDIT_CARD,
CONF_EXPIRES,
CONF_LOCATION,
CONF_LOCATION_NAME,
CONF_PAYMENT_PROFILE_ID,
CONF_SERIAL,
CONF_SID,
CONF_SYSTEM_ID,
CONF_UID,
CONF_WIFI_SSID,
}
@ -34,6 +48,7 @@ async def async_get_config_entry_diagnostics(
"entry": {
"options": dict(entry.options),
},
"subscription_data": simplisafe.subscription_data,
"systems": [system.as_dict() for system in simplisafe.systems.values()],
},
TO_REDACT,

View File

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

View File

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

View File

@ -558,7 +558,7 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity):
plex_plugin.play_now(media)
return
share_link = self.speaker.share_link
share_link = self.coordinator.share_link
if share_link.is_share_link(media_id):
if kwargs.get(ATTR_MEDIA_ENQUEUE):
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.exceptions import (
SynologyDSMAPIErrorException,
SynologyDSMException,
SynologyDSMLoginFailedException,
SynologyDSMRequestException,
)
@ -237,7 +238,11 @@ class SynoApi:
async def async_unload(self) -> None:
"""Stop interacting with the NAS and prepare for removal from hass."""
try:
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:
"""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 not fqdn_with_ssl_verification
):
_LOGGER.debug(
_LOGGER.info(
"Update host from '%s' to '%s' for NAS '%s' via SSDP discovery",
existing_entry.data[CONF_HOST],
parsed_url.hostname,

View File

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

View File

@ -7,7 +7,7 @@ from .backports.enum import StrEnum
MAJOR_VERSION: Final = 2022
MINOR_VERSION: Final = 2
PATCH_VERSION: Final = "2"
PATCH_VERSION: Final = "3"
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
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
jinja2==3.0.3
paho-mqtt==1.6.1
pillow==9.0.0
pillow==9.0.1
pip>=8.0.3,<20.3
pyserial==3.5
python-slugify==4.0.1

View File

@ -472,7 +472,10 @@ def color_rgbww_to_rgb(
except ZeroDivisionError:
ct_ratio = 0.5
color_temp_mired = min_mireds + ct_ratio * mired_range
if 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)
white_level = max(cw, ww) / 255

View File

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

View File

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

View File

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

View File

@ -1,13 +1,17 @@
"""Define patches used for androidtv tests."""
from unittest.mock import mock_open, patch
from androidtv.constants import CMD_DEVICE_PROPERTIES, CMD_MAC_ETH0, CMD_MAC_WLAN0
KEY_PYTHON = "python"
KEY_SERVER = "server"
ADB_DEVICE_TCP_ASYNC_FAKE = "AdbDeviceTcpAsyncFake"
DEVICE_ASYNC_FAKE = "DeviceAsyncFake"
PROPS_DEV_INFO = "fake\nfake\n0123456\nfake"
PROPS_DEV_MAC = "ether ab:cd:ef:gh:ij:kl brd"
class AdbDeviceTcpAsyncFake:
"""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."""
async def shell_success(self, cmd, *args, **kwargs):
"""Mock the `AdbDeviceTcpAsyncFake.shell` and `DeviceAsyncFake.shell` methods when they are successful."""
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
async def shell_fail_python(self, cmd, *args, **kwargs):
@ -185,15 +195,3 @@ PATCH_ANDROIDTV_UPDATE_EXCEPTION = patch(
"androidtv.androidtv.androidtv_async.AndroidTVAsync.update",
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,
DOMAIN,
PROP_ETHMAC,
PROP_WIFIMAC,
)
from homeassistant.components.media_player import DOMAIN as MP_DOMAIN
from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER
@ -42,6 +43,7 @@ from tests.components.androidtv.patchers import isfile
ADBKEY = "adbkey"
ETH_MAC = "a1:b1:c1:d1:e1:f1"
WIFI_MAC = "a2:b2:c2:d2:e2:f2"
HOST = "127.0.0.1"
VALID_DETECT_RULE = [{"paused": {"media_session_state": 3}}]
@ -84,18 +86,28 @@ PATCH_SETUP_ENTRY = patch(
class MockConfigDevice:
"""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."""
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):
"""Fake method to close connection."""
self.available = False
@pytest.mark.parametrize("config", [CONFIG_PYTHON_ADB, CONFIG_ADB_SERVER])
async def test_user(hass, config):
@pytest.mark.parametrize(
["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."""
flow_result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER, "show_advanced_options": True}
@ -106,7 +118,7 @@ async def test_user(hass, config):
# test with all provided
with patch(
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:
result = await hass.config_entries.flow.async_configure(
flow_result["flow_id"], user_input=config
@ -273,7 +285,7 @@ async def test_invalid_serial(hass):
"""Test for invalid serialno."""
with patch(
CONNECT_METHOD,
return_value=(MockConfigDevice(eth_mac=""), None),
return_value=(MockConfigDevice(eth_mac=None), None),
), PATCH_GET_HOST_IP:
result = await hass.config_entries.flow.async_init(
DOMAIN,

View File

@ -142,29 +142,6 @@ def _setup(config):
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(
"config",
[
@ -190,7 +167,6 @@ async def test_reconnect(hass, caplog, config):
], patchers.patch_shell(SHELL_RESPONSE_OFF)[
patch_key
], 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)
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)[
patch_key
], 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)
await hass.async_block_till_done()
@ -289,7 +264,6 @@ async def test_setup_with_adbkey(hass):
], patchers.patch_shell(SHELL_RESPONSE_OFF)[
patch_key
], 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)
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)[
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)
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)[
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)
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)[
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)
await hass.async_block_till_done()
@ -714,7 +685,6 @@ async def test_setup_fail(hass, config):
], patchers.patch_shell(SHELL_RESPONSE_OFF)[
patch_key
], 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
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)[
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)
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)[
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)
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)[
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)
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)[
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)
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)[
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)
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)[
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)
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)[
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)
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)[
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)
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)[
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)
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)[
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)
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_shell(SHELL_RESPONSE_OFF)[patch_key]:
with patchers.PATCH_DEVICE_PROPERTIES:
assert await hass.config_entries.async_setup(config_entry.entry_id)
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_shell(SHELL_RESPONSE_OFF)[patch_key]:
with patchers.PATCH_DEVICE_PROPERTIES:
assert await hass.config_entries.async_setup(config_entry.entry_id)
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_shell(SHELL_RESPONSE_OFF)[patch_key]:
with patchers.PATCH_DEVICE_PROPERTIES:
assert await hass.config_entries.async_setup(config_entry.entry_id)
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)[
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)
await hass.async_block_till_done()
@ -1249,7 +1205,6 @@ async def test_exception(hass):
], patchers.patch_shell(SHELL_RESPONSE_OFF)[
patch_key
], 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)
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):
"""Test matching based on macaddress only."""
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
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 (
CHANGE_COALESCE_TIME_WINDOW,
Light,
@ -22,9 +26,12 @@ from homeassistant.components.light import (
ATTR_RGBW_COLOR,
ATTR_RGBWW_COLOR,
ATTR_SUPPORTED_COLOR_MODES,
ATTR_WHITE,
COLOR_MODE_COLOR_TEMP,
COLOR_MODE_RGB,
COLOR_MODE_RGBW,
COLOR_MODE_RGBWW,
COLOR_MODE_WHITE,
DOMAIN,
)
from homeassistant.const import (
@ -573,7 +580,7 @@ async def test_light_restore(hass, hk_driver, events):
@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],
@ -584,8 +591,7 @@ async def test_light_restore(hass, hk_driver, events):
ATTR_BRIGHTNESS: 255,
ATTR_COLOR_MODE: COLOR_MODE_RGBW,
},
{ATTR_RGBW_COLOR: (31, 127, 71, 0)},
{ATTR_RGBW_COLOR: (15, 63, 35, 0)},
{ATTR_HS_COLOR: (145, 75), ATTR_BRIGHTNESS_PCT: 25},
],
[
[COLOR_MODE_COLOR_TEMP, COLOR_MODE_RGBWW],
@ -596,21 +602,19 @@ async def test_light_restore(hass, hk_driver, events):
ATTR_BRIGHTNESS: 255,
ATTR_COLOR_MODE: COLOR_MODE_RGBWW,
},
{ATTR_RGBWW_COLOR: (31, 127, 71, 0, 0)},
{ATTR_RGBWW_COLOR: (15, 63, 35, 0, 0)},
{ATTR_HS_COLOR: (145, 75), ATTR_BRIGHTNESS_PCT: 25},
],
],
)
async def test_light_rgb_with_white(
async def test_light_rgb_with_color_temp(
hass,
hk_driver,
events,
supported_color_modes,
state_props,
turn_on_props,
turn_on_props_with_brightness,
):
"""Test lights with RGBW/RGBWW."""
"""Test lights with RGBW/RGBWW with color temp support."""
entity_id = "light.demo"
hass.states.async_set(
@ -629,7 +633,7 @@ async def test_light_rgb_with_white(
await hass.async_block_till_done()
assert acc.char_hue.value == 23
assert acc.char_saturation.value == 100
assert acc.char_brightness.value == 50
assert acc.char_brightness.value == 100
# Set from HomeKit
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)
assert call_turn_on
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[k] == v
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 == 50
assert acc.char_brightness.value == 100
hk_driver.set_characteristics(
{
@ -697,7 +700,204 @@ async def test_light_rgb_with_white(
@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],
@ -708,8 +908,6 @@ async def test_light_rgb_with_white(
ATTR_BRIGHTNESS: 255,
ATTR_COLOR_MODE: COLOR_MODE_RGBW,
},
{ATTR_RGBW_COLOR: (31, 127, 71, 0)},
{ATTR_COLOR_TEMP: 2700},
],
[
[COLOR_MODE_COLOR_TEMP, COLOR_MODE_RGBWW],
@ -720,8 +918,6 @@ async def test_light_rgb_with_white(
ATTR_BRIGHTNESS: 255,
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,
supported_color_modes,
state_props,
turn_on_props,
turn_on_props_with_brightness,
):
"""Test lights with RGBW/RGBWW that preserves brightness when switching to color temp."""
entity_id = "light.demo"
@ -753,7 +947,7 @@ async def test_light_rgb_with_white_switch_to_temp(
await hass.async_block_till_done()
assert acc.char_hue.value == 23
assert acc.char_saturation.value == 100
assert acc.char_brightness.value == 50
assert acc.char_brightness.value == 100
# Set from HomeKit
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)
assert call_turn_on
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[k] == v
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 == 50
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: 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)
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 call_turn_on[-1].data[ATTR_COLOR_TEMP] == 500
assert len(events) == 2
assert events[-1].data[ATTR_VALUE] == "color temperature at 2700"
assert acc.char_brightness.value == 50
assert events[-1].data[ATTR_VALUE] == "color temperature at 500"
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):

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):
"""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.setup import async_setup_component
from .common import TEST_CONFIG_LEGACY
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):
@ -59,7 +61,7 @@ async def test_full_flow_implementation(hass):
assert (
result["description_placeholders"]
.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):

View File

@ -2,25 +2,22 @@
from unittest.mock import patch
from google_nest_sdm.device import Device
from google_nest_sdm.exceptions import SubscriberException
import pytest
from homeassistant.components.nest import DOMAIN
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
THERMOSTAT_TYPE = "sdm.devices.types.THERMOSTAT"
async def test_entry_diagnostics(hass, hass_client):
async def test_entry_diagnostics(
hass, hass_client, create_device, setup_platform, config_entry
):
"""Test config entry diagnostics."""
devices = {
"some-device-id": Device.MakeDevice(
{
create_device.create(
raw_data={
"name": "enterprises/project-id/devices/device-id",
"type": "sdm.devices.types.THERMOSTAT",
"assignee": "enterprises/project-id/structures/structure-id/rooms/room-id",
@ -41,15 +38,9 @@ async def test_entry_diagnostics(hass, hass_client):
"displayName": "Lobby",
}
],
},
auth=None,
)
}
assert await async_setup_sdm_platform(hass, platform=None, devices=devices)
entries = hass.config_entries.async_entries(DOMAIN)
assert len(entries) == 1
config_entry = entries[0]
)
await setup_platform()
assert config_entry.state is ConfigEntryState.LOADED
# 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."""
config_entry = create_config_entry()
config_entry.add_to_hass(hass)
with patch(
"homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation"
), patch(
"homeassistant.components.nest.api.GoogleNestSubscriber.start_async",
side_effect=SubscriberException(),
):
assert await async_setup_component(hass, DOMAIN, CONFIG)
await setup_base_platform()
assert config_entry.state is ConfigEntryState.SETUP_RETRY
assert await get_diagnostics_for_config_entry(hass, hass_client, config_entry) == {
"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."""
import time
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"
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",
},
},
}
@pytest.fixture
def nest_test_config():
"""Fixture to specify the overall test fixture configuration."""
return TEST_CONFIG_LEGACY
def make_thermostat():
@ -45,7 +33,7 @@ def make_thermostat():
return device
async def test_thermostat(hass):
async def test_thermostat(hass, setup_base_platform):
"""Test simple initialization for thermostat entities."""
thermostat = make_thermostat()
@ -58,8 +46,6 @@ async def test_thermostat(hass):
nest = MagicMock()
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(
"homeassistant.components.nest.legacy.sensor._VALID_SENSOR_TYPES",
["humidity", "temperature"],
@ -67,8 +53,7 @@ async def test_thermostat(hass):
"homeassistant.components.nest.legacy.binary_sensor._VALID_BINARY_SENSOR_TYPES",
{"fan": None},
):
assert await async_setup_component(hass, DOMAIN, CONFIG)
await hass.async_block_till_done()
await setup_base_platform()
climate = hass.states.get("climate.my_thermostat")
assert climate is not None

View File

@ -18,11 +18,12 @@ USER_ID = "12345"
@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."""
return Mock(
async_get_systems=AsyncMock(return_value={SYSTEM_ID: system_v3}),
refresh_token=REFRESH_TOKEN,
subscription_data=data_subscription,
user_id=USER_ID,
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):
"""Test config entry diagnostics."""
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": [
{
"address": REDACTED,
@ -183,7 +272,7 @@ async def test_entry_diagnostics(hass, config_entry, hass_client, setup_simplisa
"shutter_open_when_off": False,
"status": "online",
"subscription_enabled": True,
},
}
],
"chime_volume": 2,
"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)
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():
"""Test color temp to warm, cold conversion.