mirror of
https://github.com/home-assistant/core.git
synced 2025-07-20 19:57:07 +00:00
2024.9.2 (#126062)
This commit is contained in:
commit
b69b5aa82a
@ -24,5 +24,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/august",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["pubnub", "yalexs"],
|
||||
"requirements": ["yalexs==8.6.3", "yalexs-ble==2.4.3"]
|
||||
"requirements": ["yalexs==8.6.4", "yalexs-ble==2.4.3"]
|
||||
}
|
||||
|
@ -60,6 +60,9 @@ COLOR_MODE_MAP = {
|
||||
class BleBoxLightEntity(BleBoxEntity[blebox_uniapi.light.Light], LightEntity):
|
||||
"""Representation of BleBox lights."""
|
||||
|
||||
_attr_max_mireds = 370 # 1,000,000 divided by 2700 Kelvin = 370 Mireds
|
||||
_attr_min_mireds = 154 # 1,000,000 divided by 6500 Kelvin = 154 Mireds
|
||||
|
||||
def __init__(self, feature: blebox_uniapi.light.Light) -> None:
|
||||
"""Initialize a BleBox light."""
|
||||
super().__init__(feature)
|
||||
@ -87,12 +90,7 @@ class BleBoxLightEntity(BleBoxEntity[blebox_uniapi.light.Light], LightEntity):
|
||||
|
||||
Set values to _attr_ibutes if needed.
|
||||
"""
|
||||
color_mode_tmp = COLOR_MODE_MAP.get(self._feature.color_mode, ColorMode.ONOFF)
|
||||
if color_mode_tmp == ColorMode.COLOR_TEMP:
|
||||
self._attr_min_mireds = 1
|
||||
self._attr_max_mireds = 255
|
||||
|
||||
return color_mode_tmp
|
||||
return COLOR_MODE_MAP.get(self._feature.color_mode, ColorMode.ONOFF)
|
||||
|
||||
@property
|
||||
def supported_color_modes(self):
|
||||
|
@ -20,5 +20,8 @@ async def async_get_config_entry_diagnostics(
|
||||
return {
|
||||
"info": data.info.to_dict(),
|
||||
"device": data.device.to_dict(),
|
||||
"state": data.coordinator.data.state.to_dict(),
|
||||
"coordinator_data": {
|
||||
"state": data.coordinator.data.state.to_dict(),
|
||||
},
|
||||
"static": data.static.to_dict(),
|
||||
}
|
||||
|
@ -22,10 +22,10 @@ class BSBLanEntity(CoordinatorEntity[BSBLanUpdateCoordinator]):
|
||||
def __init__(self, coordinator: BSBLanUpdateCoordinator, data: BSBLanData) -> None:
|
||||
"""Initialize BSBLan entity."""
|
||||
super().__init__(coordinator, data)
|
||||
host = self.coordinator.config_entry.data["host"]
|
||||
mac = self.coordinator.config_entry.data["mac"]
|
||||
host = coordinator.config_entry.data["host"]
|
||||
mac = data.device.MAC
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, data.device.MAC)},
|
||||
identifiers={(DOMAIN, mac)},
|
||||
connections={(CONNECTION_NETWORK_MAC, format_mac(mac))},
|
||||
name=data.device.name,
|
||||
manufacturer="BSBLAN Inc.",
|
||||
|
@ -100,7 +100,7 @@ class ElevenLabsTTSEntity(TextToSpeechEntity):
|
||||
"""Load tts audio file from the engine."""
|
||||
_LOGGER.debug("Getting TTS audio for %s", message)
|
||||
_LOGGER.debug("Options: %s", options)
|
||||
voice_id = options[ATTR_VOICE]
|
||||
voice_id = options.get(ATTR_VOICE, self._default_voice_id)
|
||||
try:
|
||||
audio = await self._client.generate(
|
||||
text=message,
|
||||
|
@ -20,5 +20,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["home-assistant-frontend==20240906.0"]
|
||||
"requirements": ["home-assistant-frontend==20240909.1"]
|
||||
}
|
||||
|
@ -6,5 +6,5 @@
|
||||
"dependencies": ["network"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/govee_light_local",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["govee-local-api==1.5.1"]
|
||||
"requirements": ["govee-local-api==1.5.2"]
|
||||
}
|
||||
|
@ -51,7 +51,8 @@
|
||||
"not_hassio_thread": "The OpenThread Border Router addon can only be installed with Home Assistant OS. If you would like to use the {model} as an Thread border router, please flash the firmware manually using the [web flasher]({docs_web_flasher_url}) and set up OpenThread Border Router to communicate with it.",
|
||||
"otbr_addon_already_running": "The OpenThread Border Router add-on is already running, it cannot be installed again.",
|
||||
"zha_still_using_stick": "This {model} is in use by the Zigbee Home Automation integration. Please migrate your Zigbee network to another adapter or delete the integration and try again.",
|
||||
"otbr_still_using_stick": "This {model} is in use by the OpenThread Border Router add-on. If you use the Thread network, make sure you have alternative border routers. Uninstall the add-on and try again."
|
||||
"otbr_still_using_stick": "This {model} is in use by the OpenThread Border Router add-on. If you use the Thread network, make sure you have alternative border routers. Uninstall the add-on and try again.",
|
||||
"unsupported_firmware": "The radio firmware on your {model} could not be determined. Make sure that no other integration or addon is currently trying to communicate with the device. If you are running Home Assistant OS in a virtual machine or in Docker, please make sure that permissions are set correctly for the device."
|
||||
},
|
||||
"progress": {
|
||||
"install_zigbee_flasher_addon": "The Silicon Labs Flasher addon is installed, this may take a few minutes.",
|
||||
|
@ -113,7 +113,8 @@
|
||||
"not_hassio_thread": "[%key:component::homeassistant_hardware::firmware_picker::options::abort::not_hassio_thread%]",
|
||||
"otbr_addon_already_running": "[%key:component::homeassistant_hardware::firmware_picker::options::abort::otbr_addon_already_running%]",
|
||||
"zha_still_using_stick": "[%key:component::homeassistant_hardware::firmware_picker::options::abort::zha_still_using_stick%]",
|
||||
"otbr_still_using_stick": "[%key:component::homeassistant_hardware::firmware_picker::options::abort::otbr_still_using_stick%]"
|
||||
"otbr_still_using_stick": "[%key:component::homeassistant_hardware::firmware_picker::options::abort::otbr_still_using_stick%]",
|
||||
"unsupported_firmware": "[%key:component::homeassistant_hardware::firmware_picker::options::abort::unsupported_firmware%]"
|
||||
},
|
||||
"progress": {
|
||||
"install_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::install_addon%]",
|
||||
@ -181,7 +182,10 @@
|
||||
"zha_migration_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::zha_migration_failed%]",
|
||||
"not_hassio": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::not_hassio%]",
|
||||
"not_hassio_thread": "[%key:component::homeassistant_hardware::firmware_picker::options::abort::not_hassio_thread%]",
|
||||
"otbr_addon_already_running": "[%key:component::homeassistant_hardware::firmware_picker::options::abort::otbr_addon_already_running%]"
|
||||
"otbr_addon_already_running": "[%key:component::homeassistant_hardware::firmware_picker::options::abort::otbr_addon_already_running%]",
|
||||
"zha_still_using_stick": "[%key:component::homeassistant_hardware::firmware_picker::options::abort::zha_still_using_stick%]",
|
||||
"otbr_still_using_stick": "[%key:component::homeassistant_hardware::firmware_picker::options::abort::otbr_still_using_stick%]",
|
||||
"unsupported_firmware": "[%key:component::homeassistant_hardware::firmware_picker::options::abort::unsupported_firmware%]"
|
||||
},
|
||||
"progress": {
|
||||
"install_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::install_addon%]",
|
||||
|
@ -138,7 +138,8 @@
|
||||
"not_hassio_thread": "[%key:component::homeassistant_hardware::firmware_picker::options::abort::not_hassio_thread%]",
|
||||
"otbr_addon_already_running": "[%key:component::homeassistant_hardware::firmware_picker::options::abort::otbr_addon_already_running%]",
|
||||
"zha_still_using_stick": "[%key:component::homeassistant_hardware::firmware_picker::options::abort::zha_still_using_stick%]",
|
||||
"otbr_still_using_stick": "[%key:component::homeassistant_hardware::firmware_picker::options::abort::otbr_still_using_stick%]"
|
||||
"otbr_still_using_stick": "[%key:component::homeassistant_hardware::firmware_picker::options::abort::otbr_still_using_stick%]",
|
||||
"unsupported_firmware": "The radio firmware on your {model} could not be determined. Make sure that no other integration or addon is currently trying to communicate with the device."
|
||||
},
|
||||
"progress": {
|
||||
"install_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::install_addon%]",
|
||||
|
@ -90,8 +90,10 @@ class InComfortClimate(IncomfortEntity, ClimateEntity):
|
||||
|
||||
As we set the override, we report back the override. The actual set point is
|
||||
is returned at a later time.
|
||||
Some older thermostats return 0.0 as override, in that case we fallback to
|
||||
the actual setpoint.
|
||||
"""
|
||||
return self._room.override
|
||||
return self._room.override or self._room.setpoint
|
||||
|
||||
async def async_set_temperature(self, **kwargs: Any) -> None:
|
||||
"""Set a new target temperature for this zone."""
|
||||
|
@ -13,7 +13,7 @@
|
||||
"requirements": [
|
||||
"xknx==3.1.1",
|
||||
"xknxproject==3.7.1",
|
||||
"knx-frontend==2024.9.4.64538"
|
||||
"knx-frontend==2024.9.10.221729"
|
||||
],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
@ -38,7 +38,10 @@ def parse_invalid(exc: vol.Invalid) -> _ErrorDescription:
|
||||
|
||||
|
||||
def validate_entity_data(entity_data: dict) -> dict:
|
||||
"""Validate entity data. Return validated data or raise EntityStoreValidationException."""
|
||||
"""Validate entity data.
|
||||
|
||||
Return validated data or raise EntityStoreValidationException.
|
||||
"""
|
||||
try:
|
||||
# return so defaults are applied
|
||||
return ENTITY_STORE_DATA_SCHEMA(entity_data) # type: ignore[no-any-return]
|
||||
|
@ -22,5 +22,5 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["lmcloud"],
|
||||
"requirements": ["lmcloud==1.2.2"]
|
||||
"requirements": ["lmcloud==1.2.3"]
|
||||
}
|
||||
|
@ -48,8 +48,8 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["aiolifx", "aiolifx_effects", "bitstring"],
|
||||
"requirements": [
|
||||
"aiolifx==1.0.9",
|
||||
"aiolifx==1.1.1",
|
||||
"aiolifx-effects==0.3.2",
|
||||
"aiolifx-themes==0.5.0"
|
||||
"aiolifx-themes==0.5.5"
|
||||
]
|
||||
}
|
||||
|
@ -208,10 +208,7 @@ class LyricClimate(LyricDeviceEntity, ClimateEntity):
|
||||
if LYRIC_HVAC_MODE_COOL in device.allowed_modes:
|
||||
self._attr_hvac_modes.append(HVACMode.COOL)
|
||||
|
||||
if (
|
||||
LYRIC_HVAC_MODE_HEAT in device.allowed_modes
|
||||
and LYRIC_HVAC_MODE_COOL in device.allowed_modes
|
||||
):
|
||||
if LYRIC_HVAC_MODE_HEAT_COOL in device.allowed_modes:
|
||||
self._attr_hvac_modes.append(HVACMode.HEAT_COOL)
|
||||
|
||||
# Setup supported features
|
||||
|
@ -26,7 +26,13 @@ async def async_setup_entry(
|
||||
coordinator = hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR]
|
||||
|
||||
for blind in motion_gateway.device_list.values():
|
||||
if blind.limit_status == LimitStatus.Limit3Detected.name:
|
||||
if blind.limit_status in (
|
||||
LimitStatus.Limit3Detected.name,
|
||||
{
|
||||
"T": LimitStatus.Limit3Detected.name,
|
||||
"B": LimitStatus.Limit3Detected.name,
|
||||
},
|
||||
):
|
||||
entities.append(MotionGoFavoriteButton(coordinator, blind))
|
||||
entities.append(MotionSetFavoriteButton(coordinator, blind))
|
||||
|
||||
|
@ -21,5 +21,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/motion_blinds",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["motionblinds"],
|
||||
"requirements": ["motionblinds==0.6.24"]
|
||||
"requirements": ["motionblinds==0.6.25"]
|
||||
}
|
||||
|
@ -20,5 +20,5 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["google_nest_sdm"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["google-nest-sdm==5.0.0"]
|
||||
"requirements": ["google-nest-sdm==5.0.1"]
|
||||
}
|
||||
|
@ -28,7 +28,7 @@ class RenaultBinarySensorEntityDescription(
|
||||
"""Class describing Renault binary sensor entities."""
|
||||
|
||||
on_key: str
|
||||
on_value: StateType
|
||||
on_value: StateType | list[StateType]
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
@ -58,6 +58,9 @@ class RenaultBinarySensor(
|
||||
"""Return true if the binary sensor is on."""
|
||||
if (data := self._get_data_attr(self.entity_description.on_key)) is None:
|
||||
return None
|
||||
|
||||
if isinstance(self.entity_description.on_value, list):
|
||||
return data in self.entity_description.on_value
|
||||
return data == self.entity_description.on_value
|
||||
|
||||
|
||||
@ -68,7 +71,10 @@ BINARY_SENSOR_TYPES: tuple[RenaultBinarySensorEntityDescription, ...] = tuple(
|
||||
coordinator="battery",
|
||||
device_class=BinarySensorDeviceClass.PLUG,
|
||||
on_key="plugStatus",
|
||||
on_value=PlugState.PLUGGED.value,
|
||||
on_value=[
|
||||
PlugState.PLUGGED.value,
|
||||
PlugState.PLUGGED_WAITING_FOR_CHARGE.value,
|
||||
],
|
||||
),
|
||||
RenaultBinarySensorEntityDescription(
|
||||
key="charging",
|
||||
@ -104,13 +110,13 @@ BINARY_SENSOR_TYPES: tuple[RenaultBinarySensorEntityDescription, ...] = tuple(
|
||||
]
|
||||
+ [
|
||||
RenaultBinarySensorEntityDescription(
|
||||
key=f"{door.replace(' ','_').lower()}_door_status",
|
||||
key=f"{door.replace(' ', '_').lower()}_door_status",
|
||||
coordinator="lock_status",
|
||||
# On means open, Off means closed
|
||||
device_class=BinarySensorDeviceClass.DOOR,
|
||||
on_key=f"doorStatus{door.replace(' ','')}",
|
||||
on_key=f"doorStatus{door.replace(' ', '')}",
|
||||
on_value="open",
|
||||
translation_key=f"{door.lower().replace(' ','_')}_door_status",
|
||||
translation_key=f"{door.lower().replace(' ', '_')}_door_status",
|
||||
)
|
||||
for door in ("Rear Left", "Rear Right", "Driver", "Passenger")
|
||||
],
|
||||
|
@ -197,7 +197,13 @@ SENSOR_TYPES: tuple[RenaultSensorEntityDescription[Any], ...] = (
|
||||
translation_key="plug_state",
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
entity_class=RenaultSensor[KamereonVehicleBatteryStatusData],
|
||||
options=["unplugged", "plugged", "plug_error", "plug_unknown"],
|
||||
options=[
|
||||
"unplugged",
|
||||
"plugged",
|
||||
"plugged_waiting_for_charge",
|
||||
"plug_error",
|
||||
"plug_unknown",
|
||||
],
|
||||
value_lambda=_get_plug_state_formatted,
|
||||
),
|
||||
RenaultSensorEntityDescription(
|
||||
|
@ -141,6 +141,7 @@
|
||||
"state": {
|
||||
"unplugged": "Unplugged",
|
||||
"plugged": "Plugged in",
|
||||
"plugged_waiting_for_charge": "Plugged in, waiting for charge",
|
||||
"plug_error": "Plug error",
|
||||
"plug_unknown": "Plug unknown"
|
||||
}
|
||||
|
@ -7,5 +7,5 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["aiorussound"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["aiorussound==3.0.4"]
|
||||
"requirements": ["aiorussound==3.0.5"]
|
||||
}
|
||||
|
@ -5,5 +5,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/russound_rnet",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["russound"],
|
||||
"requirements": ["russound==0.1.9"]
|
||||
"requirements": ["russound==0.2.0"]
|
||||
}
|
||||
|
@ -96,7 +96,13 @@ class RussoundRNETDevice(MediaPlayerEntity):
|
||||
# Updated this function to make a single call to get_zone_info, so that
|
||||
# with a single call we can get On/Off, Volume and Source, reducing the
|
||||
# amount of traffic and speeding up the update process.
|
||||
ret = self._russ.get_zone_info(self._controller_id, self._zone_id, 4)
|
||||
try:
|
||||
ret = self._russ.get_zone_info(self._controller_id, self._zone_id, 4)
|
||||
except BrokenPipeError:
|
||||
_LOGGER.error("Broken Pipe Error, trying to reconnect to Russound RNET")
|
||||
self._russ.connect()
|
||||
ret = self._russ.get_zone_info(self._controller_id, self._zone_id, 4)
|
||||
|
||||
_LOGGER.debug("ret= %s", ret)
|
||||
if ret is not None:
|
||||
_LOGGER.debug(
|
||||
|
@ -42,5 +42,4 @@ class SchlageEntity(CoordinatorEntity[SchlageDataUpdateCoordinator]):
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return if entity is available."""
|
||||
# When is_locked is None the lock is unavailable.
|
||||
return super().available and self._lock.is_locked is not None
|
||||
return super().available and self.device_id in self.coordinator.data.locks
|
||||
|
@ -42,8 +42,9 @@ class SchlageLockEntity(SchlageEntity, LockEntity):
|
||||
@callback
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Handle updated data from the coordinator."""
|
||||
self._update_attrs()
|
||||
return super()._handle_coordinator_update()
|
||||
if self.device_id in self.coordinator.data.locks:
|
||||
self._update_attrs()
|
||||
super()._handle_coordinator_update()
|
||||
|
||||
def _update_attrs(self) -> None:
|
||||
"""Update our internal state attributes."""
|
||||
|
@ -64,5 +64,6 @@ class SchlageBatterySensor(SchlageEntity, SensorEntity):
|
||||
@callback
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Handle updated data from the coordinator."""
|
||||
self._attr_native_value = getattr(self._lock, self.entity_description.key)
|
||||
return super()._handle_coordinator_update()
|
||||
if self.device_id in self.coordinator.data.locks:
|
||||
self._attr_native_value = getattr(self._lock, self.entity_description.key)
|
||||
super()._handle_coordinator_update()
|
||||
|
@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/sfr_box",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["sfrbox-api==0.0.10"]
|
||||
"requirements": ["sfrbox-api==0.0.11"]
|
||||
}
|
||||
|
@ -94,8 +94,13 @@ class SmlightConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
mac = discovery_info.properties.get("mac")
|
||||
# fallback for legacy firmware
|
||||
if mac is None:
|
||||
info = await self.client.get_info()
|
||||
try:
|
||||
info = await self.client.get_info()
|
||||
except SmlightConnectionError:
|
||||
# User is likely running unsupported ESPHome firmware
|
||||
return self.async_abort(reason="cannot_connect")
|
||||
mac = info.MAC
|
||||
|
||||
await self.async_set_unique_id(format_mac(mac))
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
|
@ -84,6 +84,7 @@ REPEAT_TO_SONOS = {
|
||||
SONOS_TO_REPEAT = {meaning: mode for mode, meaning in REPEAT_TO_SONOS.items()}
|
||||
|
||||
UPNP_ERRORS_TO_IGNORE = ["701", "711", "712"]
|
||||
ANNOUNCE_NOT_SUPPORTED_ERRORS: list[str] = ["globalError"]
|
||||
|
||||
SERVICE_SNAPSHOT = "snapshot"
|
||||
SERVICE_RESTORE = "restore"
|
||||
@ -556,11 +557,24 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity):
|
||||
) from exc
|
||||
if response.get("success"):
|
||||
return
|
||||
raise HomeAssistantError(
|
||||
translation_domain=SONOS_DOMAIN,
|
||||
translation_key="announce_media_error",
|
||||
translation_placeholders={"media_id": media_id, "response": response},
|
||||
)
|
||||
if response.get("type") in ANNOUNCE_NOT_SUPPORTED_ERRORS:
|
||||
# If the speaker does not support announce do not raise and
|
||||
# fall through to_play_media to play the clip directly.
|
||||
_LOGGER.debug(
|
||||
"Speaker %s does not support announce, media_id %s response %s",
|
||||
self.speaker.zone_name,
|
||||
media_id,
|
||||
response,
|
||||
)
|
||||
else:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=SONOS_DOMAIN,
|
||||
translation_key="announce_media_error",
|
||||
translation_placeholders={
|
||||
"media_id": media_id,
|
||||
"response": response,
|
||||
},
|
||||
)
|
||||
|
||||
if spotify.is_spotify_media_type(media_type):
|
||||
media_type = spotify.resolve_spotify_media_type(media_type)
|
||||
|
@ -26,6 +26,7 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
CONF_ALIAS,
|
||||
CONF_AUTHENTICATION,
|
||||
CONF_DEVICE,
|
||||
CONF_HOST,
|
||||
CONF_MAC,
|
||||
CONF_MODEL,
|
||||
@ -44,8 +45,12 @@ from homeassistant.helpers.event import async_track_time_interval
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import (
|
||||
CONF_AES_KEYS,
|
||||
CONF_CONFIG_ENTRY_MINOR_VERSION,
|
||||
CONF_CONNECTION_PARAMETERS,
|
||||
CONF_CREDENTIALS_HASH,
|
||||
CONF_DEVICE_CONFIG,
|
||||
CONF_USES_HTTP,
|
||||
CONNECT_TIMEOUT,
|
||||
DISCOVERY_TIMEOUT,
|
||||
DOMAIN,
|
||||
@ -85,9 +90,7 @@ def async_trigger_discovery(
|
||||
CONF_ALIAS: device.alias or mac_alias(device.mac),
|
||||
CONF_HOST: device.host,
|
||||
CONF_MAC: formatted_mac,
|
||||
CONF_DEVICE_CONFIG: device.config.to_dict(
|
||||
exclude_credentials=True,
|
||||
),
|
||||
CONF_DEVICE: device,
|
||||
},
|
||||
)
|
||||
|
||||
@ -136,25 +139,27 @@ async def async_setup_entry(hass: HomeAssistant, entry: TPLinkConfigEntry) -> bo
|
||||
host: str = entry.data[CONF_HOST]
|
||||
credentials = await get_credentials(hass)
|
||||
entry_credentials_hash = entry.data.get(CONF_CREDENTIALS_HASH)
|
||||
entry_use_http = entry.data.get(CONF_USES_HTTP, False)
|
||||
entry_aes_keys = entry.data.get(CONF_AES_KEYS)
|
||||
|
||||
config: DeviceConfig | None = None
|
||||
if config_dict := entry.data.get(CONF_DEVICE_CONFIG):
|
||||
conn_params: Device.ConnectionParameters | None = None
|
||||
if conn_params_dict := entry.data.get(CONF_CONNECTION_PARAMETERS):
|
||||
try:
|
||||
config = DeviceConfig.from_dict(config_dict)
|
||||
conn_params = Device.ConnectionParameters.from_dict(conn_params_dict)
|
||||
except KasaException:
|
||||
_LOGGER.warning(
|
||||
"Invalid connection type dict for %s: %s", host, config_dict
|
||||
"Invalid connection parameters dict for %s: %s", host, conn_params_dict
|
||||
)
|
||||
|
||||
if not config:
|
||||
config = DeviceConfig(host)
|
||||
else:
|
||||
config.host = host
|
||||
|
||||
config.timeout = CONNECT_TIMEOUT
|
||||
if config.uses_http is True:
|
||||
config.http_client = create_async_tplink_clientsession(hass)
|
||||
|
||||
client = create_async_tplink_clientsession(hass) if entry_use_http else None
|
||||
config = DeviceConfig(
|
||||
host,
|
||||
timeout=CONNECT_TIMEOUT,
|
||||
http_client=client,
|
||||
aes_keys=entry_aes_keys,
|
||||
)
|
||||
if conn_params:
|
||||
config.connection_type = conn_params
|
||||
# If we have in memory credentials use them otherwise check for credentials_hash
|
||||
if credentials:
|
||||
config.credentials = credentials
|
||||
@ -173,14 +178,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: TPLinkConfigEntry) -> bo
|
||||
raise ConfigEntryNotReady from ex
|
||||
|
||||
device_credentials_hash = device.credentials_hash
|
||||
device_config_dict = device.config.to_dict(exclude_credentials=True)
|
||||
# Do not store the credentials hash inside the device_config
|
||||
device_config_dict.pop(CONF_CREDENTIALS_HASH, None)
|
||||
|
||||
# We not need to update the connection parameters or the use_http here
|
||||
# because if they were wrong we would have failed to connect.
|
||||
# Discovery will update those if necessary.
|
||||
updates: dict[str, Any] = {}
|
||||
if device_credentials_hash and device_credentials_hash != entry_credentials_hash:
|
||||
updates[CONF_CREDENTIALS_HASH] = device_credentials_hash
|
||||
if device_config_dict != config_dict:
|
||||
updates[CONF_DEVICE_CONFIG] = device_config_dict
|
||||
if entry_aes_keys != device.config.aes_keys:
|
||||
updates[CONF_AES_KEYS] = device.config.aes_keys
|
||||
if entry.data.get(CONF_ALIAS) != device.alias:
|
||||
updates[CONF_ALIAS] = device.alias
|
||||
if entry.data.get(CONF_MODEL) != device.model:
|
||||
@ -307,12 +313,20 @@ def _device_id_is_mac_or_none(mac: str, device_ids: Iterable[str]) -> str | None
|
||||
|
||||
async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
|
||||
"""Migrate old entry."""
|
||||
version = config_entry.version
|
||||
minor_version = config_entry.minor_version
|
||||
entry_version = config_entry.version
|
||||
entry_minor_version = config_entry.minor_version
|
||||
# having a condition to check for the current version allows
|
||||
# tests to be written per migration step.
|
||||
config_flow_minor_version = CONF_CONFIG_ENTRY_MINOR_VERSION
|
||||
|
||||
_LOGGER.debug("Migrating from version %s.%s", version, minor_version)
|
||||
|
||||
if version == 1 and minor_version < 3:
|
||||
new_minor_version = 3
|
||||
if (
|
||||
entry_version == 1
|
||||
and entry_minor_version < new_minor_version <= config_flow_minor_version
|
||||
):
|
||||
_LOGGER.debug(
|
||||
"Migrating from version %s.%s", entry_version, entry_minor_version
|
||||
)
|
||||
# Previously entities on child devices added themselves to the parent
|
||||
# device and set their device id as identifiers along with mac
|
||||
# as a connection which creates a single device entry linked by all
|
||||
@ -359,12 +373,19 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
|
||||
new_identifiers,
|
||||
)
|
||||
|
||||
minor_version = 3
|
||||
hass.config_entries.async_update_entry(config_entry, minor_version=3)
|
||||
hass.config_entries.async_update_entry(
|
||||
config_entry, minor_version=new_minor_version
|
||||
)
|
||||
|
||||
_LOGGER.debug("Migration to version %s.%s complete", version, minor_version)
|
||||
_LOGGER.debug(
|
||||
"Migration to version %s.%s complete", entry_version, new_minor_version
|
||||
)
|
||||
|
||||
if version == 1 and minor_version == 3:
|
||||
new_minor_version = 4
|
||||
if (
|
||||
entry_version == 1
|
||||
and entry_minor_version < new_minor_version <= config_flow_minor_version
|
||||
):
|
||||
# credentials_hash stored in the device_config should be moved to data.
|
||||
updates: dict[str, Any] = {}
|
||||
if config_dict := config_entry.data.get(CONF_DEVICE_CONFIG):
|
||||
@ -372,15 +393,44 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
|
||||
if credentials_hash := config_dict.pop(CONF_CREDENTIALS_HASH, None):
|
||||
updates[CONF_CREDENTIALS_HASH] = credentials_hash
|
||||
updates[CONF_DEVICE_CONFIG] = config_dict
|
||||
minor_version = 4
|
||||
hass.config_entries.async_update_entry(
|
||||
config_entry,
|
||||
data={
|
||||
**config_entry.data,
|
||||
**updates,
|
||||
},
|
||||
minor_version=minor_version,
|
||||
minor_version=new_minor_version,
|
||||
)
|
||||
_LOGGER.debug(
|
||||
"Migration to version %s.%s complete", entry_version, new_minor_version
|
||||
)
|
||||
_LOGGER.debug("Migration to version %s.%s complete", version, minor_version)
|
||||
|
||||
new_minor_version = 5
|
||||
if (
|
||||
entry_version == 1
|
||||
and entry_minor_version < new_minor_version <= config_flow_minor_version
|
||||
):
|
||||
# complete device config no longer to be stored, only required
|
||||
# attributes like connection parameters and aes_keys
|
||||
updates = {}
|
||||
entry_data = {
|
||||
k: v for k, v in config_entry.data.items() if k != CONF_DEVICE_CONFIG
|
||||
}
|
||||
if config_dict := config_entry.data.get(CONF_DEVICE_CONFIG):
|
||||
assert isinstance(config_dict, dict)
|
||||
if connection_parameters := config_dict.get("connection_type"):
|
||||
updates[CONF_CONNECTION_PARAMETERS] = connection_parameters
|
||||
if (use_http := config_dict.get(CONF_USES_HTTP)) is not None:
|
||||
updates[CONF_USES_HTTP] = use_http
|
||||
hass.config_entries.async_update_entry(
|
||||
config_entry,
|
||||
data={
|
||||
**entry_data,
|
||||
**updates,
|
||||
},
|
||||
minor_version=new_minor_version,
|
||||
)
|
||||
_LOGGER.debug(
|
||||
"Migration to version %s.%s complete", entry_version, new_minor_version
|
||||
)
|
||||
return True
|
||||
|
@ -46,9 +46,11 @@ from . import (
|
||||
set_credentials,
|
||||
)
|
||||
from .const import (
|
||||
CONF_CONNECTION_TYPE,
|
||||
CONF_AES_KEYS,
|
||||
CONF_CONFIG_ENTRY_MINOR_VERSION,
|
||||
CONF_CONNECTION_PARAMETERS,
|
||||
CONF_CREDENTIALS_HASH,
|
||||
CONF_DEVICE_CONFIG,
|
||||
CONF_USES_HTTP,
|
||||
CONNECT_TIMEOUT,
|
||||
DOMAIN,
|
||||
)
|
||||
@ -64,7 +66,7 @@ class TPLinkConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for tplink."""
|
||||
|
||||
VERSION = 1
|
||||
MINOR_VERSION = 4
|
||||
MINOR_VERSION = CONF_CONFIG_ENTRY_MINOR_VERSION
|
||||
reauth_entry: ConfigEntry | None = None
|
||||
|
||||
def __init__(self) -> None:
|
||||
@ -87,38 +89,43 @@ class TPLinkConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
return await self._async_handle_discovery(
|
||||
discovery_info[CONF_HOST],
|
||||
discovery_info[CONF_MAC],
|
||||
discovery_info[CONF_DEVICE_CONFIG],
|
||||
discovery_info[CONF_DEVICE],
|
||||
)
|
||||
|
||||
@callback
|
||||
def _get_config_updates(
|
||||
self, entry: ConfigEntry, host: str, config: dict
|
||||
self, entry: ConfigEntry, host: str, device: Device | None
|
||||
) -> dict | None:
|
||||
"""Return updates if the host or device config has changed."""
|
||||
entry_data = entry.data
|
||||
entry_config_dict = entry_data.get(CONF_DEVICE_CONFIG)
|
||||
if entry_config_dict == config and entry_data[CONF_HOST] == host:
|
||||
updates: dict[str, Any] = {}
|
||||
new_connection_params = False
|
||||
if entry_data[CONF_HOST] != host:
|
||||
updates[CONF_HOST] = host
|
||||
if device:
|
||||
device_conn_params_dict = device.config.connection_type.to_dict()
|
||||
entry_conn_params_dict = entry_data.get(CONF_CONNECTION_PARAMETERS)
|
||||
if device_conn_params_dict != entry_conn_params_dict:
|
||||
new_connection_params = True
|
||||
updates[CONF_CONNECTION_PARAMETERS] = device_conn_params_dict
|
||||
updates[CONF_USES_HTTP] = device.config.uses_http
|
||||
if not updates:
|
||||
return None
|
||||
updates = {**entry.data, CONF_DEVICE_CONFIG: config, CONF_HOST: host}
|
||||
updates = {**entry.data, **updates}
|
||||
# If the connection parameters have changed the credentials_hash will be invalid.
|
||||
if (
|
||||
entry_config_dict
|
||||
and isinstance(entry_config_dict, dict)
|
||||
and entry_config_dict.get(CONF_CONNECTION_TYPE)
|
||||
!= config.get(CONF_CONNECTION_TYPE)
|
||||
):
|
||||
if new_connection_params:
|
||||
updates.pop(CONF_CREDENTIALS_HASH, None)
|
||||
_LOGGER.debug(
|
||||
"Connection type changed for %s from %s to: %s",
|
||||
host,
|
||||
entry_config_dict.get(CONF_CONNECTION_TYPE),
|
||||
config.get(CONF_CONNECTION_TYPE),
|
||||
entry_conn_params_dict,
|
||||
device_conn_params_dict,
|
||||
)
|
||||
return updates
|
||||
|
||||
@callback
|
||||
def _update_config_if_entry_in_setup_error(
|
||||
self, entry: ConfigEntry, host: str, config: dict
|
||||
self, entry: ConfigEntry, host: str, device: Device | None
|
||||
) -> ConfigFlowResult | None:
|
||||
"""If discovery encounters a device that is in SETUP_ERROR or SETUP_RETRY update the device config."""
|
||||
if entry.state not in (
|
||||
@ -126,7 +133,7 @@ class TPLinkConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
ConfigEntryState.SETUP_RETRY,
|
||||
):
|
||||
return None
|
||||
if updates := self._get_config_updates(entry, host, config):
|
||||
if updates := self._get_config_updates(entry, host, device):
|
||||
return self.async_update_reload_and_abort(
|
||||
entry,
|
||||
data=updates,
|
||||
@ -135,19 +142,15 @@ class TPLinkConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
return None
|
||||
|
||||
async def _async_handle_discovery(
|
||||
self, host: str, formatted_mac: str, config: dict | None = None
|
||||
self, host: str, formatted_mac: str, device: Device | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle any discovery."""
|
||||
current_entry = await self.async_set_unique_id(
|
||||
formatted_mac, raise_on_progress=False
|
||||
)
|
||||
if (
|
||||
config
|
||||
and current_entry
|
||||
and (
|
||||
result := self._update_config_if_entry_in_setup_error(
|
||||
current_entry, host, config
|
||||
)
|
||||
if current_entry and (
|
||||
result := self._update_config_if_entry_in_setup_error(
|
||||
current_entry, host, device
|
||||
)
|
||||
):
|
||||
return result
|
||||
@ -159,9 +162,13 @@ class TPLinkConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
return self.async_abort(reason="already_in_progress")
|
||||
credentials = await get_credentials(self.hass)
|
||||
try:
|
||||
await self._async_try_discover_and_update(
|
||||
host, credentials, raise_on_progress=True
|
||||
)
|
||||
if device:
|
||||
self._discovered_device = device
|
||||
await self._async_try_connect(device, credentials)
|
||||
else:
|
||||
await self._async_try_discover_and_update(
|
||||
host, credentials, raise_on_progress=True
|
||||
)
|
||||
except AuthenticationError:
|
||||
return await self.async_step_discovery_auth_confirm()
|
||||
except KasaException:
|
||||
@ -381,14 +388,15 @@ class TPLinkConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
# This is only ever called after a successful device update so we know that
|
||||
# the credential_hash is correct and should be saved.
|
||||
self._abort_if_unique_id_configured(updates={CONF_HOST: device.host})
|
||||
data = {
|
||||
data: dict[str, Any] = {
|
||||
CONF_HOST: device.host,
|
||||
CONF_ALIAS: device.alias,
|
||||
CONF_MODEL: device.model,
|
||||
CONF_DEVICE_CONFIG: device.config.to_dict(
|
||||
exclude_credentials=True,
|
||||
),
|
||||
CONF_CONNECTION_PARAMETERS: device.config.connection_type.to_dict(),
|
||||
CONF_USES_HTTP: device.config.uses_http,
|
||||
}
|
||||
if device.config.aes_keys:
|
||||
data[CONF_AES_KEYS] = device.config.aes_keys
|
||||
if device.credentials_hash:
|
||||
data[CONF_CREDENTIALS_HASH] = device.credentials_hash
|
||||
return self.async_create_entry(
|
||||
@ -494,8 +502,7 @@ class TPLinkConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
placeholders["error"] = str(ex)
|
||||
else:
|
||||
await set_credentials(self.hass, username, password)
|
||||
config = device.config.to_dict(exclude_credentials=True)
|
||||
if updates := self._get_config_updates(reauth_entry, host, config):
|
||||
if updates := self._get_config_updates(reauth_entry, host, device):
|
||||
self.hass.config_entries.async_update_entry(
|
||||
reauth_entry, data=updates
|
||||
)
|
||||
|
@ -21,7 +21,11 @@ ATTR_TOTAL_ENERGY_KWH: Final = "total_energy_kwh"
|
||||
|
||||
CONF_DEVICE_CONFIG: Final = "device_config"
|
||||
CONF_CREDENTIALS_HASH: Final = "credentials_hash"
|
||||
CONF_CONNECTION_TYPE: Final = "connection_type"
|
||||
CONF_CONNECTION_PARAMETERS: Final = "connection_parameters"
|
||||
CONF_USES_HTTP: Final = "uses_http"
|
||||
CONF_AES_KEYS: Final = "aes_keys"
|
||||
|
||||
CONF_CONFIG_ENTRY_MINOR_VERSION: Final = 5
|
||||
|
||||
PLATFORMS: Final = [
|
||||
Platform.BINARY_SENSOR,
|
||||
|
@ -301,5 +301,5 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["kasa"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["python-kasa[speedups]==0.7.2"]
|
||||
"requirements": ["python-kasa[speedups]==0.7.3"]
|
||||
}
|
||||
|
@ -13,5 +13,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/yale",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["socketio", "engineio", "yalexs"],
|
||||
"requirements": ["yalexs==8.6.3", "yalexs-ble==2.4.3"]
|
||||
"requirements": ["yalexs==8.6.4", "yalexs-ble==2.4.3"]
|
||||
}
|
||||
|
@ -154,10 +154,15 @@ class YaleDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
except YALE_BASE_ERRORS as error:
|
||||
raise UpdateFailed from error
|
||||
|
||||
cycle = data.cycle["data"] if data.cycle else None
|
||||
status = data.status["data"] if data.status else None
|
||||
online = data.online["data"] if data.online else None
|
||||
panel_info = data.panel_info["data"] if data.panel_info else None
|
||||
|
||||
return {
|
||||
"arm_status": arm_status,
|
||||
"cycle": data.cycle,
|
||||
"status": data.status,
|
||||
"online": data.online,
|
||||
"panel_info": data.panel_info,
|
||||
"cycle": cycle,
|
||||
"status": status,
|
||||
"online": online,
|
||||
"panel_info": panel_info,
|
||||
}
|
||||
|
@ -1166,7 +1166,8 @@ CONF_ZHA_OPTIONS_SCHEMA = vol.Schema(
|
||||
CONF_CONSIDER_UNAVAILABLE_BATTERY,
|
||||
default=CONF_DEFAULT_CONSIDER_UNAVAILABLE_BATTERY,
|
||||
): cv.positive_int,
|
||||
}
|
||||
},
|
||||
extra=vol.REMOVE_EXTRA,
|
||||
)
|
||||
|
||||
CONF_ZHA_ALARM_SCHEMA = vol.Schema(
|
||||
|
@ -21,7 +21,7 @@
|
||||
"zha",
|
||||
"universal_silabs_flasher"
|
||||
],
|
||||
"requirements": ["universal-silabs-flasher==0.0.22", "zha==0.0.32"],
|
||||
"requirements": ["universal-silabs-flasher==0.0.22", "zha==0.0.33"],
|
||||
"usb": [
|
||||
{
|
||||
"vid": "10C4",
|
||||
|
@ -24,7 +24,7 @@ if TYPE_CHECKING:
|
||||
APPLICATION_NAME: Final = "HomeAssistant"
|
||||
MAJOR_VERSION: Final = 2024
|
||||
MINOR_VERSION: Final = 9
|
||||
PATCH_VERSION: Final = "1"
|
||||
PATCH_VERSION: Final = "2"
|
||||
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
||||
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
|
||||
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0)
|
||||
|
@ -31,7 +31,7 @@ habluetooth==3.4.0
|
||||
hass-nabucasa==0.81.1
|
||||
hassil==1.7.4
|
||||
home-assistant-bluetooth==1.12.2
|
||||
home-assistant-frontend==20240906.0
|
||||
home-assistant-frontend==20240909.1
|
||||
home-assistant-intents==2024.9.4
|
||||
httpx==0.27.0
|
||||
ifaddr==0.2.0
|
||||
@ -185,3 +185,9 @@ tuf>=4.0.0
|
||||
|
||||
# https://github.com/jd/tenacity/issues/471
|
||||
tenacity!=8.4.0
|
||||
|
||||
# pyasn1.compat.octets was removed in pyasn1 0.6.1 and breaks some integrations
|
||||
# and tests that import it directly
|
||||
# https://github.com/pyasn1/pyasn1/pull/60
|
||||
# https://github.com/lextudio/pysnmp/issues/114
|
||||
pyasn1==0.6.0
|
||||
|
@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "homeassistant"
|
||||
version = "2024.9.1"
|
||||
version = "2024.9.2"
|
||||
license = {text = "Apache-2.0"}
|
||||
description = "Open-source home automation platform running on Python 3."
|
||||
readme = "README.rst"
|
||||
|
@ -273,10 +273,10 @@ aiokef==0.2.16
|
||||
aiolifx-effects==0.3.2
|
||||
|
||||
# homeassistant.components.lifx
|
||||
aiolifx-themes==0.5.0
|
||||
aiolifx-themes==0.5.5
|
||||
|
||||
# homeassistant.components.lifx
|
||||
aiolifx==1.0.9
|
||||
aiolifx==1.1.1
|
||||
|
||||
# homeassistant.components.livisi
|
||||
aiolivisi==0.0.19
|
||||
@ -350,7 +350,7 @@ aioridwell==2024.01.0
|
||||
aioruckus==0.41
|
||||
|
||||
# homeassistant.components.russound_rio
|
||||
aiorussound==3.0.4
|
||||
aiorussound==3.0.5
|
||||
|
||||
# homeassistant.components.ruuvi_gateway
|
||||
aioruuvigateway==0.1.0
|
||||
@ -992,7 +992,7 @@ google-cloud-texttospeech==2.16.3
|
||||
google-generativeai==0.6.0
|
||||
|
||||
# homeassistant.components.nest
|
||||
google-nest-sdm==5.0.0
|
||||
google-nest-sdm==5.0.1
|
||||
|
||||
# homeassistant.components.google_travel_time
|
||||
googlemaps==2.5.1
|
||||
@ -1007,7 +1007,7 @@ gotailwind==0.2.3
|
||||
govee-ble==0.40.0
|
||||
|
||||
# homeassistant.components.govee_light_local
|
||||
govee-local-api==1.5.1
|
||||
govee-local-api==1.5.2
|
||||
|
||||
# homeassistant.components.remote_rpi_gpio
|
||||
gpiozero==1.6.2
|
||||
@ -1102,7 +1102,7 @@ hole==0.8.0
|
||||
holidays==0.56
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20240906.0
|
||||
home-assistant-frontend==20240909.1
|
||||
|
||||
# homeassistant.components.conversation
|
||||
home-assistant-intents==2024.9.4
|
||||
@ -1225,7 +1225,7 @@ kiwiki-client==0.1.1
|
||||
knocki==0.3.1
|
||||
|
||||
# homeassistant.components.knx
|
||||
knx-frontend==2024.9.4.64538
|
||||
knx-frontend==2024.9.10.221729
|
||||
|
||||
# homeassistant.components.konnected
|
||||
konnected==1.2.0
|
||||
@ -1282,7 +1282,7 @@ linear-garage-door==0.2.9
|
||||
linode-api==4.1.9b1
|
||||
|
||||
# homeassistant.components.lamarzocco
|
||||
lmcloud==1.2.2
|
||||
lmcloud==1.2.3
|
||||
|
||||
# homeassistant.components.google_maps
|
||||
locationsharinglib==5.0.1
|
||||
@ -1366,7 +1366,7 @@ monzopy==1.3.2
|
||||
mopeka-iot-ble==0.8.0
|
||||
|
||||
# homeassistant.components.motion_blinds
|
||||
motionblinds==0.6.24
|
||||
motionblinds==0.6.25
|
||||
|
||||
# homeassistant.components.motionblinds_ble
|
||||
motionblindsble==0.1.1
|
||||
@ -2313,7 +2313,7 @@ python-join-api==0.0.9
|
||||
python-juicenet==1.1.0
|
||||
|
||||
# homeassistant.components.tplink
|
||||
python-kasa[speedups]==0.7.2
|
||||
python-kasa[speedups]==0.7.3
|
||||
|
||||
# homeassistant.components.linkplay
|
||||
python-linkplay==0.0.9
|
||||
@ -2546,7 +2546,7 @@ rpi-bad-power==0.1.0
|
||||
rtsp-to-webrtc==0.5.1
|
||||
|
||||
# homeassistant.components.russound_rnet
|
||||
russound==0.1.9
|
||||
russound==0.2.0
|
||||
|
||||
# homeassistant.components.ruuvitag_ble
|
||||
ruuvitag-ble==0.1.2
|
||||
@ -2595,7 +2595,7 @@ sensorpush-ble==1.6.2
|
||||
sentry-sdk==1.40.3
|
||||
|
||||
# homeassistant.components.sfr_box
|
||||
sfrbox-api==0.0.10
|
||||
sfrbox-api==0.0.11
|
||||
|
||||
# homeassistant.components.sharkiq
|
||||
sharkiq==1.0.2
|
||||
@ -2976,7 +2976,7 @@ yalexs-ble==2.4.3
|
||||
|
||||
# homeassistant.components.august
|
||||
# homeassistant.components.yale
|
||||
yalexs==8.6.3
|
||||
yalexs==8.6.4
|
||||
|
||||
# homeassistant.components.yeelight
|
||||
yeelight==0.7.14
|
||||
@ -3009,7 +3009,7 @@ zeroconf==0.133.0
|
||||
zeversolar==0.3.1
|
||||
|
||||
# homeassistant.components.zha
|
||||
zha==0.0.32
|
||||
zha==0.0.33
|
||||
|
||||
# homeassistant.components.zhong_hong
|
||||
zhong-hong-hvac==1.0.12
|
||||
|
@ -255,10 +255,10 @@ aiokafka==0.10.0
|
||||
aiolifx-effects==0.3.2
|
||||
|
||||
# homeassistant.components.lifx
|
||||
aiolifx-themes==0.5.0
|
||||
aiolifx-themes==0.5.5
|
||||
|
||||
# homeassistant.components.lifx
|
||||
aiolifx==1.0.9
|
||||
aiolifx==1.1.1
|
||||
|
||||
# homeassistant.components.livisi
|
||||
aiolivisi==0.0.19
|
||||
@ -332,7 +332,7 @@ aioridwell==2024.01.0
|
||||
aioruckus==0.41
|
||||
|
||||
# homeassistant.components.russound_rio
|
||||
aiorussound==3.0.4
|
||||
aiorussound==3.0.5
|
||||
|
||||
# homeassistant.components.ruuvi_gateway
|
||||
aioruuvigateway==0.1.0
|
||||
@ -839,7 +839,7 @@ google-cloud-pubsub==2.13.11
|
||||
google-generativeai==0.6.0
|
||||
|
||||
# homeassistant.components.nest
|
||||
google-nest-sdm==5.0.0
|
||||
google-nest-sdm==5.0.1
|
||||
|
||||
# homeassistant.components.google_travel_time
|
||||
googlemaps==2.5.1
|
||||
@ -851,7 +851,7 @@ gotailwind==0.2.3
|
||||
govee-ble==0.40.0
|
||||
|
||||
# homeassistant.components.govee_light_local
|
||||
govee-local-api==1.5.1
|
||||
govee-local-api==1.5.2
|
||||
|
||||
# homeassistant.components.gpsd
|
||||
gps3==0.33.3
|
||||
@ -925,7 +925,7 @@ hole==0.8.0
|
||||
holidays==0.56
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20240906.0
|
||||
home-assistant-frontend==20240909.1
|
||||
|
||||
# homeassistant.components.conversation
|
||||
home-assistant-intents==2024.9.4
|
||||
@ -1021,7 +1021,7 @@ kegtron-ble==0.4.0
|
||||
knocki==0.3.1
|
||||
|
||||
# homeassistant.components.knx
|
||||
knx-frontend==2024.9.4.64538
|
||||
knx-frontend==2024.9.10.221729
|
||||
|
||||
# homeassistant.components.konnected
|
||||
konnected==1.2.0
|
||||
@ -1060,7 +1060,7 @@ libsoundtouch==0.8
|
||||
linear-garage-door==0.2.9
|
||||
|
||||
# homeassistant.components.lamarzocco
|
||||
lmcloud==1.2.2
|
||||
lmcloud==1.2.3
|
||||
|
||||
# homeassistant.components.london_underground
|
||||
london-tube-status==0.5
|
||||
@ -1132,7 +1132,7 @@ monzopy==1.3.2
|
||||
mopeka-iot-ble==0.8.0
|
||||
|
||||
# homeassistant.components.motion_blinds
|
||||
motionblinds==0.6.24
|
||||
motionblinds==0.6.25
|
||||
|
||||
# homeassistant.components.motionblinds_ble
|
||||
motionblindsble==0.1.1
|
||||
@ -1831,7 +1831,7 @@ python-izone==1.2.9
|
||||
python-juicenet==1.1.0
|
||||
|
||||
# homeassistant.components.tplink
|
||||
python-kasa[speedups]==0.7.2
|
||||
python-kasa[speedups]==0.7.3
|
||||
|
||||
# homeassistant.components.linkplay
|
||||
python-linkplay==0.0.9
|
||||
@ -2053,7 +2053,7 @@ sensorpush-ble==1.6.2
|
||||
sentry-sdk==1.40.3
|
||||
|
||||
# homeassistant.components.sfr_box
|
||||
sfrbox-api==0.0.10
|
||||
sfrbox-api==0.0.11
|
||||
|
||||
# homeassistant.components.sharkiq
|
||||
sharkiq==1.0.2
|
||||
@ -2356,7 +2356,7 @@ yalexs-ble==2.4.3
|
||||
|
||||
# homeassistant.components.august
|
||||
# homeassistant.components.yale
|
||||
yalexs==8.6.3
|
||||
yalexs==8.6.4
|
||||
|
||||
# homeassistant.components.yeelight
|
||||
yeelight==0.7.14
|
||||
@ -2383,7 +2383,7 @@ zeroconf==0.133.0
|
||||
zeversolar==0.3.1
|
||||
|
||||
# homeassistant.components.zha
|
||||
zha==0.0.32
|
||||
zha==0.0.33
|
||||
|
||||
# homeassistant.components.zwave_js
|
||||
zwave-js-server-python==0.57.0
|
||||
|
@ -206,6 +206,12 @@ tuf>=4.0.0
|
||||
|
||||
# https://github.com/jd/tenacity/issues/471
|
||||
tenacity!=8.4.0
|
||||
|
||||
# pyasn1.compat.octets was removed in pyasn1 0.6.1 and breaks some integrations
|
||||
# and tests that import it directly
|
||||
# https://github.com/pyasn1/pyasn1/pull/60
|
||||
# https://github.com/lextudio/pysnmp/issues/114
|
||||
pyasn1==0.6.0
|
||||
"""
|
||||
|
||||
GENERATED_MESSAGE = (
|
||||
|
@ -160,7 +160,6 @@ EXCEPTIONS = {
|
||||
"pyvera", # https://github.com/maximvelichko/pyvera/pull/164
|
||||
"pyxeoma", # https://github.com/jeradM/pyxeoma/pull/11
|
||||
"repoze.lru",
|
||||
"russound", # https://github.com/laf/russound/pull/14 # codespell:ignore laf
|
||||
"ruuvitag-ble", # https://github.com/Bluetooth-Devices/ruuvitag-ble/pull/10
|
||||
"sensirion-ble", # https://github.com/akx/sensirion-ble/pull/9
|
||||
"sharp_aquos_rc", # https://github.com/jmoore987/sharp_aquos_rc/pull/14
|
||||
|
@ -1,6 +1,52 @@
|
||||
# serializer version: 1
|
||||
# name: test_diagnostics
|
||||
dict({
|
||||
'coordinator_data': dict({
|
||||
'state': dict({
|
||||
'current_temperature': dict({
|
||||
'data_type': 0,
|
||||
'desc': '',
|
||||
'name': 'Room temp 1 actual value',
|
||||
'unit': '°C',
|
||||
'value': '18.6',
|
||||
}),
|
||||
'hvac_action': dict({
|
||||
'data_type': 1,
|
||||
'desc': 'Raumtemp’begrenzung',
|
||||
'name': 'Status heating circuit 1',
|
||||
'unit': '',
|
||||
'value': '122',
|
||||
}),
|
||||
'hvac_mode': dict({
|
||||
'data_type': 1,
|
||||
'desc': 'Komfort',
|
||||
'name': 'Operating mode',
|
||||
'unit': '',
|
||||
'value': 'heat',
|
||||
}),
|
||||
'hvac_mode2': dict({
|
||||
'data_type': 1,
|
||||
'desc': 'Reduziert',
|
||||
'name': 'Operating mode',
|
||||
'unit': '',
|
||||
'value': '2',
|
||||
}),
|
||||
'room1_thermostat_mode': dict({
|
||||
'data_type': 1,
|
||||
'desc': 'Kein Bedarf',
|
||||
'name': 'Raumthermostat 1',
|
||||
'unit': '',
|
||||
'value': '0',
|
||||
}),
|
||||
'target_temperature': dict({
|
||||
'data_type': 0,
|
||||
'desc': '',
|
||||
'name': 'Room temperature Comfort setpoint',
|
||||
'unit': '°C',
|
||||
'value': '18.5',
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
'device': dict({
|
||||
'MAC': '00:80:41:19:69:90',
|
||||
'name': 'BSB-LAN',
|
||||
@ -30,48 +76,20 @@
|
||||
'value': 'RVS21.831F/127',
|
||||
}),
|
||||
}),
|
||||
'state': dict({
|
||||
'current_temperature': dict({
|
||||
'static': dict({
|
||||
'max_temp': dict({
|
||||
'data_type': 0,
|
||||
'desc': '',
|
||||
'name': 'Room temp 1 actual value',
|
||||
'name': 'Summer/winter changeover temp heat circuit 1',
|
||||
'unit': '°C',
|
||||
'value': '18.6',
|
||||
'value': '20.0',
|
||||
}),
|
||||
'hvac_action': dict({
|
||||
'data_type': 1,
|
||||
'desc': 'Raumtemp’begrenzung',
|
||||
'name': 'Status heating circuit 1',
|
||||
'unit': '',
|
||||
'value': '122',
|
||||
}),
|
||||
'hvac_mode': dict({
|
||||
'data_type': 1,
|
||||
'desc': 'Komfort',
|
||||
'name': 'Operating mode',
|
||||
'unit': '',
|
||||
'value': 'heat',
|
||||
}),
|
||||
'hvac_mode2': dict({
|
||||
'data_type': 1,
|
||||
'desc': 'Reduziert',
|
||||
'name': 'Operating mode',
|
||||
'unit': '',
|
||||
'value': '2',
|
||||
}),
|
||||
'room1_thermostat_mode': dict({
|
||||
'data_type': 1,
|
||||
'desc': 'Kein Bedarf',
|
||||
'name': 'Raumthermostat 1',
|
||||
'unit': '',
|
||||
'value': '0',
|
||||
}),
|
||||
'target_temperature': dict({
|
||||
'min_temp': dict({
|
||||
'data_type': 0,
|
||||
'desc': '',
|
||||
'name': 'Room temperature Comfort setpoint',
|
||||
'name': 'Room temp frost protection setpoint',
|
||||
'unit': '°C',
|
||||
'value': '18.5',
|
||||
'value': '8.0',
|
||||
}),
|
||||
}),
|
||||
})
|
||||
|
@ -268,3 +268,49 @@ async def test_tts_service_speak_error(
|
||||
tts_entity._client.generate.assert_called_once_with(
|
||||
text="There is a person at the front door.", voice="voice1", model="model1"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("setup", "tts_service", "service_data"),
|
||||
[
|
||||
(
|
||||
"mock_config_entry_setup",
|
||||
"speak",
|
||||
{
|
||||
ATTR_ENTITY_ID: "tts.mock_title",
|
||||
tts.ATTR_MEDIA_PLAYER_ENTITY_ID: "media_player.something",
|
||||
tts.ATTR_MESSAGE: "There is a person at the front door.",
|
||||
tts.ATTR_OPTIONS: {},
|
||||
},
|
||||
),
|
||||
],
|
||||
indirect=["setup"],
|
||||
)
|
||||
async def test_tts_service_speak_without_options(
|
||||
setup: AsyncMock,
|
||||
hass: HomeAssistant,
|
||||
hass_client: ClientSessionGenerator,
|
||||
calls: list[ServiceCall],
|
||||
tts_service: str,
|
||||
service_data: dict[str, Any],
|
||||
) -> None:
|
||||
"""Test service call say with http response 200."""
|
||||
tts_entity = hass.data[tts.DOMAIN].get_entity(service_data[ATTR_ENTITY_ID])
|
||||
tts_entity._client.generate.reset_mock()
|
||||
|
||||
await hass.services.async_call(
|
||||
tts.DOMAIN,
|
||||
tts_service,
|
||||
service_data,
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert len(calls) == 1
|
||||
assert (
|
||||
await retrieve_media(hass, hass_client, calls[0].data[ATTR_MEDIA_CONTENT_ID])
|
||||
== HTTPStatus.OK
|
||||
)
|
||||
|
||||
tts_entity._client.generate.assert_called_once_with(
|
||||
text="There is a person at the front door.", voice="voice1", model="model1"
|
||||
)
|
||||
|
@ -1,5 +1,5 @@
|
||||
# serializer version: 1
|
||||
# name: test_setup_platform[climate.thermostat_1-entry]
|
||||
# name: test_setup_platform[legacy_thermostat][climate.thermostat_1-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
@ -38,7 +38,73 @@
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_setup_platform[climate.thermostat_1-state]
|
||||
# name: test_setup_platform[legacy_thermostat][climate.thermostat_1-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'current_temperature': 21.4,
|
||||
'friendly_name': 'Thermostat 1',
|
||||
'hvac_action': <HVACAction.IDLE: 'idle'>,
|
||||
'hvac_modes': list([
|
||||
<HVACMode.HEAT: 'heat'>,
|
||||
]),
|
||||
'max_temp': 30.0,
|
||||
'min_temp': 5.0,
|
||||
'status': dict({
|
||||
'override': 0.0,
|
||||
'room_temp': 21.42,
|
||||
'setpoint': 18.0,
|
||||
}),
|
||||
'supported_features': <ClimateEntityFeature: 1>,
|
||||
'temperature': 18.0,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'climate.thermostat_1',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'heat',
|
||||
})
|
||||
# ---
|
||||
# name: test_setup_platform[new_thermostat][climate.thermostat_1-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'hvac_modes': list([
|
||||
<HVACMode.HEAT: 'heat'>,
|
||||
]),
|
||||
'max_temp': 30.0,
|
||||
'min_temp': 5.0,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'climate',
|
||||
'entity_category': None,
|
||||
'entity_id': 'climate.thermostat_1',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': None,
|
||||
'platform': 'incomfort',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': <ClimateEntityFeature: 1>,
|
||||
'translation_key': None,
|
||||
'unique_id': 'c0ffeec0ffee_1',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_setup_platform[new_thermostat][climate.thermostat_1-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'current_temperature': 21.4,
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from syrupy import SnapshotAssertion
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
@ -13,6 +14,14 @@ from tests.common import snapshot_platform
|
||||
|
||||
|
||||
@patch("homeassistant.components.incomfort.PLATFORMS", [Platform.CLIMATE])
|
||||
@pytest.mark.parametrize(
|
||||
"mock_room_status",
|
||||
[
|
||||
{"room_temp": 21.42, "setpoint": 18.0, "override": 18.0},
|
||||
{"room_temp": 21.42, "setpoint": 18.0, "override": 0.0},
|
||||
],
|
||||
ids=["new_thermostat", "legacy_thermostat"],
|
||||
)
|
||||
async def test_setup_platform(
|
||||
hass: HomeAssistant,
|
||||
mock_incomfort: MagicMock,
|
||||
@ -20,6 +29,10 @@ async def test_setup_platform(
|
||||
snapshot: SnapshotAssertion,
|
||||
mock_config_entry: ConfigEntry,
|
||||
) -> None:
|
||||
"""Test the incomfort entities are set up correctly."""
|
||||
"""Test the incomfort entities are set up correctly.
|
||||
|
||||
Legacy thermostats report 0.0 as override if no override is set,
|
||||
but new thermostat sync the override with the actual setpoint instead.
|
||||
"""
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
|
||||
|
@ -65,10 +65,13 @@ class MockLifxCommand:
|
||||
"""Init command."""
|
||||
self.bulb = bulb
|
||||
self.calls = []
|
||||
self.msg_kwargs = kwargs
|
||||
self.msg_kwargs = {
|
||||
k.removeprefix("msg_"): v for k, v in kwargs.items() if k.startswith("msg_")
|
||||
}
|
||||
for k, v in kwargs.items():
|
||||
if k != "callb":
|
||||
setattr(self.bulb, k, v)
|
||||
if k.startswith("msg_") or k == "callb":
|
||||
continue
|
||||
setattr(self.bulb, k, v)
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
"""Call command."""
|
||||
@ -156,9 +159,16 @@ def _mocked_infrared_bulb() -> Light:
|
||||
def _mocked_light_strip() -> Light:
|
||||
bulb = _mocked_bulb()
|
||||
bulb.product = 31 # LIFX Z
|
||||
bulb.color_zones = [MagicMock(), MagicMock()]
|
||||
bulb.zones_count = 3
|
||||
bulb.color_zones = [MagicMock()] * 3
|
||||
bulb.effect = {"effect": "MOVE", "speed": 3, "duration": 0, "direction": "RIGHT"}
|
||||
bulb.get_color_zones = MockLifxCommand(bulb)
|
||||
bulb.get_color_zones = MockLifxCommand(
|
||||
bulb,
|
||||
msg_seq_num=bulb.seq_next(),
|
||||
msg_count=bulb.zones_count,
|
||||
msg_index=0,
|
||||
msg_color=bulb.color_zones,
|
||||
)
|
||||
bulb.set_color_zones = MockLifxCommand(bulb)
|
||||
bulb.get_multizone_effect = MockLifxCommand(bulb)
|
||||
bulb.set_multizone_effect = MockLifxCommand(bulb)
|
||||
|
@ -9,6 +9,7 @@ from . import (
|
||||
DEFAULT_ENTRY_TITLE,
|
||||
IP_ADDRESS,
|
||||
SERIAL,
|
||||
MockLifxCommand,
|
||||
_mocked_bulb,
|
||||
_mocked_clean_bulb,
|
||||
_mocked_infrared_bulb,
|
||||
@ -188,6 +189,22 @@ async def test_legacy_multizone_bulb_diagnostics(
|
||||
)
|
||||
config_entry.add_to_hass(hass)
|
||||
bulb = _mocked_light_strip()
|
||||
bulb.get_color_zones = MockLifxCommand(
|
||||
bulb,
|
||||
msg_seq_num=0,
|
||||
msg_count=8,
|
||||
msg_color=[
|
||||
(54612, 65535, 65535, 3500),
|
||||
(54612, 65535, 65535, 3500),
|
||||
(54612, 65535, 65535, 3500),
|
||||
(54612, 65535, 65535, 3500),
|
||||
(46420, 65535, 65535, 3500),
|
||||
(46420, 65535, 65535, 3500),
|
||||
(46420, 65535, 65535, 3500),
|
||||
(46420, 65535, 65535, 3500),
|
||||
],
|
||||
msg_index=0,
|
||||
)
|
||||
bulb.zones_count = 8
|
||||
bulb.color_zones = [
|
||||
(54612, 65535, 65535, 3500),
|
||||
@ -302,6 +319,22 @@ async def test_multizone_bulb_diagnostics(
|
||||
config_entry.add_to_hass(hass)
|
||||
bulb = _mocked_light_strip()
|
||||
bulb.product = 38
|
||||
bulb.get_color_zones = MockLifxCommand(
|
||||
bulb,
|
||||
msg_seq_num=0,
|
||||
msg_count=8,
|
||||
msg_color=[
|
||||
(54612, 65535, 65535, 3500),
|
||||
(54612, 65535, 65535, 3500),
|
||||
(54612, 65535, 65535, 3500),
|
||||
(54612, 65535, 65535, 3500),
|
||||
(46420, 65535, 65535, 3500),
|
||||
(46420, 65535, 65535, 3500),
|
||||
(46420, 65535, 65535, 3500),
|
||||
(46420, 65535, 65535, 3500),
|
||||
],
|
||||
msg_index=0,
|
||||
)
|
||||
bulb.zones_count = 8
|
||||
bulb.color_zones = [
|
||||
(54612, 65535, 65535, 3500),
|
||||
|
@ -192,15 +192,7 @@ async def test_light_strip(hass: HomeAssistant) -> None:
|
||||
{ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 100},
|
||||
blocking=True,
|
||||
)
|
||||
call_dict = bulb.set_color_zones.calls[0][1]
|
||||
call_dict.pop("callb")
|
||||
assert call_dict == {
|
||||
"apply": 0,
|
||||
"color": [],
|
||||
"duration": 0,
|
||||
"end_index": 0,
|
||||
"start_index": 0,
|
||||
}
|
||||
assert len(bulb.set_color_zones.calls) == 0
|
||||
bulb.set_color_zones.reset_mock()
|
||||
|
||||
await hass.services.async_call(
|
||||
@ -209,15 +201,7 @@ async def test_light_strip(hass: HomeAssistant) -> None:
|
||||
{ATTR_ENTITY_ID: entity_id, ATTR_HS_COLOR: (10, 30)},
|
||||
blocking=True,
|
||||
)
|
||||
call_dict = bulb.set_color_zones.calls[0][1]
|
||||
call_dict.pop("callb")
|
||||
assert call_dict == {
|
||||
"apply": 0,
|
||||
"color": [],
|
||||
"duration": 0,
|
||||
"end_index": 0,
|
||||
"start_index": 0,
|
||||
}
|
||||
assert len(bulb.set_color_zones.calls) == 0
|
||||
bulb.set_color_zones.reset_mock()
|
||||
|
||||
bulb.color_zones = [
|
||||
@ -238,7 +222,7 @@ async def test_light_strip(hass: HomeAssistant) -> None:
|
||||
blocking=True,
|
||||
)
|
||||
# Single color uses the fast path
|
||||
assert bulb.set_color.calls[0][0][0] == [1820, 19660, 65535, 3500]
|
||||
assert bulb.set_color.calls[1][0][0] == [1820, 19660, 65535, 3500]
|
||||
bulb.set_color.reset_mock()
|
||||
assert len(bulb.set_color_zones.calls) == 0
|
||||
|
||||
@ -422,7 +406,9 @@ async def test_light_strip(hass: HomeAssistant) -> None:
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
bulb.get_color_zones = MockLifxCommand(bulb)
|
||||
bulb.get_color_zones = MockLifxCommand(
|
||||
bulb, msg_seq_num=0, msg_color=[0, 0, 65535, 3500] * 3, msg_index=0, msg_count=3
|
||||
)
|
||||
bulb.get_color = MockFailingLifxCommand(bulb)
|
||||
|
||||
with pytest.raises(HomeAssistantError):
|
||||
@ -587,14 +573,14 @@ async def test_extended_multizone_messages(hass: HomeAssistant) -> None:
|
||||
bulb.set_extended_color_zones.reset_mock()
|
||||
|
||||
bulb.color_zones = [
|
||||
(0, 65535, 65535, 3500),
|
||||
(54612, 65535, 65535, 3500),
|
||||
(54612, 65535, 65535, 3500),
|
||||
(54612, 65535, 65535, 3500),
|
||||
(46420, 65535, 65535, 3500),
|
||||
(46420, 65535, 65535, 3500),
|
||||
(46420, 65535, 65535, 3500),
|
||||
(46420, 65535, 65535, 3500),
|
||||
[0, 65535, 65535, 3500],
|
||||
[54612, 65535, 65535, 3500],
|
||||
[54612, 65535, 65535, 3500],
|
||||
[54612, 65535, 65535, 3500],
|
||||
[46420, 65535, 65535, 3500],
|
||||
[46420, 65535, 65535, 3500],
|
||||
[46420, 65535, 65535, 3500],
|
||||
[46420, 65535, 65535, 3500],
|
||||
]
|
||||
|
||||
await hass.services.async_call(
|
||||
@ -1308,7 +1294,11 @@ async def test_config_zoned_light_strip_fails(
|
||||
def __call__(self, callb=None, *args, **kwargs):
|
||||
"""Call command."""
|
||||
self.call_count += 1
|
||||
response = None if self.call_count >= 2 else MockMessage()
|
||||
response = (
|
||||
None
|
||||
if self.call_count >= 2
|
||||
else MockMessage(seq_num=0, color=[], index=0, count=0)
|
||||
)
|
||||
if callb:
|
||||
callb(self.bulb, response)
|
||||
|
||||
@ -1349,7 +1339,15 @@ async def test_legacy_zoned_light_strip(
|
||||
self.call_count += 1
|
||||
self.bulb.color_zones = [None] * 12
|
||||
if callb:
|
||||
callb(self.bulb, MockMessage())
|
||||
callb(
|
||||
self.bulb,
|
||||
MockMessage(
|
||||
seq_num=0,
|
||||
index=0,
|
||||
count=self.bulb.zones_count,
|
||||
color=self.bulb.color_zones,
|
||||
),
|
||||
)
|
||||
|
||||
get_color_zones_mock = MockPopulateLifxZonesCommand(light_strip)
|
||||
light_strip.get_color_zones = get_color_zones_mock
|
||||
@ -1946,6 +1944,33 @@ async def test_light_strip_zones_not_populated_yet(hass: HomeAssistant) -> None:
|
||||
bulb.power_level = 65535
|
||||
bulb.color_zones = None
|
||||
bulb.color = [65535, 65535, 65535, 65535]
|
||||
bulb.get_color_zones = next(
|
||||
iter(
|
||||
[
|
||||
MockLifxCommand(
|
||||
bulb,
|
||||
msg_seq_num=0,
|
||||
msg_color=[0, 0, 65535, 3500] * 8,
|
||||
msg_index=0,
|
||||
msg_count=16,
|
||||
),
|
||||
MockLifxCommand(
|
||||
bulb,
|
||||
msg_seq_num=1,
|
||||
msg_color=[0, 0, 65535, 3500] * 8,
|
||||
msg_index=0,
|
||||
msg_count=16,
|
||||
),
|
||||
MockLifxCommand(
|
||||
bulb,
|
||||
msg_seq_num=2,
|
||||
msg_color=[0, 0, 65535, 3500] * 8,
|
||||
msg_index=8,
|
||||
msg_count=16,
|
||||
),
|
||||
]
|
||||
)
|
||||
)
|
||||
assert bulb.get_color_zones.calls == []
|
||||
|
||||
with (
|
||||
|
@ -246,7 +246,13 @@ MOCK_VEHICLES = {
|
||||
ATTR_DEVICE_CLASS: SensorDeviceClass.ENUM,
|
||||
ATTR_ENTITY_ID: "sensor.reg_number_plug_state",
|
||||
ATTR_ICON: "mdi:power-plug",
|
||||
ATTR_OPTIONS: ["unplugged", "plugged", "plug_error", "plug_unknown"],
|
||||
ATTR_OPTIONS: [
|
||||
"unplugged",
|
||||
"plugged",
|
||||
"plugged_waiting_for_charge",
|
||||
"plug_error",
|
||||
"plug_unknown",
|
||||
],
|
||||
ATTR_STATE: "plugged",
|
||||
ATTR_UNIQUE_ID: "vf1aaaaa555777999_plug_state",
|
||||
},
|
||||
@ -487,7 +493,13 @@ MOCK_VEHICLES = {
|
||||
ATTR_DEVICE_CLASS: SensorDeviceClass.ENUM,
|
||||
ATTR_ENTITY_ID: "sensor.reg_number_plug_state",
|
||||
ATTR_ICON: "mdi:power-plug-off",
|
||||
ATTR_OPTIONS: ["unplugged", "plugged", "plug_error", "plug_unknown"],
|
||||
ATTR_OPTIONS: [
|
||||
"unplugged",
|
||||
"plugged",
|
||||
"plugged_waiting_for_charge",
|
||||
"plug_error",
|
||||
"plug_unknown",
|
||||
],
|
||||
ATTR_STATE: "unplugged",
|
||||
ATTR_UNIQUE_ID: "vf1aaaaa555777999_plug_state",
|
||||
},
|
||||
@ -725,7 +737,13 @@ MOCK_VEHICLES = {
|
||||
ATTR_DEVICE_CLASS: SensorDeviceClass.ENUM,
|
||||
ATTR_ENTITY_ID: "sensor.reg_number_plug_state",
|
||||
ATTR_ICON: "mdi:power-plug",
|
||||
ATTR_OPTIONS: ["unplugged", "plugged", "plug_error", "plug_unknown"],
|
||||
ATTR_OPTIONS: [
|
||||
"unplugged",
|
||||
"plugged",
|
||||
"plugged_waiting_for_charge",
|
||||
"plug_error",
|
||||
"plug_unknown",
|
||||
],
|
||||
ATTR_STATE: "plugged",
|
||||
ATTR_UNIQUE_ID: "vf1aaaaa555777123_plug_state",
|
||||
},
|
||||
|
@ -494,6 +494,7 @@
|
||||
'options': list([
|
||||
'unplugged',
|
||||
'plugged',
|
||||
'plugged_waiting_for_charge',
|
||||
'plug_error',
|
||||
'plug_unknown',
|
||||
]),
|
||||
@ -921,6 +922,7 @@
|
||||
'options': list([
|
||||
'unplugged',
|
||||
'plugged',
|
||||
'plugged_waiting_for_charge',
|
||||
'plug_error',
|
||||
'plug_unknown',
|
||||
]),
|
||||
@ -1249,6 +1251,7 @@
|
||||
'options': list([
|
||||
'unplugged',
|
||||
'plugged',
|
||||
'plugged_waiting_for_charge',
|
||||
'plug_error',
|
||||
'plug_unknown',
|
||||
]),
|
||||
@ -1674,6 +1677,7 @@
|
||||
'options': list([
|
||||
'unplugged',
|
||||
'plugged',
|
||||
'plugged_waiting_for_charge',
|
||||
'plug_error',
|
||||
'plug_unknown',
|
||||
]),
|
||||
@ -2000,6 +2004,7 @@
|
||||
'options': list([
|
||||
'unplugged',
|
||||
'plugged',
|
||||
'plugged_waiting_for_charge',
|
||||
'plug_error',
|
||||
'plug_unknown',
|
||||
]),
|
||||
@ -2456,6 +2461,7 @@
|
||||
'options': list([
|
||||
'unplugged',
|
||||
'plugged',
|
||||
'plugged_waiting_for_charge',
|
||||
'plug_error',
|
||||
'plug_unknown',
|
||||
]),
|
||||
@ -3104,6 +3110,7 @@
|
||||
'options': list([
|
||||
'unplugged',
|
||||
'plugged',
|
||||
'plugged_waiting_for_charge',
|
||||
'plug_error',
|
||||
'plug_unknown',
|
||||
]),
|
||||
@ -3531,6 +3538,7 @@
|
||||
'options': list([
|
||||
'unplugged',
|
||||
'plugged',
|
||||
'plugged_waiting_for_charge',
|
||||
'plug_error',
|
||||
'plug_unknown',
|
||||
]),
|
||||
@ -3859,6 +3867,7 @@
|
||||
'options': list([
|
||||
'unplugged',
|
||||
'plugged',
|
||||
'plugged_waiting_for_charge',
|
||||
'plug_error',
|
||||
'plug_unknown',
|
||||
]),
|
||||
@ -4284,6 +4293,7 @@
|
||||
'options': list([
|
||||
'unplugged',
|
||||
'plugged',
|
||||
'plugged_waiting_for_charge',
|
||||
'plug_error',
|
||||
'plug_unknown',
|
||||
]),
|
||||
@ -4610,6 +4620,7 @@
|
||||
'options': list([
|
||||
'unplugged',
|
||||
'plugged',
|
||||
'plugged_waiting_for_charge',
|
||||
'plug_error',
|
||||
'plug_unknown',
|
||||
]),
|
||||
@ -5066,6 +5077,7 @@
|
||||
'options': list([
|
||||
'unplugged',
|
||||
'plugged',
|
||||
'plugged_waiting_for_charge',
|
||||
'plug_error',
|
||||
'plug_unknown',
|
||||
]),
|
||||
|
@ -3,37 +3,56 @@
|
||||
from datetime import timedelta
|
||||
from unittest.mock import Mock
|
||||
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
from pyschlage.exceptions import UnknownError
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorDeviceClass
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import STATE_ON, STATE_UNAVAILABLE
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.util.dt import utcnow
|
||||
|
||||
from tests.common import async_fire_time_changed
|
||||
|
||||
|
||||
async def test_keypad_disabled_binary_sensor(
|
||||
hass: HomeAssistant, mock_lock: Mock, mock_added_config_entry: ConfigEntry
|
||||
hass: HomeAssistant,
|
||||
mock_schlage: Mock,
|
||||
mock_lock: Mock,
|
||||
mock_added_config_entry: ConfigEntry,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
) -> None:
|
||||
"""Test the keypad_disabled binary_sensor."""
|
||||
mock_lock.keypad_disabled.reset_mock()
|
||||
mock_lock.keypad_disabled.return_value = True
|
||||
|
||||
# Make the coordinator refresh data.
|
||||
async_fire_time_changed(hass, utcnow() + timedelta(seconds=31))
|
||||
freezer.tick(timedelta(seconds=30))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
keypad = hass.states.get("binary_sensor.vault_door_keypad_disabled")
|
||||
assert keypad is not None
|
||||
assert keypad.state == "on"
|
||||
assert keypad.state == STATE_ON
|
||||
assert keypad.attributes["device_class"] == BinarySensorDeviceClass.PROBLEM
|
||||
|
||||
mock_lock.keypad_disabled.assert_called_once_with([])
|
||||
|
||||
mock_schlage.locks.return_value = []
|
||||
# Make the coordinator refresh data.
|
||||
freezer.tick(timedelta(seconds=30))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
keypad = hass.states.get("binary_sensor.vault_door_keypad_disabled")
|
||||
assert keypad is not None
|
||||
assert keypad.state == STATE_UNAVAILABLE
|
||||
|
||||
|
||||
async def test_keypad_disabled_binary_sensor_use_previous_logs_on_failure(
|
||||
hass: HomeAssistant, mock_lock: Mock, mock_added_config_entry: ConfigEntry
|
||||
hass: HomeAssistant,
|
||||
mock_schlage: Mock,
|
||||
mock_lock: Mock,
|
||||
mock_added_config_entry: ConfigEntry,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
) -> None:
|
||||
"""Test the keypad_disabled binary_sensor."""
|
||||
mock_lock.keypad_disabled.reset_mock()
|
||||
@ -42,12 +61,13 @@ async def test_keypad_disabled_binary_sensor_use_previous_logs_on_failure(
|
||||
mock_lock.logs.side_effect = UnknownError("Cannot load logs")
|
||||
|
||||
# Make the coordinator refresh data.
|
||||
async_fire_time_changed(hass, utcnow() + timedelta(seconds=31))
|
||||
freezer.tick(timedelta(seconds=30))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
keypad = hass.states.get("binary_sensor.vault_door_keypad_disabled")
|
||||
assert keypad is not None
|
||||
assert keypad.state == "on"
|
||||
assert keypad.state == STATE_ON
|
||||
assert keypad.attributes["device_class"] == BinarySensorDeviceClass.PROBLEM
|
||||
|
||||
mock_lock.keypad_disabled.assert_called_once_with([])
|
||||
|
@ -3,12 +3,20 @@
|
||||
from datetime import timedelta
|
||||
from unittest.mock import Mock
|
||||
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
|
||||
from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import ATTR_ENTITY_ID, SERVICE_LOCK, SERVICE_UNLOCK
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
SERVICE_LOCK,
|
||||
SERVICE_UNLOCK,
|
||||
STATE_JAMMED,
|
||||
STATE_UNAVAILABLE,
|
||||
STATE_UNLOCKED,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.util.dt import utcnow
|
||||
|
||||
from tests.common import async_fire_time_changed
|
||||
|
||||
@ -26,6 +34,40 @@ async def test_lock_device_registry(
|
||||
assert device.manufacturer == "Schlage"
|
||||
|
||||
|
||||
async def test_lock_attributes(
|
||||
hass: HomeAssistant,
|
||||
mock_added_config_entry: ConfigEntry,
|
||||
mock_schlage: Mock,
|
||||
mock_lock: Mock,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
) -> None:
|
||||
"""Test lock attributes."""
|
||||
lock = hass.states.get("lock.vault_door")
|
||||
assert lock is not None
|
||||
assert lock.state == STATE_UNLOCKED
|
||||
assert lock.attributes["changed_by"] == "thumbturn"
|
||||
|
||||
mock_lock.is_locked = False
|
||||
mock_lock.is_jammed = True
|
||||
# Make the coordinator refresh data.
|
||||
freezer.tick(timedelta(seconds=30))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
lock = hass.states.get("lock.vault_door")
|
||||
assert lock is not None
|
||||
assert lock.state == STATE_JAMMED
|
||||
|
||||
mock_schlage.locks.return_value = []
|
||||
# Make the coordinator refresh data.
|
||||
freezer.tick(timedelta(seconds=30))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
lock = hass.states.get("lock.vault_door")
|
||||
assert lock is not None
|
||||
assert lock.state == STATE_UNAVAILABLE
|
||||
assert "changed_by" not in lock.attributes
|
||||
|
||||
|
||||
async def test_lock_services(
|
||||
hass: HomeAssistant, mock_lock: Mock, mock_added_config_entry: ConfigEntry
|
||||
) -> None:
|
||||
@ -52,14 +94,18 @@ async def test_lock_services(
|
||||
|
||||
|
||||
async def test_changed_by(
|
||||
hass: HomeAssistant, mock_lock: Mock, mock_added_config_entry: ConfigEntry
|
||||
hass: HomeAssistant,
|
||||
mock_lock: Mock,
|
||||
mock_added_config_entry: ConfigEntry,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
) -> None:
|
||||
"""Test population of the changed_by attribute."""
|
||||
mock_lock.last_changed_by.reset_mock()
|
||||
mock_lock.last_changed_by.return_value = "access code - foo"
|
||||
|
||||
# Make the coordinator refresh data.
|
||||
async_fire_time_changed(hass, utcnow() + timedelta(seconds=31))
|
||||
freezer.tick(timedelta(seconds=30))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
mock_lock.last_changed_by.assert_called_once_with()
|
||||
|
||||
|
@ -31,7 +31,7 @@
|
||||
'product_id': 'NB6VAC-FXC-r0',
|
||||
'refclient': '',
|
||||
'serial_number': '**REDACTED**',
|
||||
'temperature': 27560.0,
|
||||
'temperature': 27560,
|
||||
'uptime': 2353575,
|
||||
'version_bootloader': 'NB6VAC-BOOTLOADER-R4.0.8',
|
||||
'version_dsldriver': 'NB6VAC-XDSL-A2pv6F039p',
|
||||
@ -90,7 +90,7 @@
|
||||
'product_id': 'NB6VAC-FXC-r0',
|
||||
'refclient': '',
|
||||
'serial_number': '**REDACTED**',
|
||||
'temperature': 27560.0,
|
||||
'temperature': 27560,
|
||||
'uptime': 2353575,
|
||||
'version_bootloader': 'NB6VAC-BOOTLOADER-R4.0.8',
|
||||
'version_dsldriver': 'NB6VAC-XDSL-A2pv6F039p',
|
||||
|
@ -336,6 +336,22 @@ async def test_zeroconf_cannot_connect(
|
||||
assert result2["reason"] == "cannot_connect"
|
||||
|
||||
|
||||
async def test_zeroconf_legacy_cannot_connect(
|
||||
hass: HomeAssistant, mock_smlight_client: MagicMock
|
||||
) -> None:
|
||||
"""Test we abort flow on zeroconf discovery unsupported firmware."""
|
||||
mock_smlight_client.get_info.side_effect = SmlightConnectionError
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_ZEROCONF},
|
||||
data=DISCOVERY_INFO_LEGACY,
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "cannot_connect"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_smlight_client")
|
||||
async def test_zeroconf_legacy_mac(
|
||||
hass: HomeAssistant, mock_smlight_client: MagicMock, mock_setup_entry: AsyncMock
|
||||
|
@ -1123,6 +1123,27 @@ async def test_play_media_announce(
|
||||
)
|
||||
assert sonos_websocket.play_clip.call_count == 1
|
||||
|
||||
# Test speakers that do not support announce. This
|
||||
# will result in playing the clip directly via play_uri
|
||||
sonos_websocket.play_clip.reset_mock()
|
||||
sonos_websocket.play_clip.side_effect = None
|
||||
retval = {"success": 0, "type": "globalError"}
|
||||
sonos_websocket.play_clip.return_value = [retval, {}]
|
||||
|
||||
await hass.services.async_call(
|
||||
MP_DOMAIN,
|
||||
SERVICE_PLAY_MEDIA,
|
||||
{
|
||||
ATTR_ENTITY_ID: "media_player.zone_a",
|
||||
ATTR_MEDIA_CONTENT_TYPE: "music",
|
||||
ATTR_MEDIA_CONTENT_ID: content_id,
|
||||
ATTR_MEDIA_ANNOUNCE: True,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
assert sonos_websocket.play_clip.call_count == 1
|
||||
soco.play_uri.assert_called_with(content_id, force_radio=False)
|
||||
|
||||
|
||||
async def test_media_get_queue(
|
||||
hass: HomeAssistant,
|
||||
|
@ -21,11 +21,13 @@ from kasa.protocol import BaseProtocol
|
||||
from syrupy import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.tplink import (
|
||||
CONF_AES_KEYS,
|
||||
CONF_ALIAS,
|
||||
CONF_CONNECTION_PARAMETERS,
|
||||
CONF_CREDENTIALS_HASH,
|
||||
CONF_DEVICE_CONFIG,
|
||||
CONF_HOST,
|
||||
CONF_MODEL,
|
||||
CONF_USES_HTTP,
|
||||
Credentials,
|
||||
)
|
||||
from homeassistant.components.tplink.const import DOMAIN
|
||||
@ -54,35 +56,42 @@ DHCP_FORMATTED_MAC_ADDRESS = MAC_ADDRESS.replace(":", "")
|
||||
MAC_ADDRESS2 = "11:22:33:44:55:66"
|
||||
DEFAULT_ENTRY_TITLE = f"{ALIAS} {MODEL}"
|
||||
CREDENTIALS_HASH_LEGACY = ""
|
||||
CONN_PARAMS_LEGACY = DeviceConnectionParameters(
|
||||
DeviceFamily.IotSmartPlugSwitch, DeviceEncryptionType.Xor
|
||||
)
|
||||
DEVICE_CONFIG_LEGACY = DeviceConfig(IP_ADDRESS)
|
||||
DEVICE_CONFIG_DICT_LEGACY = DEVICE_CONFIG_LEGACY.to_dict(exclude_credentials=True)
|
||||
CREDENTIALS = Credentials("foo", "bar")
|
||||
CREDENTIALS_HASH_AES = "AES/abcdefghijklmnopqrstuvabcdefghijklmnopqrstuv=="
|
||||
CREDENTIALS_HASH_KLAP = "KLAP/abcdefghijklmnopqrstuv=="
|
||||
CONN_PARAMS_KLAP = DeviceConnectionParameters(
|
||||
DeviceFamily.SmartTapoPlug, DeviceEncryptionType.Klap
|
||||
)
|
||||
DEVICE_CONFIG_KLAP = DeviceConfig(
|
||||
IP_ADDRESS,
|
||||
credentials=CREDENTIALS,
|
||||
connection_type=DeviceConnectionParameters(
|
||||
DeviceFamily.SmartTapoPlug, DeviceEncryptionType.Klap
|
||||
),
|
||||
connection_type=CONN_PARAMS_KLAP,
|
||||
uses_http=True,
|
||||
)
|
||||
CONN_PARAMS_AES = DeviceConnectionParameters(
|
||||
DeviceFamily.SmartTapoPlug, DeviceEncryptionType.Aes
|
||||
)
|
||||
AES_KEYS = {"private": "foo", "public": "bar"}
|
||||
DEVICE_CONFIG_AES = DeviceConfig(
|
||||
IP_ADDRESS2,
|
||||
credentials=CREDENTIALS,
|
||||
connection_type=DeviceConnectionParameters(
|
||||
DeviceFamily.SmartTapoPlug, DeviceEncryptionType.Aes
|
||||
),
|
||||
connection_type=CONN_PARAMS_AES,
|
||||
uses_http=True,
|
||||
aes_keys=AES_KEYS,
|
||||
)
|
||||
DEVICE_CONFIG_DICT_KLAP = DEVICE_CONFIG_KLAP.to_dict(exclude_credentials=True)
|
||||
DEVICE_CONFIG_DICT_AES = DEVICE_CONFIG_AES.to_dict(exclude_credentials=True)
|
||||
|
||||
CREATE_ENTRY_DATA_LEGACY = {
|
||||
CONF_HOST: IP_ADDRESS,
|
||||
CONF_ALIAS: ALIAS,
|
||||
CONF_MODEL: MODEL,
|
||||
CONF_DEVICE_CONFIG: DEVICE_CONFIG_DICT_LEGACY,
|
||||
CONF_CONNECTION_PARAMETERS: CONN_PARAMS_LEGACY.to_dict(),
|
||||
CONF_USES_HTTP: False,
|
||||
}
|
||||
|
||||
CREATE_ENTRY_DATA_KLAP = {
|
||||
@ -90,23 +99,18 @@ CREATE_ENTRY_DATA_KLAP = {
|
||||
CONF_ALIAS: ALIAS,
|
||||
CONF_MODEL: MODEL,
|
||||
CONF_CREDENTIALS_HASH: CREDENTIALS_HASH_KLAP,
|
||||
CONF_DEVICE_CONFIG: DEVICE_CONFIG_DICT_KLAP,
|
||||
CONF_CONNECTION_PARAMETERS: CONN_PARAMS_KLAP.to_dict(),
|
||||
CONF_USES_HTTP: True,
|
||||
}
|
||||
CREATE_ENTRY_DATA_AES = {
|
||||
CONF_HOST: IP_ADDRESS2,
|
||||
CONF_ALIAS: ALIAS,
|
||||
CONF_MODEL: MODEL,
|
||||
CONF_CREDENTIALS_HASH: CREDENTIALS_HASH_AES,
|
||||
CONF_DEVICE_CONFIG: DEVICE_CONFIG_DICT_AES,
|
||||
CONF_CONNECTION_PARAMETERS: CONN_PARAMS_AES.to_dict(),
|
||||
CONF_USES_HTTP: True,
|
||||
CONF_AES_KEYS: AES_KEYS,
|
||||
}
|
||||
CONNECTION_TYPE_KLAP = DeviceConnectionParameters(
|
||||
DeviceFamily.SmartTapoPlug, DeviceEncryptionType.Klap
|
||||
)
|
||||
CONNECTION_TYPE_KLAP_DICT = CONNECTION_TYPE_KLAP.to_dict()
|
||||
CONNECTION_TYPE_AES = DeviceConnectionParameters(
|
||||
DeviceFamily.SmartTapoPlug, DeviceEncryptionType.Aes
|
||||
)
|
||||
CONNECTION_TYPE_AES_DICT = CONNECTION_TYPE_AES.to_dict()
|
||||
|
||||
|
||||
def _load_feature_fixtures():
|
||||
@ -452,11 +456,11 @@ MODULE_TO_MOCK_GEN = {
|
||||
}
|
||||
|
||||
|
||||
def _patch_discovery(device=None, no_device=False):
|
||||
def _patch_discovery(device=None, no_device=False, ip_address=IP_ADDRESS):
|
||||
async def _discovery(*args, **kwargs):
|
||||
if no_device:
|
||||
return {}
|
||||
return {IP_ADDRESS: _mocked_device()}
|
||||
return {ip_address: device if device else _mocked_device()}
|
||||
|
||||
return patch("homeassistant.components.tplink.Discover.discover", new=_discovery)
|
||||
|
||||
|
@ -1,9 +1,9 @@
|
||||
"""tplink conftest."""
|
||||
|
||||
from collections.abc import Generator
|
||||
import copy
|
||||
from unittest.mock import DEFAULT, AsyncMock, patch
|
||||
|
||||
from kasa import DeviceConfig
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.tplink import DOMAIN
|
||||
@ -34,13 +34,13 @@ def mock_discovery():
|
||||
discover_single=DEFAULT,
|
||||
) as mock_discovery:
|
||||
device = _mocked_device(
|
||||
device_config=copy.deepcopy(DEVICE_CONFIG_KLAP),
|
||||
device_config=DeviceConfig.from_dict(DEVICE_CONFIG_KLAP.to_dict()),
|
||||
credentials_hash=CREDENTIALS_HASH_KLAP,
|
||||
alias=None,
|
||||
)
|
||||
devices = {
|
||||
"127.0.0.1": _mocked_device(
|
||||
device_config=copy.deepcopy(DEVICE_CONFIG_KLAP),
|
||||
device_config=DeviceConfig.from_dict(DEVICE_CONFIG_KLAP.to_dict()),
|
||||
credentials_hash=CREDENTIALS_HASH_KLAP,
|
||||
alias=None,
|
||||
)
|
||||
@ -57,12 +57,12 @@ def mock_connect():
|
||||
with patch("homeassistant.components.tplink.Device.connect") as mock_connect:
|
||||
devices = {
|
||||
IP_ADDRESS: _mocked_device(
|
||||
device_config=DEVICE_CONFIG_KLAP,
|
||||
device_config=DeviceConfig.from_dict(DEVICE_CONFIG_KLAP.to_dict()),
|
||||
credentials_hash=CREDENTIALS_HASH_KLAP,
|
||||
ip_address=IP_ADDRESS,
|
||||
),
|
||||
IP_ADDRESS2: _mocked_device(
|
||||
device_config=DEVICE_CONFIG_AES,
|
||||
device_config=DeviceConfig.from_dict(DEVICE_CONFIG_AES.to_dict()),
|
||||
credentials_hash=CREDENTIALS_HASH_AES,
|
||||
mac=MAC_ADDRESS2,
|
||||
ip_address=IP_ADDRESS2,
|
||||
|
@ -1,5 +1,6 @@
|
||||
"""Test the tplink config flow."""
|
||||
|
||||
from contextlib import contextmanager
|
||||
import logging
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
@ -17,7 +18,7 @@ from homeassistant.components.tplink import (
|
||||
KasaException,
|
||||
)
|
||||
from homeassistant.components.tplink.const import (
|
||||
CONF_CONNECTION_TYPE,
|
||||
CONF_CONNECTION_PARAMETERS,
|
||||
CONF_CREDENTIALS_HASH,
|
||||
CONF_DEVICE_CONFIG,
|
||||
)
|
||||
@ -34,17 +35,21 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
|
||||
from . import (
|
||||
AES_KEYS,
|
||||
ALIAS,
|
||||
CONNECTION_TYPE_KLAP_DICT,
|
||||
CONN_PARAMS_AES,
|
||||
CONN_PARAMS_KLAP,
|
||||
CONN_PARAMS_LEGACY,
|
||||
CREATE_ENTRY_DATA_AES,
|
||||
CREATE_ENTRY_DATA_KLAP,
|
||||
CREATE_ENTRY_DATA_LEGACY,
|
||||
CREDENTIALS_HASH_AES,
|
||||
CREDENTIALS_HASH_KLAP,
|
||||
DEFAULT_ENTRY_TITLE,
|
||||
DEVICE_CONFIG_DICT_AES,
|
||||
DEVICE_CONFIG_AES,
|
||||
DEVICE_CONFIG_DICT_KLAP,
|
||||
DEVICE_CONFIG_DICT_LEGACY,
|
||||
DEVICE_CONFIG_KLAP,
|
||||
DEVICE_CONFIG_LEGACY,
|
||||
DHCP_FORMATTED_MAC_ADDRESS,
|
||||
IP_ADDRESS,
|
||||
MAC_ADDRESS,
|
||||
@ -59,9 +64,44 @@ from . import (
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def test_discovery(hass: HomeAssistant) -> None:
|
||||
@contextmanager
|
||||
def override_side_effect(mock: AsyncMock, effect):
|
||||
"""Temporarily override a mock side effect and replace afterwards."""
|
||||
try:
|
||||
default_side_effect = mock.side_effect
|
||||
mock.side_effect = effect
|
||||
yield mock
|
||||
finally:
|
||||
mock.side_effect = default_side_effect
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("device_config", "expected_entry_data", "credentials_hash"),
|
||||
[
|
||||
pytest.param(
|
||||
DEVICE_CONFIG_KLAP, CREATE_ENTRY_DATA_KLAP, CREDENTIALS_HASH_KLAP, id="KLAP"
|
||||
),
|
||||
pytest.param(
|
||||
DEVICE_CONFIG_AES, CREATE_ENTRY_DATA_AES, CREDENTIALS_HASH_AES, id="AES"
|
||||
),
|
||||
pytest.param(DEVICE_CONFIG_LEGACY, CREATE_ENTRY_DATA_LEGACY, None, id="Legacy"),
|
||||
],
|
||||
)
|
||||
async def test_discovery(
|
||||
hass: HomeAssistant, device_config, expected_entry_data, credentials_hash
|
||||
) -> None:
|
||||
"""Test setting up discovery."""
|
||||
with _patch_discovery(), _patch_single_discovery(), _patch_connect():
|
||||
ip_address = device_config.host
|
||||
device = _mocked_device(
|
||||
device_config=device_config,
|
||||
credentials_hash=credentials_hash,
|
||||
ip_address=ip_address,
|
||||
)
|
||||
with (
|
||||
_patch_discovery(device, ip_address=ip_address),
|
||||
_patch_single_discovery(device),
|
||||
_patch_connect(device),
|
||||
):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
@ -91,9 +131,9 @@ async def test_discovery(hass: HomeAssistant) -> None:
|
||||
assert not result2["errors"]
|
||||
|
||||
with (
|
||||
_patch_discovery(),
|
||||
_patch_single_discovery(),
|
||||
_patch_connect(),
|
||||
_patch_discovery(device, ip_address=ip_address),
|
||||
_patch_single_discovery(device),
|
||||
_patch_connect(device),
|
||||
patch(f"{MODULE}.async_setup", return_value=True) as mock_setup,
|
||||
patch(f"{MODULE}.async_setup_entry", return_value=True) as mock_setup_entry,
|
||||
):
|
||||
@ -105,7 +145,7 @@ async def test_discovery(hass: HomeAssistant) -> None:
|
||||
|
||||
assert result3["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result3["title"] == DEFAULT_ENTRY_TITLE
|
||||
assert result3["data"] == CREATE_ENTRY_DATA_LEGACY
|
||||
assert result3["data"] == expected_entry_data
|
||||
mock_setup.assert_called_once()
|
||||
mock_setup_entry.assert_called_once()
|
||||
|
||||
@ -130,24 +170,25 @@ async def test_discovery_auth(
|
||||
) -> None:
|
||||
"""Test authenticated discovery."""
|
||||
|
||||
mock_discovery["mock_device"].update.side_effect = AuthenticationError
|
||||
mock_device = mock_connect["mock_devices"][IP_ADDRESS]
|
||||
assert mock_device.config == DEVICE_CONFIG_KLAP
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY},
|
||||
data={
|
||||
CONF_HOST: IP_ADDRESS,
|
||||
CONF_MAC: MAC_ADDRESS,
|
||||
CONF_ALIAS: ALIAS,
|
||||
CONF_DEVICE_CONFIG: DEVICE_CONFIG_DICT_KLAP,
|
||||
},
|
||||
)
|
||||
with override_side_effect(mock_connect["connect"], AuthenticationError):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY},
|
||||
data={
|
||||
CONF_HOST: IP_ADDRESS,
|
||||
CONF_MAC: MAC_ADDRESS,
|
||||
CONF_ALIAS: ALIAS,
|
||||
CONF_DEVICE: mock_device,
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "discovery_auth_confirm"
|
||||
assert not result["errors"]
|
||||
|
||||
mock_discovery["mock_device"].update.reset_mock(side_effect=True)
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
@ -172,40 +213,43 @@ async def test_discovery_auth(
|
||||
)
|
||||
async def test_discovery_auth_errors(
|
||||
hass: HomeAssistant,
|
||||
mock_discovery: AsyncMock,
|
||||
mock_connect: AsyncMock,
|
||||
mock_init,
|
||||
error_type,
|
||||
errors_msg,
|
||||
error_placement,
|
||||
) -> None:
|
||||
"""Test handling of discovery authentication errors."""
|
||||
mock_discovery["mock_device"].update.side_effect = AuthenticationError
|
||||
default_connect_side_effect = mock_connect["connect"].side_effect
|
||||
mock_connect["connect"].side_effect = error_type
|
||||
"""Test handling of discovery authentication errors.
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY},
|
||||
data={
|
||||
CONF_HOST: IP_ADDRESS,
|
||||
CONF_MAC: MAC_ADDRESS,
|
||||
CONF_ALIAS: ALIAS,
|
||||
CONF_DEVICE_CONFIG: DEVICE_CONFIG_DICT_KLAP,
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
Tests for errors received during credential
|
||||
entry during discovery_auth_confirm.
|
||||
"""
|
||||
mock_device = mock_connect["mock_devices"][IP_ADDRESS]
|
||||
|
||||
with override_side_effect(mock_connect["connect"], AuthenticationError):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY},
|
||||
data={
|
||||
CONF_HOST: IP_ADDRESS,
|
||||
CONF_MAC: MAC_ADDRESS,
|
||||
CONF_ALIAS: ALIAS,
|
||||
CONF_DEVICE: mock_device,
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "discovery_auth_confirm"
|
||||
assert not result["errors"]
|
||||
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
CONF_USERNAME: "fake_username",
|
||||
CONF_PASSWORD: "fake_password",
|
||||
},
|
||||
)
|
||||
with override_side_effect(mock_connect["connect"], error_type):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
CONF_USERNAME: "fake_username",
|
||||
CONF_PASSWORD: "fake_password",
|
||||
},
|
||||
)
|
||||
|
||||
assert result2["type"] is FlowResultType.FORM
|
||||
assert result2["errors"] == {error_placement: errors_msg}
|
||||
@ -213,7 +257,6 @@ async def test_discovery_auth_errors(
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
mock_connect["connect"].side_effect = default_connect_side_effect
|
||||
result3 = await hass.config_entries.flow.async_configure(
|
||||
result2["flow_id"],
|
||||
{
|
||||
@ -228,29 +271,29 @@ async def test_discovery_auth_errors(
|
||||
|
||||
async def test_discovery_new_credentials(
|
||||
hass: HomeAssistant,
|
||||
mock_discovery: AsyncMock,
|
||||
mock_connect: AsyncMock,
|
||||
mock_init,
|
||||
) -> None:
|
||||
"""Test setting up discovery with new credentials."""
|
||||
mock_discovery["mock_device"].update.side_effect = AuthenticationError
|
||||
mock_device = mock_connect["mock_devices"][IP_ADDRESS]
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY},
|
||||
data={
|
||||
CONF_HOST: IP_ADDRESS,
|
||||
CONF_MAC: MAC_ADDRESS,
|
||||
CONF_ALIAS: ALIAS,
|
||||
CONF_DEVICE_CONFIG: DEVICE_CONFIG_DICT_KLAP,
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
with override_side_effect(mock_connect["connect"], AuthenticationError):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY},
|
||||
data={
|
||||
CONF_HOST: IP_ADDRESS,
|
||||
CONF_MAC: MAC_ADDRESS,
|
||||
CONF_ALIAS: ALIAS,
|
||||
CONF_DEVICE: mock_device,
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "discovery_auth_confirm"
|
||||
assert not result["errors"]
|
||||
|
||||
assert mock_connect["connect"].call_count == 0
|
||||
assert mock_connect["connect"].call_count == 1
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.tplink.config_flow.get_credentials",
|
||||
@ -260,7 +303,7 @@ async def test_discovery_new_credentials(
|
||||
result["flow_id"],
|
||||
)
|
||||
|
||||
assert mock_connect["connect"].call_count == 1
|
||||
assert mock_connect["connect"].call_count == 2
|
||||
assert result2["type"] is FlowResultType.FORM
|
||||
assert result2["step_id"] == "discovery_confirm"
|
||||
|
||||
@ -277,48 +320,54 @@ async def test_discovery_new_credentials(
|
||||
|
||||
async def test_discovery_new_credentials_invalid(
|
||||
hass: HomeAssistant,
|
||||
mock_discovery: AsyncMock,
|
||||
mock_connect: AsyncMock,
|
||||
mock_init,
|
||||
) -> None:
|
||||
"""Test setting up discovery with new invalid credentials."""
|
||||
mock_discovery["mock_device"].update.side_effect = AuthenticationError
|
||||
default_connect_side_effect = mock_connect["connect"].side_effect
|
||||
mock_device = mock_connect["mock_devices"][IP_ADDRESS]
|
||||
|
||||
mock_connect["connect"].side_effect = AuthenticationError
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY},
|
||||
data={
|
||||
CONF_HOST: IP_ADDRESS,
|
||||
CONF_MAC: MAC_ADDRESS,
|
||||
CONF_ALIAS: ALIAS,
|
||||
CONF_DEVICE_CONFIG: DEVICE_CONFIG_DICT_KLAP,
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
with (
|
||||
patch("homeassistant.components.tplink.Discover.discover", return_value={}),
|
||||
patch(
|
||||
"homeassistant.components.tplink.config_flow.get_credentials",
|
||||
return_value=None,
|
||||
),
|
||||
override_side_effect(mock_connect["connect"], AuthenticationError),
|
||||
):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY},
|
||||
data={
|
||||
CONF_HOST: IP_ADDRESS,
|
||||
CONF_MAC: MAC_ADDRESS,
|
||||
CONF_ALIAS: ALIAS,
|
||||
CONF_DEVICE: mock_device,
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "discovery_auth_confirm"
|
||||
assert not result["errors"]
|
||||
|
||||
assert mock_connect["connect"].call_count == 0
|
||||
assert mock_connect["connect"].call_count == 1
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.tplink.config_flow.get_credentials",
|
||||
return_value=Credentials("fake_user", "fake_pass"),
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.tplink.config_flow.get_credentials",
|
||||
return_value=Credentials("fake_user", "fake_pass"),
|
||||
),
|
||||
override_side_effect(mock_connect["connect"], AuthenticationError),
|
||||
):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
)
|
||||
|
||||
assert mock_connect["connect"].call_count == 1
|
||||
assert mock_connect["connect"].call_count == 2
|
||||
assert result2["type"] is FlowResultType.FORM
|
||||
assert result2["step_id"] == "discovery_auth_confirm"
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
mock_connect["connect"].side_effect = default_connect_side_effect
|
||||
result3 = await hass.config_entries.flow.async_configure(
|
||||
result2["flow_id"],
|
||||
{
|
||||
@ -577,32 +626,30 @@ async def test_manual_auth_errors(
|
||||
assert not result["errors"]
|
||||
|
||||
mock_discovery["mock_device"].update.side_effect = AuthenticationError
|
||||
default_connect_side_effect = mock_connect["connect"].side_effect
|
||||
mock_connect["connect"].side_effect = error_type
|
||||
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={CONF_HOST: IP_ADDRESS}
|
||||
)
|
||||
with override_side_effect(mock_connect["connect"], error_type):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={CONF_HOST: IP_ADDRESS}
|
||||
)
|
||||
assert result2["type"] is FlowResultType.FORM
|
||||
assert result2["step_id"] == "user_auth_confirm"
|
||||
assert not result2["errors"]
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
result3 = await hass.config_entries.flow.async_configure(
|
||||
result2["flow_id"],
|
||||
user_input={
|
||||
CONF_USERNAME: "fake_username",
|
||||
CONF_PASSWORD: "fake_password",
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
with override_side_effect(mock_connect["connect"], error_type):
|
||||
result3 = await hass.config_entries.flow.async_configure(
|
||||
result2["flow_id"],
|
||||
user_input={
|
||||
CONF_USERNAME: "fake_username",
|
||||
CONF_PASSWORD: "fake_password",
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert result3["type"] is FlowResultType.FORM
|
||||
assert result3["step_id"] == "user_auth_confirm"
|
||||
assert result3["errors"] == {error_placement: errors_msg}
|
||||
assert result3["description_placeholders"]["error"] == str(error_type)
|
||||
|
||||
mock_connect["connect"].side_effect = default_connect_side_effect
|
||||
result4 = await hass.config_entries.flow.async_configure(
|
||||
result3["flow_id"],
|
||||
{
|
||||
@ -628,7 +675,7 @@ async def test_discovered_by_discovery_and_dhcp(hass: HomeAssistant) -> None:
|
||||
CONF_HOST: IP_ADDRESS,
|
||||
CONF_MAC: MAC_ADDRESS,
|
||||
CONF_ALIAS: ALIAS,
|
||||
CONF_DEVICE_CONFIG: DEVICE_CONFIG_DICT_LEGACY,
|
||||
CONF_DEVICE: _mocked_device(device_config=DEVICE_CONFIG_LEGACY),
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
@ -691,7 +738,7 @@ async def test_discovered_by_discovery_and_dhcp(hass: HomeAssistant) -> None:
|
||||
CONF_HOST: IP_ADDRESS,
|
||||
CONF_MAC: MAC_ADDRESS,
|
||||
CONF_ALIAS: ALIAS,
|
||||
CONF_DEVICE_CONFIG: DEVICE_CONFIG_DICT_LEGACY,
|
||||
CONF_DEVICE: _mocked_device(device_config=DEVICE_CONFIG_LEGACY),
|
||||
},
|
||||
),
|
||||
],
|
||||
@ -745,7 +792,7 @@ async def test_discovered_by_dhcp_or_discovery(
|
||||
CONF_HOST: IP_ADDRESS,
|
||||
CONF_MAC: MAC_ADDRESS,
|
||||
CONF_ALIAS: ALIAS,
|
||||
CONF_DEVICE_CONFIG: DEVICE_CONFIG_DICT_LEGACY,
|
||||
CONF_DEVICE: _mocked_device(device_config=DEVICE_CONFIG_LEGACY),
|
||||
},
|
||||
),
|
||||
],
|
||||
@ -775,9 +822,11 @@ async def test_integration_discovery_with_ip_change(
|
||||
mock_connect: AsyncMock,
|
||||
) -> None:
|
||||
"""Test reauth flow."""
|
||||
mock_connect["connect"].side_effect = KasaException()
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
with patch("homeassistant.components.tplink.Discover.discover", return_value={}):
|
||||
with (
|
||||
patch("homeassistant.components.tplink.Discover.discover", return_value={}),
|
||||
override_side_effect(mock_connect["connect"], KasaException()),
|
||||
):
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
@ -785,39 +834,57 @@ async def test_integration_discovery_with_ip_change(
|
||||
|
||||
flows = hass.config_entries.flow.async_progress()
|
||||
assert len(flows) == 0
|
||||
assert mock_config_entry.data[CONF_DEVICE_CONFIG] == DEVICE_CONFIG_DICT_LEGACY
|
||||
assert mock_config_entry.data[CONF_DEVICE_CONFIG].get(CONF_HOST) == "127.0.0.1"
|
||||
|
||||
discovery_result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY},
|
||||
data={
|
||||
CONF_HOST: "127.0.0.2",
|
||||
CONF_MAC: MAC_ADDRESS,
|
||||
CONF_ALIAS: ALIAS,
|
||||
CONF_DEVICE_CONFIG: DEVICE_CONFIG_DICT_KLAP,
|
||||
},
|
||||
assert (
|
||||
mock_config_entry.data[CONF_CONNECTION_PARAMETERS]
|
||||
== CONN_PARAMS_LEGACY.to_dict()
|
||||
)
|
||||
assert mock_config_entry.data[CONF_HOST] == "127.0.0.1"
|
||||
|
||||
mocked_device = _mocked_device(device_config=DEVICE_CONFIG_KLAP)
|
||||
with override_side_effect(mock_connect["connect"], lambda *_, **__: mocked_device):
|
||||
discovery_result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY},
|
||||
data={
|
||||
CONF_HOST: "127.0.0.2",
|
||||
CONF_MAC: MAC_ADDRESS,
|
||||
CONF_ALIAS: ALIAS,
|
||||
CONF_DEVICE: mocked_device,
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert discovery_result["type"] is FlowResultType.ABORT
|
||||
assert discovery_result["reason"] == "already_configured"
|
||||
assert mock_config_entry.data[CONF_DEVICE_CONFIG] == DEVICE_CONFIG_DICT_KLAP
|
||||
assert (
|
||||
mock_config_entry.data[CONF_CONNECTION_PARAMETERS] == CONN_PARAMS_KLAP.to_dict()
|
||||
)
|
||||
assert mock_config_entry.data[CONF_HOST] == "127.0.0.2"
|
||||
|
||||
config = DeviceConfig.from_dict(DEVICE_CONFIG_DICT_KLAP)
|
||||
|
||||
# Do a reload here and check that the
|
||||
# new config is picked up in setup_entry
|
||||
mock_connect["connect"].reset_mock(side_effect=True)
|
||||
bulb = _mocked_device(
|
||||
device_config=config,
|
||||
mac=mock_config_entry.unique_id,
|
||||
)
|
||||
mock_connect["connect"].return_value = bulb
|
||||
await hass.config_entries.async_reload(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.tplink.async_create_clientsession",
|
||||
return_value="Foo",
|
||||
),
|
||||
override_side_effect(mock_connect["connect"], lambda *_, **__: bulb),
|
||||
):
|
||||
await hass.config_entries.async_reload(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
assert mock_config_entry.state is ConfigEntryState.LOADED
|
||||
# Check that init set the new host correctly before calling connect
|
||||
assert config.host == "127.0.0.1"
|
||||
config.host = "127.0.0.2"
|
||||
config.uses_http = False # Not passed in to new config class
|
||||
config.http_client = "Foo"
|
||||
mock_connect["connect"].assert_awaited_once_with(config=config)
|
||||
|
||||
|
||||
@ -831,8 +898,6 @@ async def test_integration_discovery_with_connection_change(
|
||||
|
||||
And that connection_hash is removed as it will be invalid.
|
||||
"""
|
||||
mock_connect["connect"].side_effect = KasaException()
|
||||
|
||||
mock_config_entry = MockConfigEntry(
|
||||
title="TPLink",
|
||||
domain=DOMAIN,
|
||||
@ -840,7 +905,10 @@ async def test_integration_discovery_with_connection_change(
|
||||
unique_id=MAC_ADDRESS2,
|
||||
)
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
with patch("homeassistant.components.tplink.Discover.discover", return_value={}):
|
||||
with (
|
||||
patch("homeassistant.components.tplink.Discover.discover", return_value={}),
|
||||
override_side_effect(mock_connect["connect"], KasaException()),
|
||||
):
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
@ -854,43 +922,57 @@ async def test_integration_discovery_with_connection_change(
|
||||
== 0
|
||||
)
|
||||
assert mock_config_entry.data[CONF_HOST] == "127.0.0.2"
|
||||
assert mock_config_entry.data[CONF_DEVICE_CONFIG] == DEVICE_CONFIG_DICT_AES
|
||||
assert mock_config_entry.data[CONF_DEVICE_CONFIG].get(CONF_HOST) == "127.0.0.2"
|
||||
assert (
|
||||
mock_config_entry.data[CONF_CONNECTION_PARAMETERS] == CONN_PARAMS_AES.to_dict()
|
||||
)
|
||||
assert mock_config_entry.data[CONF_CREDENTIALS_HASH] == CREDENTIALS_HASH_AES
|
||||
|
||||
mock_connect["connect"].reset_mock()
|
||||
NEW_DEVICE_CONFIG = {
|
||||
**DEVICE_CONFIG_DICT_KLAP,
|
||||
CONF_CONNECTION_TYPE: CONNECTION_TYPE_KLAP_DICT,
|
||||
"connection_type": CONN_PARAMS_KLAP.to_dict(),
|
||||
CONF_HOST: "127.0.0.2",
|
||||
}
|
||||
config = DeviceConfig.from_dict(NEW_DEVICE_CONFIG)
|
||||
# Reset the connect mock so when the config flow reloads the entry it succeeds
|
||||
mock_connect["connect"].reset_mock(side_effect=True)
|
||||
|
||||
bulb = _mocked_device(
|
||||
device_config=config,
|
||||
mac=mock_config_entry.unique_id,
|
||||
)
|
||||
mock_connect["connect"].return_value = bulb
|
||||
|
||||
discovery_result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY},
|
||||
data={
|
||||
CONF_HOST: "127.0.0.2",
|
||||
CONF_MAC: MAC_ADDRESS2,
|
||||
CONF_ALIAS: ALIAS,
|
||||
CONF_DEVICE_CONFIG: NEW_DEVICE_CONFIG,
|
||||
},
|
||||
)
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.tplink.async_create_clientsession",
|
||||
return_value="Foo",
|
||||
),
|
||||
override_side_effect(mock_connect["connect"], lambda *_, **__: bulb),
|
||||
):
|
||||
discovery_result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY},
|
||||
data={
|
||||
CONF_HOST: "127.0.0.2",
|
||||
CONF_MAC: MAC_ADDRESS2,
|
||||
CONF_ALIAS: ALIAS,
|
||||
CONF_DEVICE: bulb,
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
assert discovery_result["type"] is FlowResultType.ABORT
|
||||
assert discovery_result["reason"] == "already_configured"
|
||||
assert mock_config_entry.data[CONF_DEVICE_CONFIG] == NEW_DEVICE_CONFIG
|
||||
assert (
|
||||
mock_config_entry.data[CONF_CONNECTION_PARAMETERS] == CONN_PARAMS_KLAP.to_dict()
|
||||
)
|
||||
assert mock_config_entry.data[CONF_HOST] == "127.0.0.2"
|
||||
assert CREDENTIALS_HASH_AES not in mock_config_entry.data
|
||||
|
||||
assert mock_config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
config.host = "127.0.0.2"
|
||||
config.uses_http = False # Not passed in to new config class
|
||||
config.http_client = "Foo"
|
||||
config.aes_keys = AES_KEYS
|
||||
mock_connect["connect"].assert_awaited_once_with(config=config)
|
||||
|
||||
|
||||
@ -901,17 +983,18 @@ async def test_dhcp_discovery_with_ip_change(
|
||||
mock_connect: AsyncMock,
|
||||
) -> None:
|
||||
"""Test dhcp discovery with an IP change."""
|
||||
mock_connect["connect"].side_effect = KasaException()
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
with patch("homeassistant.components.tplink.Discover.discover", return_value={}):
|
||||
with (
|
||||
patch("homeassistant.components.tplink.Discover.discover", return_value={}),
|
||||
override_side_effect(mock_connect["connect"], KasaException()),
|
||||
):
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY
|
||||
|
||||
flows = hass.config_entries.flow.async_progress()
|
||||
assert len(flows) == 0
|
||||
assert mock_config_entry.data[CONF_DEVICE_CONFIG] == DEVICE_CONFIG_DICT_LEGACY
|
||||
assert mock_config_entry.data[CONF_DEVICE_CONFIG].get(CONF_HOST) == "127.0.0.1"
|
||||
assert mock_config_entry.data[CONF_HOST] == "127.0.0.1"
|
||||
|
||||
discovery_result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
@ -966,8 +1049,7 @@ async def test_reauth_update_with_encryption_change(
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test reauth flow."""
|
||||
orig_side_effect = mock_connect["connect"].side_effect
|
||||
mock_connect["connect"].side_effect = AuthenticationError()
|
||||
|
||||
mock_config_entry = MockConfigEntry(
|
||||
title="TPLink",
|
||||
domain=DOMAIN,
|
||||
@ -975,10 +1057,15 @@ async def test_reauth_update_with_encryption_change(
|
||||
unique_id=MAC_ADDRESS2,
|
||||
)
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
assert mock_config_entry.data[CONF_DEVICE_CONFIG] == DEVICE_CONFIG_DICT_AES
|
||||
assert (
|
||||
mock_config_entry.data[CONF_CONNECTION_PARAMETERS] == CONN_PARAMS_AES.to_dict()
|
||||
)
|
||||
assert mock_config_entry.data[CONF_CREDENTIALS_HASH] == CREDENTIALS_HASH_AES
|
||||
|
||||
with patch("homeassistant.components.tplink.Discover.discover", return_value={}):
|
||||
with (
|
||||
patch("homeassistant.components.tplink.Discover.discover", return_value={}),
|
||||
override_side_effect(mock_connect["connect"], AuthenticationError()),
|
||||
):
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR
|
||||
@ -988,7 +1075,9 @@ async def test_reauth_update_with_encryption_change(
|
||||
assert len(flows) == 1
|
||||
[result] = flows
|
||||
assert result["step_id"] == "reauth_confirm"
|
||||
assert mock_config_entry.data[CONF_DEVICE_CONFIG] == DEVICE_CONFIG_DICT_AES
|
||||
assert (
|
||||
mock_config_entry.data[CONF_CONNECTION_PARAMETERS] == CONN_PARAMS_AES.to_dict()
|
||||
)
|
||||
assert CONF_CREDENTIALS_HASH not in mock_config_entry.data
|
||||
|
||||
new_config = DeviceConfig(
|
||||
@ -1005,7 +1094,6 @@ async def test_reauth_update_with_encryption_change(
|
||||
mock_connect["mock_devices"]["127.0.0.2"].config = new_config
|
||||
mock_connect["mock_devices"]["127.0.0.2"].credentials_hash = CREDENTIALS_HASH_KLAP
|
||||
|
||||
mock_connect["connect"].side_effect = orig_side_effect
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
@ -1023,10 +1111,10 @@ async def test_reauth_update_with_encryption_change(
|
||||
assert result2["type"] is FlowResultType.ABORT
|
||||
assert result2["reason"] == "reauth_successful"
|
||||
assert mock_config_entry.state is ConfigEntryState.LOADED
|
||||
assert mock_config_entry.data[CONF_DEVICE_CONFIG] == {
|
||||
**DEVICE_CONFIG_DICT_KLAP,
|
||||
CONF_HOST: "127.0.0.2",
|
||||
}
|
||||
assert (
|
||||
mock_config_entry.data[CONF_CONNECTION_PARAMETERS] == CONN_PARAMS_KLAP.to_dict()
|
||||
)
|
||||
assert mock_config_entry.data[CONF_HOST] == "127.0.0.2"
|
||||
assert mock_config_entry.data[CONF_CREDENTIALS_HASH] == CREDENTIALS_HASH_KLAP
|
||||
|
||||
|
||||
@ -1037,9 +1125,11 @@ async def test_reauth_update_from_discovery(
|
||||
mock_connect: AsyncMock,
|
||||
) -> None:
|
||||
"""Test reauth flow."""
|
||||
mock_connect["connect"].side_effect = AuthenticationError
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
with patch("homeassistant.components.tplink.Discover.discover", return_value={}):
|
||||
with (
|
||||
patch("homeassistant.components.tplink.Discover.discover", return_value={}),
|
||||
override_side_effect(mock_connect["connect"], AuthenticationError()),
|
||||
):
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
@ -1049,22 +1139,32 @@ async def test_reauth_update_from_discovery(
|
||||
assert len(flows) == 1
|
||||
[result] = flows
|
||||
assert result["step_id"] == "reauth_confirm"
|
||||
assert mock_config_entry.data[CONF_DEVICE_CONFIG] == DEVICE_CONFIG_DICT_LEGACY
|
||||
|
||||
discovery_result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY},
|
||||
data={
|
||||
CONF_HOST: IP_ADDRESS,
|
||||
CONF_MAC: MAC_ADDRESS,
|
||||
CONF_ALIAS: ALIAS,
|
||||
CONF_DEVICE_CONFIG: DEVICE_CONFIG_DICT_KLAP,
|
||||
},
|
||||
assert (
|
||||
mock_config_entry.data[CONF_CONNECTION_PARAMETERS]
|
||||
== CONN_PARAMS_LEGACY.to_dict()
|
||||
)
|
||||
|
||||
device = _mocked_device(
|
||||
device_config=DEVICE_CONFIG_KLAP,
|
||||
mac=mock_config_entry.unique_id,
|
||||
)
|
||||
with override_side_effect(mock_connect["connect"], lambda *_, **__: device):
|
||||
discovery_result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY},
|
||||
data={
|
||||
CONF_HOST: IP_ADDRESS,
|
||||
CONF_MAC: MAC_ADDRESS,
|
||||
CONF_ALIAS: ALIAS,
|
||||
CONF_DEVICE: device,
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert discovery_result["type"] is FlowResultType.ABORT
|
||||
assert discovery_result["reason"] == "already_configured"
|
||||
assert mock_config_entry.data[CONF_DEVICE_CONFIG] == DEVICE_CONFIG_DICT_KLAP
|
||||
assert (
|
||||
mock_config_entry.data[CONF_CONNECTION_PARAMETERS] == CONN_PARAMS_KLAP.to_dict()
|
||||
)
|
||||
|
||||
|
||||
async def test_reauth_update_from_discovery_with_ip_change(
|
||||
@ -1074,9 +1174,11 @@ async def test_reauth_update_from_discovery_with_ip_change(
|
||||
mock_connect: AsyncMock,
|
||||
) -> None:
|
||||
"""Test reauth flow."""
|
||||
mock_connect["connect"].side_effect = AuthenticationError()
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
with patch("homeassistant.components.tplink.Discover.discover", return_value={}):
|
||||
with (
|
||||
patch("homeassistant.components.tplink.Discover.discover", return_value={}),
|
||||
override_side_effect(mock_connect["connect"], AuthenticationError()),
|
||||
):
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR
|
||||
@ -1085,22 +1187,32 @@ async def test_reauth_update_from_discovery_with_ip_change(
|
||||
assert len(flows) == 1
|
||||
[result] = flows
|
||||
assert result["step_id"] == "reauth_confirm"
|
||||
assert mock_config_entry.data[CONF_DEVICE_CONFIG] == DEVICE_CONFIG_DICT_LEGACY
|
||||
|
||||
discovery_result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY},
|
||||
data={
|
||||
CONF_HOST: "127.0.0.2",
|
||||
CONF_MAC: MAC_ADDRESS,
|
||||
CONF_ALIAS: ALIAS,
|
||||
CONF_DEVICE_CONFIG: DEVICE_CONFIG_DICT_KLAP,
|
||||
},
|
||||
assert (
|
||||
mock_config_entry.data[CONF_CONNECTION_PARAMETERS]
|
||||
== CONN_PARAMS_LEGACY.to_dict()
|
||||
)
|
||||
|
||||
device = _mocked_device(
|
||||
device_config=DEVICE_CONFIG_KLAP,
|
||||
mac=mock_config_entry.unique_id,
|
||||
)
|
||||
with override_side_effect(mock_connect["connect"], lambda *_, **__: device):
|
||||
discovery_result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY},
|
||||
data={
|
||||
CONF_HOST: "127.0.0.2",
|
||||
CONF_MAC: MAC_ADDRESS,
|
||||
CONF_ALIAS: ALIAS,
|
||||
CONF_DEVICE: device,
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert discovery_result["type"] is FlowResultType.ABORT
|
||||
assert discovery_result["reason"] == "already_configured"
|
||||
assert mock_config_entry.data[CONF_DEVICE_CONFIG] == DEVICE_CONFIG_DICT_KLAP
|
||||
assert (
|
||||
mock_config_entry.data[CONF_CONNECTION_PARAMETERS] == CONN_PARAMS_KLAP.to_dict()
|
||||
)
|
||||
assert mock_config_entry.data[CONF_HOST] == "127.0.0.2"
|
||||
|
||||
|
||||
@ -1111,8 +1223,8 @@ async def test_reauth_no_update_if_config_and_ip_the_same(
|
||||
mock_connect: AsyncMock,
|
||||
) -> None:
|
||||
"""Test reauth discovery does not update when the host and config are the same."""
|
||||
mock_connect["connect"].side_effect = AuthenticationError()
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
hass.config_entries.async_update_entry(
|
||||
mock_config_entry,
|
||||
data={
|
||||
@ -1120,30 +1232,40 @@ async def test_reauth_no_update_if_config_and_ip_the_same(
|
||||
CONF_DEVICE_CONFIG: DEVICE_CONFIG_DICT_KLAP,
|
||||
},
|
||||
)
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
with override_side_effect(mock_connect["connect"], AuthenticationError()):
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR
|
||||
|
||||
flows = hass.config_entries.flow.async_progress()
|
||||
assert len(flows) == 1
|
||||
[result] = flows
|
||||
assert result["step_id"] == "reauth_confirm"
|
||||
assert mock_config_entry.data[CONF_DEVICE_CONFIG] == DEVICE_CONFIG_DICT_KLAP
|
||||
|
||||
discovery_result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY},
|
||||
data={
|
||||
CONF_HOST: IP_ADDRESS,
|
||||
CONF_MAC: MAC_ADDRESS,
|
||||
CONF_ALIAS: ALIAS,
|
||||
CONF_DEVICE_CONFIG: DEVICE_CONFIG_DICT_KLAP,
|
||||
},
|
||||
assert (
|
||||
mock_config_entry.data[CONF_CONNECTION_PARAMETERS] == CONN_PARAMS_KLAP.to_dict()
|
||||
)
|
||||
|
||||
device = _mocked_device(
|
||||
device_config=DEVICE_CONFIG_KLAP,
|
||||
mac=mock_config_entry.unique_id,
|
||||
)
|
||||
with override_side_effect(mock_connect["connect"], lambda *_, **__: device):
|
||||
discovery_result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY},
|
||||
data={
|
||||
CONF_HOST: IP_ADDRESS,
|
||||
CONF_MAC: MAC_ADDRESS,
|
||||
CONF_ALIAS: ALIAS,
|
||||
CONF_DEVICE: device,
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert discovery_result["type"] is FlowResultType.ABORT
|
||||
assert discovery_result["reason"] == "already_configured"
|
||||
assert mock_config_entry.data[CONF_DEVICE_CONFIG] == DEVICE_CONFIG_DICT_KLAP
|
||||
assert (
|
||||
mock_config_entry.data[CONF_CONNECTION_PARAMETERS] == CONN_PARAMS_KLAP.to_dict()
|
||||
)
|
||||
assert mock_config_entry.data[CONF_HOST] == IP_ADDRESS
|
||||
|
||||
|
||||
@ -1241,17 +1363,15 @@ async def test_pick_device_errors(
|
||||
assert result2["step_id"] == "pick_device"
|
||||
assert not result2["errors"]
|
||||
|
||||
default_connect_side_effect = mock_connect["connect"].side_effect
|
||||
mock_connect["connect"].side_effect = error_type
|
||||
result3 = await hass.config_entries.flow.async_configure(
|
||||
result2["flow_id"],
|
||||
{CONF_DEVICE: MAC_ADDRESS},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
with override_side_effect(mock_connect["connect"], error_type):
|
||||
result3 = await hass.config_entries.flow.async_configure(
|
||||
result2["flow_id"],
|
||||
{CONF_DEVICE: MAC_ADDRESS},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert result3["type"] == expected_flow
|
||||
|
||||
if expected_flow != FlowResultType.ABORT:
|
||||
mock_connect["connect"].side_effect = default_connect_side_effect
|
||||
result4 = await hass.config_entries.flow.async_configure(
|
||||
result3["flow_id"],
|
||||
user_input={
|
||||
@ -1300,17 +1420,17 @@ async def test_discovery_timeout_connect_legacy_error(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
mock_discovery["discover_single"].side_effect = TimeoutError
|
||||
mock_connect["connect"].side_effect = KasaException
|
||||
await hass.async_block_till_done()
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
assert not result["errors"]
|
||||
assert mock_connect["connect"].call_count == 0
|
||||
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], {CONF_HOST: IP_ADDRESS}
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
with override_side_effect(mock_connect["connect"], KasaException):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], {CONF_HOST: IP_ADDRESS}
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert result2["type"] is FlowResultType.FORM
|
||||
assert result2["errors"] == {"base": "cannot_connect"}
|
||||
assert mock_connect["connect"].call_count == 1
|
||||
@ -1334,17 +1454,17 @@ async def test_reauth_update_other_flows(
|
||||
data={**CREATE_ENTRY_DATA_AES},
|
||||
unique_id=MAC_ADDRESS2,
|
||||
)
|
||||
default_side_effect = mock_connect["connect"].side_effect
|
||||
mock_connect["connect"].side_effect = AuthenticationError()
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
mock_config_entry2.add_to_hass(hass)
|
||||
with patch("homeassistant.components.tplink.Discover.discover", return_value={}):
|
||||
with (
|
||||
patch("homeassistant.components.tplink.Discover.discover", return_value={}),
|
||||
override_side_effect(mock_connect["connect"], AuthenticationError()),
|
||||
):
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert mock_config_entry2.state is ConfigEntryState.SETUP_ERROR
|
||||
assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR
|
||||
mock_connect["connect"].side_effect = default_side_effect
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
@ -1353,7 +1473,9 @@ async def test_reauth_update_other_flows(
|
||||
flows_by_entry_id = {flow["context"]["entry_id"]: flow for flow in flows}
|
||||
result = flows_by_entry_id[mock_config_entry.entry_id]
|
||||
assert result["step_id"] == "reauth_confirm"
|
||||
assert mock_config_entry.data[CONF_DEVICE_CONFIG] == DEVICE_CONFIG_DICT_KLAP
|
||||
assert (
|
||||
mock_config_entry.data[CONF_CONNECTION_PARAMETERS] == CONN_PARAMS_KLAP.to_dict()
|
||||
)
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
|
@ -4,6 +4,7 @@ from __future__ import annotations
|
||||
|
||||
import copy
|
||||
from datetime import timedelta
|
||||
from typing import Any
|
||||
from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch
|
||||
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
@ -13,14 +14,18 @@ import pytest
|
||||
from homeassistant import setup
|
||||
from homeassistant.components import tplink
|
||||
from homeassistant.components.tplink.const import (
|
||||
CONF_AES_KEYS,
|
||||
CONF_CONNECTION_PARAMETERS,
|
||||
CONF_CREDENTIALS_HASH,
|
||||
CONF_DEVICE_CONFIG,
|
||||
DOMAIN,
|
||||
)
|
||||
from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState
|
||||
from homeassistant.const import (
|
||||
CONF_ALIAS,
|
||||
CONF_AUTHENTICATION,
|
||||
CONF_HOST,
|
||||
CONF_MODEL,
|
||||
CONF_PASSWORD,
|
||||
CONF_USERNAME,
|
||||
STATE_ON,
|
||||
@ -33,13 +38,20 @@ from homeassistant.setup import async_setup_component
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from . import (
|
||||
ALIAS,
|
||||
CREATE_ENTRY_DATA_AES,
|
||||
CREATE_ENTRY_DATA_KLAP,
|
||||
CREATE_ENTRY_DATA_LEGACY,
|
||||
CREDENTIALS_HASH_AES,
|
||||
CREDENTIALS_HASH_KLAP,
|
||||
DEVICE_CONFIG_AES,
|
||||
DEVICE_CONFIG_KLAP,
|
||||
DEVICE_CONFIG_LEGACY,
|
||||
DEVICE_ID,
|
||||
DEVICE_ID_MAC,
|
||||
IP_ADDRESS,
|
||||
MAC_ADDRESS,
|
||||
MODEL,
|
||||
_mocked_device,
|
||||
_patch_connect,
|
||||
_patch_discovery,
|
||||
@ -207,16 +219,21 @@ async def test_config_entry_with_stored_credentials(
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})[CONF_AUTHENTICATION] = auth
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
with patch(
|
||||
"homeassistant.components.tplink.async_create_clientsession", return_value="Foo"
|
||||
):
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
assert mock_config_entry.state is ConfigEntryState.LOADED
|
||||
config = DEVICE_CONFIG_KLAP
|
||||
config = DeviceConfig.from_dict(DEVICE_CONFIG_KLAP.to_dict())
|
||||
config.uses_http = False
|
||||
config.http_client = "Foo"
|
||||
assert config.credentials != stored_credentials
|
||||
config.credentials = stored_credentials
|
||||
mock_connect["connect"].assert_called_once_with(config=config)
|
||||
|
||||
|
||||
async def test_config_entry_device_config_invalid(
|
||||
async def test_config_entry_conn_params_invalid(
|
||||
hass: HomeAssistant,
|
||||
mock_discovery: AsyncMock,
|
||||
mock_connect: AsyncMock,
|
||||
@ -224,7 +241,7 @@ async def test_config_entry_device_config_invalid(
|
||||
) -> None:
|
||||
"""Test that an invalid device config logs an error and loads the config entry."""
|
||||
entry_data = copy.deepcopy(CREATE_ENTRY_DATA_KLAP)
|
||||
entry_data[CONF_DEVICE_CONFIG] = {"foo": "bar"}
|
||||
entry_data[CONF_CONNECTION_PARAMETERS] = {"foo": "bar"}
|
||||
mock_config_entry = MockConfigEntry(
|
||||
title="TPLink",
|
||||
domain=DOMAIN,
|
||||
@ -237,7 +254,7 @@ async def test_config_entry_device_config_invalid(
|
||||
assert mock_config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
assert (
|
||||
f"Invalid connection type dict for {IP_ADDRESS}: {entry_data.get(CONF_DEVICE_CONFIG)}"
|
||||
f"Invalid connection parameters dict for {IP_ADDRESS}: {entry_data.get(CONF_CONNECTION_PARAMETERS)}"
|
||||
in caplog.text
|
||||
)
|
||||
|
||||
@ -495,8 +512,9 @@ async def test_unlink_devices(
|
||||
}
|
||||
assert device_entries[0].identifiers == set(test_identifiers)
|
||||
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
with patch("homeassistant.components.tplink.CONF_CONFIG_ENTRY_MINOR_VERSION", 3):
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
device_entries = dr.async_entries_for_config_entry(device_registry, entry.entry_id)
|
||||
|
||||
@ -504,7 +522,7 @@ async def test_unlink_devices(
|
||||
|
||||
assert device_entries[0].identifiers == set(expected_identifiers)
|
||||
assert entry.version == 1
|
||||
assert entry.minor_version == 4
|
||||
assert entry.minor_version == 3
|
||||
|
||||
assert update_msg in caplog.text
|
||||
assert "Migration to version 1.3 complete" in caplog.text
|
||||
@ -545,6 +563,7 @@ async def test_move_credentials_hash(
|
||||
with (
|
||||
patch("homeassistant.components.tplink.Device.connect", new=_connect),
|
||||
patch("homeassistant.components.tplink.PLATFORMS", []),
|
||||
patch("homeassistant.components.tplink.CONF_CONFIG_ENTRY_MINOR_VERSION", 4),
|
||||
):
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
@ -589,6 +608,7 @@ async def test_move_credentials_hash_auth_error(
|
||||
side_effect=AuthenticationError,
|
||||
),
|
||||
patch("homeassistant.components.tplink.PLATFORMS", []),
|
||||
patch("homeassistant.components.tplink.CONF_CONFIG_ENTRY_MINOR_VERSION", 4),
|
||||
):
|
||||
entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
@ -631,6 +651,7 @@ async def test_move_credentials_hash_other_error(
|
||||
"homeassistant.components.tplink.Device.connect", side_effect=KasaException
|
||||
),
|
||||
patch("homeassistant.components.tplink.PLATFORMS", []),
|
||||
patch("homeassistant.components.tplink.CONF_CONFIG_ENTRY_MINOR_VERSION", 4),
|
||||
):
|
||||
entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
@ -647,10 +668,8 @@ async def test_credentials_hash(
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
"""Test credentials_hash used to call connect."""
|
||||
device_config = {**DEVICE_CONFIG_KLAP.to_dict(exclude_credentials=True)}
|
||||
entry_data = {
|
||||
**CREATE_ENTRY_DATA_KLAP,
|
||||
CONF_DEVICE_CONFIG: device_config,
|
||||
CONF_CREDENTIALS_HASH: "theHash",
|
||||
}
|
||||
|
||||
@ -674,9 +693,7 @@ async def test_credentials_hash(
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert entry.state is ConfigEntryState.LOADED
|
||||
assert CONF_CREDENTIALS_HASH not in entry.data[CONF_DEVICE_CONFIG]
|
||||
assert CONF_CREDENTIALS_HASH in entry.data
|
||||
assert entry.data[CONF_DEVICE_CONFIG] == device_config
|
||||
assert entry.data[CONF_CREDENTIALS_HASH] == "theHash"
|
||||
|
||||
|
||||
@ -684,10 +701,8 @@ async def test_credentials_hash_auth_error(
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
"""Test credentials_hash is deleted after an auth failure."""
|
||||
device_config = {**DEVICE_CONFIG_KLAP.to_dict(exclude_credentials=True)}
|
||||
entry_data = {
|
||||
**CREATE_ENTRY_DATA_KLAP,
|
||||
CONF_DEVICE_CONFIG: device_config,
|
||||
CONF_CREDENTIALS_HASH: "theHash",
|
||||
}
|
||||
|
||||
@ -700,6 +715,10 @@ async def test_credentials_hash_auth_error(
|
||||
|
||||
with (
|
||||
patch("homeassistant.components.tplink.PLATFORMS", []),
|
||||
patch(
|
||||
"homeassistant.components.tplink.async_create_clientsession",
|
||||
return_value="Foo",
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.tplink.Device.connect",
|
||||
side_effect=AuthenticationError,
|
||||
@ -712,6 +731,76 @@ async def test_credentials_hash_auth_error(
|
||||
expected_config = DeviceConfig.from_dict(
|
||||
DEVICE_CONFIG_KLAP.to_dict(exclude_credentials=True, credentials_hash="theHash")
|
||||
)
|
||||
expected_config.uses_http = False
|
||||
expected_config.http_client = "Foo"
|
||||
connect_mock.assert_called_with(config=expected_config)
|
||||
assert entry.state is ConfigEntryState.SETUP_ERROR
|
||||
assert CONF_CREDENTIALS_HASH not in entry.data
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("device_config", "expected_entry_data", "credentials_hash"),
|
||||
[
|
||||
pytest.param(
|
||||
DEVICE_CONFIG_KLAP, CREATE_ENTRY_DATA_KLAP, CREDENTIALS_HASH_KLAP, id="KLAP"
|
||||
),
|
||||
pytest.param(
|
||||
DEVICE_CONFIG_AES, CREATE_ENTRY_DATA_AES, CREDENTIALS_HASH_AES, id="AES"
|
||||
),
|
||||
pytest.param(DEVICE_CONFIG_LEGACY, CREATE_ENTRY_DATA_LEGACY, None, id="Legacy"),
|
||||
],
|
||||
)
|
||||
async def test_migrate_remove_device_config(
|
||||
hass: HomeAssistant,
|
||||
mock_connect: AsyncMock,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
device_config: DeviceConfig,
|
||||
expected_entry_data: dict[str, Any],
|
||||
credentials_hash: str,
|
||||
) -> None:
|
||||
"""Test credentials hash moved to parent.
|
||||
|
||||
As async_setup_entry will succeed the hash on the parent is updated
|
||||
from the device.
|
||||
"""
|
||||
OLD_CREATE_ENTRY_DATA = {
|
||||
CONF_HOST: expected_entry_data[CONF_HOST],
|
||||
CONF_ALIAS: ALIAS,
|
||||
CONF_MODEL: MODEL,
|
||||
CONF_DEVICE_CONFIG: device_config.to_dict(exclude_credentials=True),
|
||||
}
|
||||
|
||||
entry = MockConfigEntry(
|
||||
title="TPLink",
|
||||
domain=DOMAIN,
|
||||
data=OLD_CREATE_ENTRY_DATA,
|
||||
entry_id="123456",
|
||||
unique_id=MAC_ADDRESS,
|
||||
version=1,
|
||||
minor_version=4,
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
async def _connect(config):
|
||||
config.credentials_hash = credentials_hash
|
||||
config.aes_keys = expected_entry_data.get(CONF_AES_KEYS)
|
||||
return _mocked_device(device_config=config, credentials_hash=credentials_hash)
|
||||
|
||||
with (
|
||||
patch("homeassistant.components.tplink.Device.connect", new=_connect),
|
||||
patch("homeassistant.components.tplink.PLATFORMS", []),
|
||||
patch(
|
||||
"homeassistant.components.tplink.async_create_clientsession",
|
||||
return_value="Foo",
|
||||
),
|
||||
patch("homeassistant.components.tplink.CONF_CONFIG_ENTRY_MINOR_VERSION", 5),
|
||||
):
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert entry.minor_version == 5
|
||||
assert entry.state is ConfigEntryState.LOADED
|
||||
assert CONF_DEVICE_CONFIG not in entry.data
|
||||
assert entry.data == expected_entry_data
|
||||
|
||||
assert "Migration to version 1.5 complete" in caplog.text
|
||||
|
@ -82,10 +82,10 @@ def get_fixture_data() -> dict[str, Any]:
|
||||
def get_update_data(loaded_fixture: dict[str, Any]) -> YaleSmartAlarmData:
|
||||
"""Load update data and return."""
|
||||
|
||||
status = loaded_fixture["STATUS"]
|
||||
cycle = loaded_fixture["CYCLE"]
|
||||
online = loaded_fixture["ONLINE"]
|
||||
panel_info = loaded_fixture["PANEL INFO"]
|
||||
status = {"data": loaded_fixture["STATUS"]}
|
||||
cycle = {"data": loaded_fixture["CYCLE"]}
|
||||
online = {"data": loaded_fixture["ONLINE"]}
|
||||
panel_info = {"data": loaded_fixture["PANEL INFO"]}
|
||||
return YaleSmartAlarmData(
|
||||
status=status,
|
||||
cycle=cycle,
|
||||
@ -98,14 +98,14 @@ def get_update_data(loaded_fixture: dict[str, Any]) -> YaleSmartAlarmData:
|
||||
def get_diag_data(loaded_fixture: dict[str, Any]) -> YaleSmartAlarmData:
|
||||
"""Load all data and return."""
|
||||
|
||||
devices = loaded_fixture["DEVICES"]
|
||||
mode = loaded_fixture["MODE"]
|
||||
status = loaded_fixture["STATUS"]
|
||||
cycle = loaded_fixture["CYCLE"]
|
||||
online = loaded_fixture["ONLINE"]
|
||||
history = loaded_fixture["HISTORY"]
|
||||
panel_info = loaded_fixture["PANEL INFO"]
|
||||
auth_check = loaded_fixture["AUTH CHECK"]
|
||||
devices = {"data": loaded_fixture["DEVICES"]}
|
||||
mode = {"data": loaded_fixture["MODE"]}
|
||||
status = {"data": loaded_fixture["STATUS"]}
|
||||
cycle = {"data": loaded_fixture["CYCLE"]}
|
||||
online = {"data": loaded_fixture["ONLINE"]}
|
||||
history = {"data": loaded_fixture["HISTORY"]}
|
||||
panel_info = {"data": loaded_fixture["PANEL INFO"]}
|
||||
auth_check = {"data": loaded_fixture["AUTH CHECK"]}
|
||||
return YaleSmartAlarmData(
|
||||
devices=devices,
|
||||
mode=mode,
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -55,7 +55,7 @@ async def test_lock_service_calls(
|
||||
client = load_config_entry[1]
|
||||
|
||||
data = deepcopy(get_data.cycle)
|
||||
data["data"] = data.pop("device_status")
|
||||
data["data"] = data["data"].pop("device_status")
|
||||
|
||||
client.auth.get_authenticated = Mock(return_value=data)
|
||||
client.auth.post_authenticated = Mock(return_value={"code": "000"})
|
||||
@ -109,7 +109,7 @@ async def test_lock_service_call_fails(
|
||||
client = load_config_entry[1]
|
||||
|
||||
data = deepcopy(get_data.cycle)
|
||||
data["data"] = data.pop("device_status")
|
||||
data["data"] = data["data"].pop("device_status")
|
||||
|
||||
client.auth.get_authenticated = Mock(return_value=data)
|
||||
client.auth.post_authenticated = Mock(side_effect=UnknownError("test_side_effect"))
|
||||
@ -161,7 +161,7 @@ async def test_lock_service_call_fails_with_incorrect_status(
|
||||
client = load_config_entry[1]
|
||||
|
||||
data = deepcopy(get_data.cycle)
|
||||
data["data"] = data.pop("device_status")
|
||||
data["data"] = data["data"].pop("device_status")
|
||||
|
||||
client.auth.get_authenticated = Mock(return_value=data)
|
||||
client.auth.post_authenticated = Mock(return_value={"code": "FFF"})
|
||||
|
@ -5,16 +5,23 @@ from typing import Any
|
||||
|
||||
import pytest
|
||||
import voluptuous_serialize
|
||||
from zigpy.application import ControllerApplication
|
||||
from zigpy.types.basic import uint16_t
|
||||
from zigpy.zcl.clusters import lighting
|
||||
|
||||
import homeassistant.components.zha.const as zha_const
|
||||
from homeassistant.components.zha.helpers import (
|
||||
cluster_command_schema_to_vol_schema,
|
||||
convert_to_zcl_values,
|
||||
create_zha_config,
|
||||
exclude_none_values,
|
||||
get_zha_data,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -177,3 +184,35 @@ def test_exclude_none_values(
|
||||
|
||||
for key in expected_output:
|
||||
assert expected_output[key] == obj[key]
|
||||
|
||||
|
||||
async def test_create_zha_config_remove_unused(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
mock_zigpy_connect: ControllerApplication,
|
||||
) -> None:
|
||||
"""Test creating ZHA config data with unused keys."""
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
options = config_entry.options.copy()
|
||||
options["custom_configuration"]["zha_options"]["some_random_key"] = "a value"
|
||||
|
||||
hass.config_entries.async_update_entry(config_entry, options=options)
|
||||
|
||||
assert (
|
||||
config_entry.options["custom_configuration"]["zha_options"]["some_random_key"]
|
||||
== "a value"
|
||||
)
|
||||
|
||||
status = await async_setup_component(
|
||||
hass,
|
||||
zha_const.DOMAIN,
|
||||
{zha_const.DOMAIN: {zha_const.CONF_ENABLE_QUIRKS: False}},
|
||||
)
|
||||
assert status is True
|
||||
await hass.async_block_till_done()
|
||||
|
||||
ha_zha_data = get_zha_data(hass)
|
||||
|
||||
# Does not error out
|
||||
create_zha_config(hass, ha_zha_data)
|
||||
|
Loading…
x
Reference in New Issue
Block a user