This commit is contained in:
Franck Nijhof 2024-09-16 19:44:37 +02:00 committed by GitHub
commit b69b5aa82a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
68 changed files with 2078 additions and 1332 deletions

View File

@ -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"]
}

View File

@ -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):

View File

@ -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(),
}

View File

@ -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.",

View File

@ -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,

View File

@ -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"]
}

View File

@ -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"]
}

View File

@ -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.",

View File

@ -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%]",

View File

@ -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%]",

View File

@ -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."""

View File

@ -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
}

View File

@ -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]

View File

@ -22,5 +22,5 @@
"integration_type": "device",
"iot_class": "cloud_polling",
"loggers": ["lmcloud"],
"requirements": ["lmcloud==1.2.2"]
"requirements": ["lmcloud==1.2.3"]
}

View File

@ -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"
]
}

View File

@ -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

View File

@ -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))

View File

@ -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"]
}

View File

@ -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"]
}

View File

@ -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")
],

View File

@ -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(

View File

@ -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"
}

View File

@ -7,5 +7,5 @@
"iot_class": "local_push",
"loggers": ["aiorussound"],
"quality_scale": "silver",
"requirements": ["aiorussound==3.0.4"]
"requirements": ["aiorussound==3.0.5"]
}

View File

@ -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"]
}

View File

@ -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(

View File

@ -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

View File

@ -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."""

View File

@ -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()

View File

@ -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"]
}

View File

@ -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()

View File

@ -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)

View File

@ -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

View File

@ -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
)

View File

@ -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,

View File

@ -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"]
}

View File

@ -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"]
}

View File

@ -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,
}

View File

@ -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(

View File

@ -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",

View File

@ -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)

View File

@ -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

View File

@ -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"

View File

@ -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

View File

@ -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

View File

@ -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 = (

View File

@ -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

View File

@ -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': '&deg;C',
'value': '18.6',
}),
'hvac_action': dict({
'data_type': 1,
'desc': 'Raumtempbegrenzung',
'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': '&deg;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': '&deg;C',
'value': '18.6',
'value': '20.0',
}),
'hvac_action': dict({
'data_type': 1,
'desc': 'Raumtempbegrenzung',
'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': '&deg;C',
'value': '18.5',
'value': '8.0',
}),
}),
})

View File

@ -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"
)

View File

@ -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,

View File

@ -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)

View File

@ -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)

View File

@ -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),

View File

@ -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 (

View File

@ -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",
},

View File

@ -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',
]),

View File

@ -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([])

View File

@ -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()

View File

@ -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',

View File

@ -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

View File

@ -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,

View File

@ -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)

View File

@ -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,

View File

@ -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={

View File

@ -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

View File

@ -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,

View File

@ -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"})

View File

@ -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)