mirror of
https://github.com/home-assistant/core.git
synced 2025-07-14 08:47:10 +00:00
Merge pull request #65955 from home-assistant/rc
This commit is contained in:
commit
f170aba0cc
@ -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 = (
|
||||
|
@ -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:
|
||||
|
@ -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"))
|
||||
|
@ -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
|
||||
|
||||
|
@ -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 = {}
|
||||
|
@ -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)
|
||||
|
@ -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"],
|
||||
|
@ -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"
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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."""
|
||||
|
||||
|
@ -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)
|
||||
|
44
homeassistant/components/homekit/diagnostics.py
Normal file
44
homeassistant/components/homekit/diagnostics.py
Normal 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
|
@ -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,33 +184,32 @@ 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
|
||||
params[ATTR_HS_COLOR] = hue_sat
|
||||
|
||||
if (
|
||||
brightness_pct
|
||||
@ -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,52 +231,59 @@ 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))
|
||||
):
|
||||
# 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
|
||||
# homekit accessory it will try to restore the last brightness state
|
||||
# which will be the last value saved by char_brightness.set_value.
|
||||
# But if it is set to 0, HomeKit will update the brightness to 100 as
|
||||
# it thinks 0 is off.
|
||||
#
|
||||
# Therefore, if the the brightness is 0 and the device is still on,
|
||||
# the brightness is mapped to 1 otherwise the update is ignored in
|
||||
# order to avoid this incorrect behavior.
|
||||
if brightness == 0 and state == STATE_ON:
|
||||
brightness = 1
|
||||
self.char_brightness.set_value(brightness)
|
||||
if (
|
||||
self.brightness_supported
|
||||
and (brightness := attributes.get(ATTR_BRIGHTNESS)) is not None
|
||||
and 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
|
||||
# homekit accessory it will try to restore the last brightness state
|
||||
# which will be the last value saved by char_brightness.set_value.
|
||||
# But if it is set to 0, HomeKit will update the brightness to 100 as
|
||||
# it thinks 0 is off.
|
||||
#
|
||||
# Therefore, if the the brightness is 0 and the device is still on,
|
||||
# the brightness is mapped to 1 otherwise the update is ignored in
|
||||
# order to avoid this incorrect behavior.
|
||||
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
|
||||
if self.color_temp_supported:
|
||||
color_temp = attributes.get(ATTR_COLOR_TEMP)
|
||||
# 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()
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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",
|
||||
|
@ -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"}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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": []
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
}
|
||||
|
@ -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.",
|
||||
|
@ -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"
|
||||
}
|
||||
|
@ -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:
|
||||
|
@ -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,
|
||||
|
@ -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": [
|
||||
|
@ -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."],
|
||||
|
@ -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)
|
||||
|
@ -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."""
|
||||
await self._syno_api_executer(self.dsm.logout)
|
||||
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."""
|
||||
|
@ -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,
|
||||
|
@ -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"
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -472,7 +472,10 @@ def color_rgbww_to_rgb(
|
||||
except ZeroDivisionError:
|
||||
ct_ratio = 0.5
|
||||
color_temp_mired = min_mireds + ct_ratio * mired_range
|
||||
color_temp_kelvin = color_temperature_mired_to_kelvin(color_temp_mired)
|
||||
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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
},
|
||||
)
|
||||
|
@ -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,
|
||||
|
@ -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,9 +167,8 @@ 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()
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await hass.helpers.entity_component.async_update_entity(entity_id)
|
||||
state = hass.states.get(entity_id)
|
||||
@ -259,9 +235,8 @@ 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()
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await hass.helpers.entity_component.async_update_entity(entity_id)
|
||||
state = hass.states.get(entity_id)
|
||||
@ -289,9 +264,8 @@ 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()
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await hass.helpers.entity_component.async_update_entity(entity_id)
|
||||
state = hass.states.get(entity_id)
|
||||
@ -324,9 +298,8 @@ 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()
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await hass.helpers.entity_component.async_update_entity(entity_id)
|
||||
state = hass.states.get(entity_id)
|
||||
@ -404,9 +377,8 @@ 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()
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await hass.helpers.entity_component.async_update_entity(entity_id)
|
||||
state = hass.states.get(entity_id)
|
||||
@ -486,9 +458,8 @@ 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()
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await hass.helpers.entity_component.async_update_entity(entity_id)
|
||||
state = hass.states.get(entity_id)
|
||||
@ -714,9 +685,8 @@ 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()
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id) is False
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await hass.helpers.entity_component.async_update_entity(entity_id)
|
||||
state = hass.states.get(entity_id)
|
||||
@ -733,9 +703,8 @@ 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()
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
with patch(
|
||||
"androidtv.basetv.basetv_async.BaseTVAsync.adb_shell", return_value=response
|
||||
@ -763,9 +732,8 @@ 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()
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
with patch(
|
||||
"androidtv.basetv.basetv_async.BaseTVAsync.adb_shell",
|
||||
@ -793,9 +761,8 @@ 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()
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
with patch(
|
||||
"androidtv.basetv.basetv_async.BaseTVAsync.adb_shell", return_value=response
|
||||
@ -823,9 +790,8 @@ 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()
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
with patch(
|
||||
"androidtv.androidtv.androidtv_async.AndroidTVAsync.get_properties_dict",
|
||||
@ -853,9 +819,8 @@ 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()
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
with patch(
|
||||
"androidtv.basetv.basetv_async.BaseTVAsync.learn_sendevent",
|
||||
@ -882,9 +847,8 @@ 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()
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
with patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]:
|
||||
await hass.helpers.entity_component.async_update_entity(entity_id)
|
||||
@ -918,9 +882,8 @@ 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()
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Failed download because path is not whitelisted
|
||||
with patch("androidtv.basetv.basetv_async.BaseTVAsync.adb_pull") as patch_pull:
|
||||
@ -965,9 +928,8 @@ 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()
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Failed upload because path is not whitelisted
|
||||
with patch("androidtv.basetv.basetv_async.BaseTVAsync.adb_push") as patch_push:
|
||||
@ -1010,9 +972,8 @@ 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()
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
with patch(
|
||||
"androidtv.basetv.basetv_async.BaseTVAsync.set_volume_level", return_value=0.5
|
||||
@ -1038,9 +999,8 @@ 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()
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
with patchers.patch_shell("11")[patch_key]:
|
||||
await hass.helpers.entity_component.async_update_entity(entity_id)
|
||||
@ -1115,9 +1075,8 @@ 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()
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
with patchers.patch_shell(SHELL_RESPONSE_STANDBY)[patch_key]:
|
||||
await _test_service(
|
||||
@ -1162,9 +1121,8 @@ 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()
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
with patchers.patch_shell(SHELL_RESPONSE_STANDBY)[patch_key]:
|
||||
await _test_service(hass, entity_id, SERVICE_MEDIA_STOP, "back")
|
||||
@ -1179,9 +1137,8 @@ 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()
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
with patchers.patch_shell(SHELL_RESPONSE_STANDBY)[patch_key]:
|
||||
service_data = {ATTR_ENTITY_ID: entity_id, ATTR_MEDIA_VOLUME_MUTED: True}
|
||||
@ -1224,9 +1181,8 @@ 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()
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
with patch(
|
||||
"androidtv.androidtv.androidtv_async.AndroidTVAsync.adb_close"
|
||||
@ -1249,9 +1205,8 @@ 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()
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await hass.helpers.entity_component.async_update_entity(entity_id)
|
||||
state = hass.states.get(entity_id)
|
||||
|
@ -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*"}]
|
||||
|
119
tests/components/homekit/test_diagnostics.py
Normal file
119
tests/components/homekit/test_diagnostics.py
Normal 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()
|
@ -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):
|
||||
|
@ -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."""
|
||||
|
@ -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):
|
||||
|
@ -2,54 +2,45 @@
|
||||
|
||||
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(
|
||||
{
|
||||
"name": "enterprises/project-id/devices/device-id",
|
||||
"type": "sdm.devices.types.THERMOSTAT",
|
||||
"assignee": "enterprises/project-id/structures/structure-id/rooms/room-id",
|
||||
"traits": {
|
||||
"sdm.devices.traits.Info": {
|
||||
"customName": "My Sensor",
|
||||
},
|
||||
"sdm.devices.traits.Temperature": {
|
||||
"ambientTemperatureCelsius": 25.1,
|
||||
},
|
||||
"sdm.devices.traits.Humidity": {
|
||||
"ambientHumidityPercent": 35.0,
|
||||
},
|
||||
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",
|
||||
"traits": {
|
||||
"sdm.devices.traits.Info": {
|
||||
"customName": "My Sensor",
|
||||
},
|
||||
"sdm.devices.traits.Temperature": {
|
||||
"ambientTemperatureCelsius": 25.1,
|
||||
},
|
||||
"sdm.devices.traits.Humidity": {
|
||||
"ambientHumidityPercent": 35.0,
|
||||
},
|
||||
"parentRelations": [
|
||||
{
|
||||
"parent": "enterprises/project-id/structures/structure-id/rooms/room-id",
|
||||
"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]
|
||||
"parentRelations": [
|
||||
{
|
||||
"parent": "enterprises/project-id/structures/structure-id/rooms/room-id",
|
||||
"displayName": "Lobby",
|
||||
}
|
||||
],
|
||||
}
|
||||
)
|
||||
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) == {}
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
)
|
||||
|
@ -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,
|
||||
|
@ -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.
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user