Merge pull request #67730 from home-assistant/rc

This commit is contained in:
Paulus Schoutsen
2022-03-06 12:11:16 -08:00
committed by GitHub
32 changed files with 327 additions and 141 deletions

View File

@@ -204,13 +204,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
# Init ip webcam
cam = PyDroidIPCam(
hass.loop,
websession,
host,
cam_config[CONF_PORT],
username=username,
password=password,
timeout=cam_config[CONF_TIMEOUT],
ssl=False,
)
if switches is None:

View File

@@ -2,7 +2,7 @@
"domain": "android_ip_webcam",
"name": "Android IP Webcam",
"documentation": "https://www.home-assistant.io/integrations/android_ip_webcam",
"requirements": ["pydroid-ipcam==0.8"],
"requirements": ["pydroid-ipcam==1.3.1"],
"codeowners": [],
"iot_class": "local_polling"
}

View File

@@ -279,9 +279,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
keypad.add_callback(_element_changed)
try:
if not await async_wait_for_elk_to_sync(
elk, LOGIN_TIMEOUT, SYNC_TIMEOUT, bool(conf[CONF_USERNAME])
):
if not await async_wait_for_elk_to_sync(elk, LOGIN_TIMEOUT, SYNC_TIMEOUT):
return False
except asyncio.TimeoutError as exc:
raise ConfigEntryNotReady(f"Timed out connecting to {conf[CONF_HOST]}") from exc
@@ -334,7 +332,6 @@ async def async_wait_for_elk_to_sync(
elk: elkm1.Elk,
login_timeout: int,
sync_timeout: int,
password_auth: bool,
) -> bool:
"""Wait until the elk has finished sync. Can fail login or timeout."""
@@ -354,18 +351,23 @@ async def async_wait_for_elk_to_sync(
login_event.set()
sync_event.set()
def first_response(*args, **kwargs):
_LOGGER.debug("ElkM1 received first response (VN)")
login_event.set()
def sync_complete():
sync_event.set()
success = True
elk.add_handler("login", login_status)
# VN is the first command sent for panel, when we get
# it back we now we are logged in either with or without a password
elk.add_handler("VN", first_response)
elk.add_handler("sync_complete", sync_complete)
events = []
if password_auth:
events.append(("login", login_event, login_timeout))
events.append(("sync_complete", sync_event, sync_timeout))
for name, event, timeout in events:
for name, event, timeout in (
("login", login_event, login_timeout),
("sync_complete", sync_event, sync_timeout),
):
_LOGGER.debug("Waiting for %s event for %s seconds", name, timeout)
try:
async with async_timeout.timeout(timeout):

View File

@@ -81,10 +81,11 @@ async def validate_input(data: dict[str, str], mac: str | None) -> dict[str, str
)
elk.connect()
if not await async_wait_for_elk_to_sync(
elk, LOGIN_TIMEOUT, VALIDATE_TIMEOUT, bool(userid)
):
raise InvalidAuth
try:
if not await async_wait_for_elk_to_sync(elk, LOGIN_TIMEOUT, VALIDATE_TIMEOUT):
raise InvalidAuth
finally:
elk.disconnect()
short_mac = _short_mac(mac) if mac else None
if prefix and prefix != short_mac:
@@ -227,7 +228,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
try:
info = await validate_input(user_input, self.unique_id)
except asyncio.TimeoutError:
return {CONF_HOST: "cannot_connect"}, None
return {"base": "cannot_connect"}, None
except InvalidAuth:
return {CONF_PASSWORD: "invalid_auth"}, None
except Exception: # pylint: disable=broad-except
@@ -287,9 +288,13 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
if device := await async_discover_device(
self.hass, user_input[CONF_ADDRESS]
):
await self.async_set_unique_id(dr.format_mac(device.mac_address))
await self.async_set_unique_id(
dr.format_mac(device.mac_address), raise_on_progress=False
)
self._abort_if_unique_id_configured()
user_input[CONF_ADDRESS] = f"{device.ip_address}:{device.port}"
# Ignore the port from discovery since its always going to be
# 2601 if secure is turned on even though they may want insecure
user_input[CONF_ADDRESS] = device.ip_address
errors, result = await self._async_create_or_error(user_input, False)
if not errors:
return result
@@ -324,7 +329,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
if is_ip_address(host) and (
device := await async_discover_device(self.hass, host)
):
await self.async_set_unique_id(dr.format_mac(device.mac_address))
await self.async_set_unique_id(
dr.format_mac(device.mac_address), raise_on_progress=False
)
self._abort_if_unique_id_configured()
return (await self._async_create_or_error(user_input, True))[1]

View File

@@ -1,6 +1,7 @@
"""AVM FRITZ!Box connectivity sensor."""
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
import logging
@@ -14,8 +15,8 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import EntityCategory
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .common import AvmWrapper, FritzBoxBaseEntity
from .const import DOMAIN, MeshRoles
from .common import AvmWrapper, ConnectionInfo, FritzBoxBaseEntity
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
@@ -24,7 +25,7 @@ _LOGGER = logging.getLogger(__name__)
class FritzBinarySensorEntityDescription(BinarySensorEntityDescription):
"""Describes Fritz sensor entity."""
exclude_mesh_role: MeshRoles = MeshRoles.SLAVE
is_suitable: Callable[[ConnectionInfo], bool] = lambda info: info.wan_enabled
SENSOR_TYPES: tuple[FritzBinarySensorEntityDescription, ...] = (
@@ -45,7 +46,7 @@ SENSOR_TYPES: tuple[FritzBinarySensorEntityDescription, ...] = (
name="Firmware Update",
device_class=BinarySensorDeviceClass.UPDATE,
entity_category=EntityCategory.DIAGNOSTIC,
exclude_mesh_role=MeshRoles.NONE,
is_suitable=lambda info: True,
),
)
@@ -57,10 +58,12 @@ async def async_setup_entry(
_LOGGER.debug("Setting up FRITZ!Box binary sensors")
avm_wrapper: AvmWrapper = hass.data[DOMAIN][entry.entry_id]
connection_info = await avm_wrapper.async_get_connection_info()
entities = [
FritzBoxBinarySensor(avm_wrapper, entry.title, description)
for description in SENSOR_TYPES
if (description.exclude_mesh_role != avm_wrapper.mesh_role)
if description.is_suitable(connection_info)
]
async_add_entities(entities, True)

View File

@@ -642,6 +642,22 @@ class AvmWrapper(FritzBoxTools):
partial(self.get_wan_link_properties)
)
async def async_get_connection_info(self) -> ConnectionInfo:
"""Return ConnectionInfo data."""
link_properties = await self.async_get_wan_link_properties()
connection_info = ConnectionInfo(
connection=link_properties.get("NewWANAccessType", "").lower(),
mesh_role=self.mesh_role,
wan_enabled=self.device_is_router,
)
_LOGGER.debug(
"ConnectionInfo for FritzBox %s: %s",
self.host,
connection_info,
)
return connection_info
async def async_get_port_mapping(self, con_type: str, index: int) -> dict[str, Any]:
"""Call GetGenericPortMappingEntry action."""
@@ -970,3 +986,12 @@ class FritzBoxBaseEntity:
name=self._device_name,
sw_version=self._avm_wrapper.current_firmware,
)
@dataclass
class ConnectionInfo:
"""Fritz sensor connection information class."""
connection: str
mesh_role: MeshRoles
wan_enabled: bool

View File

@@ -57,6 +57,7 @@ SERVICE_SET_GUEST_WIFI_PW = "set_guest_wifi_password"
SWITCH_TYPE_DEFLECTION = "CallDeflection"
SWITCH_TYPE_PORTFORWARD = "PortForward"
SWITCH_TYPE_PROFILE = "Profile"
SWITCH_TYPE_WIFINETWORK = "WiFiNetwork"
UPTIME_DEVIATION = 5

View File

@@ -22,6 +22,9 @@ async def async_get_config_entry_diagnostics(
"entry": async_redact_data(entry.as_dict(), TO_REDACT),
"device_info": {
"model": avm_wrapper.model,
"unique_id": avm_wrapper.unique_id.replace(
avm_wrapper.unique_id[6:11], "XX:XX"
),
"current_firmware": avm_wrapper.current_firmware,
"latest_firmware": avm_wrapper.latest_firmware,
"update_available": avm_wrapper.update_available,

View File

@@ -28,8 +28,8 @@ from homeassistant.helpers.entity import EntityCategory
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util.dt import utcnow
from .common import AvmWrapper, FritzBoxBaseEntity
from .const import DOMAIN, DSL_CONNECTION, UPTIME_DEVIATION, MeshRoles
from .common import AvmWrapper, ConnectionInfo, FritzBoxBaseEntity
from .const import DOMAIN, DSL_CONNECTION, UPTIME_DEVIATION
_LOGGER = logging.getLogger(__name__)
@@ -134,15 +134,6 @@ def _retrieve_link_attenuation_received_state(
return status.attenuation[1] / 10 # type: ignore[no-any-return]
@dataclass
class ConnectionInfo:
"""Fritz sensor connection information class."""
connection: str
mesh_role: MeshRoles
wan_enabled: bool
@dataclass
class FritzRequireKeysMixin:
"""Fritz sensor data class."""
@@ -283,18 +274,7 @@ async def async_setup_entry(
_LOGGER.debug("Setting up FRITZ!Box sensors")
avm_wrapper: AvmWrapper = hass.data[DOMAIN][entry.entry_id]
link_properties = await avm_wrapper.async_get_wan_link_properties()
connection_info = ConnectionInfo(
connection=link_properties.get("NewWANAccessType", "").lower(),
mesh_role=avm_wrapper.mesh_role,
wan_enabled=avm_wrapper.device_is_router,
)
_LOGGER.debug(
"ConnectionInfo for FritzBox %s: %s",
avm_wrapper.host,
connection_info,
)
connection_info = await avm_wrapper.async_get_connection_info()
entities = [
FritzBoxSensor(avm_wrapper, entry.title, description)

View File

@@ -30,6 +30,7 @@ from .const import (
DOMAIN,
SWITCH_TYPE_DEFLECTION,
SWITCH_TYPE_PORTFORWARD,
SWITCH_TYPE_PROFILE,
SWITCH_TYPE_WIFINETWORK,
WIFI_STANDARD,
MeshRoles,
@@ -185,6 +186,7 @@ def profile_entities_list(
data_fritz: FritzData,
) -> list[FritzBoxProfileSwitch]:
"""Add new tracker entities from the AVM device."""
_LOGGER.debug("Setting up %s switches", SWITCH_TYPE_PROFILE)
new_profiles: list[FritzBoxProfileSwitch] = []
@@ -198,11 +200,15 @@ def profile_entities_list(
if device_filter_out_from_trackers(
mac, device, data_fritz.profile_switches.values()
):
_LOGGER.debug(
"Skipping profile switch creation for device %s", device.hostname
)
continue
new_profiles.append(FritzBoxProfileSwitch(avm_wrapper, device))
data_fritz.profile_switches[avm_wrapper.unique_id].add(mac)
_LOGGER.debug("Creating %s profile switches", len(new_profiles))
return new_profiles

View File

@@ -59,11 +59,12 @@ SERVICE_SET = "set"
SERVICE_REMOVE = "remove"
PLATFORMS = [
Platform.LIGHT,
Platform.COVER,
Platform.NOTIFY,
Platform.FAN,
Platform.BINARY_SENSOR,
Platform.COVER,
Platform.FAN,
Platform.LIGHT,
Platform.MEDIA_PLAYER,
Platform.NOTIFY,
]
REG_KEY = f"{DOMAIN}_registry"

View File

@@ -3,7 +3,7 @@
"name": "HomeKit Controller",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/homekit_controller",
"requirements": ["aiohomekit==0.7.15"],
"requirements": ["aiohomekit==0.7.16"],
"zeroconf": ["_hap._tcp.local."],
"after_dependencies": ["zeroconf"],
"codeowners": ["@Jc2k", "@bdraco"],

View File

@@ -3,7 +3,7 @@
"name": "LIFX",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/lifx",
"requirements": ["aiolifx==0.7.0", "aiolifx_effects==0.2.2"],
"requirements": ["aiolifx==0.7.1", "aiolifx_effects==0.2.2"],
"homekit": {
"models": ["LIFX"]
},

View File

@@ -104,7 +104,7 @@ class RenaultVehicleProxy:
coordinator = self.coordinators[key]
if coordinator.not_supported:
# Remove endpoint as it is not supported for this vehicle.
LOGGER.warning(
LOGGER.info(
"Ignoring endpoint %s as it is not supported for this vehicle: %s",
coordinator.name,
coordinator.last_exception,
@@ -112,7 +112,7 @@ class RenaultVehicleProxy:
del self.coordinators[key]
elif coordinator.access_denied:
# Remove endpoint as it is denied for this vehicle.
LOGGER.warning(
LOGGER.info(
"Ignoring endpoint %s as it is denied for this vehicle: %s",
coordinator.name,
coordinator.last_exception,

View File

@@ -2,7 +2,7 @@
"domain": "rfxtrx",
"name": "RFXCOM RFXtrx",
"documentation": "https://www.home-assistant.io/integrations/rfxtrx",
"requirements": ["pyRFXtrx==0.27.1"],
"requirements": ["pyRFXtrx==0.28.0"],
"codeowners": ["@danielhiversen", "@elupus", "@RobBie1221"],
"config_flow": true,
"iot_class": "local_push",

View File

@@ -1,14 +1,6 @@
"""Support for Roku."""
from __future__ import annotations
from collections.abc import Awaitable, Callable, Coroutine
from functools import wraps
import logging
from typing import Any, TypeVar
from rokuecp import RokuConnectionError, RokuError
from typing_extensions import Concatenate, ParamSpec
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, Platform
from homeassistant.core import HomeAssistant
@@ -16,7 +8,6 @@ from homeassistant.helpers import config_validation as cv
from .const import DOMAIN
from .coordinator import RokuDataUpdateCoordinator
from .entity import RokuEntity
CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False)
@@ -27,10 +18,6 @@ PLATFORMS = [
Platform.SELECT,
Platform.SENSOR,
]
_LOGGER = logging.getLogger(__name__)
_T = TypeVar("_T", bound="RokuEntity")
_P = ParamSpec("_P")
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
@@ -53,22 +40,3 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok
def roku_exception_handler(
func: Callable[Concatenate[_T, _P], Awaitable[None]] # type: ignore[misc]
) -> Callable[Concatenate[_T, _P], Coroutine[Any, Any, None]]: # type: ignore[misc]
"""Decorate Roku calls to handle Roku exceptions."""
@wraps(func)
async def wrapper(self: _T, *args: _P.args, **kwargs: _P.kwargs) -> None:
try:
await func(self, *args, **kwargs)
except RokuConnectionError as error:
if self.available:
_LOGGER.error("Error communicating with API: %s", error)
except RokuError as error:
if self.available:
_LOGGER.error("Invalid response from API: %s", error)
return wrapper

View File

@@ -1,6 +1,21 @@
"""Helpers for Roku."""
from __future__ import annotations
from collections.abc import Awaitable, Callable, Coroutine
from functools import wraps
import logging
from typing import Any, TypeVar
from rokuecp import RokuConnectionError, RokuConnectionTimeoutError, RokuError
from typing_extensions import Concatenate, ParamSpec
from .entity import RokuEntity
_LOGGER = logging.getLogger(__name__)
_T = TypeVar("_T", bound=RokuEntity)
_P = ParamSpec("_P")
def format_channel_name(channel_number: str, channel_name: str | None = None) -> str:
"""Format a Roku Channel name."""
@@ -8,3 +23,28 @@ def format_channel_name(channel_number: str, channel_name: str | None = None) ->
return f"{channel_name} ({channel_number})"
return channel_number
def roku_exception_handler(ignore_timeout: bool = False) -> Callable[..., Callable]:
"""Decorate Roku calls to handle Roku exceptions."""
def decorator(
func: Callable[Concatenate[_T, _P], Awaitable[None]], # type: ignore[misc]
) -> Callable[Concatenate[_T, _P], Coroutine[Any, Any, None]]: # type: ignore[misc]
@wraps(func)
async def wrapper(self: _T, *args: _P.args, **kwargs: _P.kwargs) -> None:
try:
await func(self, *args, **kwargs)
except RokuConnectionTimeoutError as error:
if not ignore_timeout and self.available:
_LOGGER.error("Error communicating with API: %s", error)
except RokuConnectionError as error:
if self.available:
_LOGGER.error("Error communicating with API: %s", error)
except RokuError as error:
if self.available:
_LOGGER.error("Invalid response from API: %s", error)
return wrapper
return decorator

View File

@@ -2,7 +2,7 @@
"domain": "roku",
"name": "Roku",
"documentation": "https://www.home-assistant.io/integrations/roku",
"requirements": ["rokuecp==0.14.1"],
"requirements": ["rokuecp==0.15.0"],
"homekit": {
"models": ["3810X", "4660X", "7820X", "C105X", "C135X"]
},

View File

@@ -51,7 +51,6 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_platform
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import roku_exception_handler
from .browse_media import async_browse_media
from .const import (
ATTR_ARTIST_NAME,
@@ -65,7 +64,7 @@ from .const import (
)
from .coordinator import RokuDataUpdateCoordinator
from .entity import RokuEntity
from .helpers import format_channel_name
from .helpers import format_channel_name, roku_exception_handler
_LOGGER = logging.getLogger(__name__)
@@ -289,7 +288,7 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity):
app.name for app in self.coordinator.data.apps if app.name is not None
)
@roku_exception_handler
@roku_exception_handler()
async def search(self, keyword: str) -> None:
"""Emulate opening the search screen and entering the search keyword."""
await self.coordinator.roku.search(keyword)
@@ -321,68 +320,68 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity):
media_content_type,
)
@roku_exception_handler
@roku_exception_handler()
async def async_turn_on(self) -> None:
"""Turn on the Roku."""
await self.coordinator.roku.remote("poweron")
await self.coordinator.async_request_refresh()
@roku_exception_handler
@roku_exception_handler(ignore_timeout=True)
async def async_turn_off(self) -> None:
"""Turn off the Roku."""
await self.coordinator.roku.remote("poweroff")
await self.coordinator.async_request_refresh()
@roku_exception_handler
@roku_exception_handler()
async def async_media_pause(self) -> None:
"""Send pause command."""
if self.state not in (STATE_STANDBY, STATE_PAUSED):
await self.coordinator.roku.remote("play")
await self.coordinator.async_request_refresh()
@roku_exception_handler
@roku_exception_handler()
async def async_media_play(self) -> None:
"""Send play command."""
if self.state not in (STATE_STANDBY, STATE_PLAYING):
await self.coordinator.roku.remote("play")
await self.coordinator.async_request_refresh()
@roku_exception_handler
@roku_exception_handler()
async def async_media_play_pause(self) -> None:
"""Send play/pause command."""
if self.state != STATE_STANDBY:
await self.coordinator.roku.remote("play")
await self.coordinator.async_request_refresh()
@roku_exception_handler
@roku_exception_handler()
async def async_media_previous_track(self) -> None:
"""Send previous track command."""
await self.coordinator.roku.remote("reverse")
await self.coordinator.async_request_refresh()
@roku_exception_handler
@roku_exception_handler()
async def async_media_next_track(self) -> None:
"""Send next track command."""
await self.coordinator.roku.remote("forward")
await self.coordinator.async_request_refresh()
@roku_exception_handler
@roku_exception_handler()
async def async_mute_volume(self, mute: bool) -> None:
"""Mute the volume."""
await self.coordinator.roku.remote("volume_mute")
await self.coordinator.async_request_refresh()
@roku_exception_handler
@roku_exception_handler()
async def async_volume_up(self) -> None:
"""Volume up media player."""
await self.coordinator.roku.remote("volume_up")
@roku_exception_handler
@roku_exception_handler()
async def async_volume_down(self) -> None:
"""Volume down media player."""
await self.coordinator.roku.remote("volume_down")
@roku_exception_handler
@roku_exception_handler()
async def async_play_media(
self, media_type: str, media_id: str, **kwargs: Any
) -> None:
@@ -487,7 +486,7 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity):
await self.coordinator.async_request_refresh()
@roku_exception_handler
@roku_exception_handler()
async def async_select_source(self, source: str) -> None:
"""Select input source."""
if source == "Home":

View File

@@ -9,10 +9,10 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import roku_exception_handler
from .const import DOMAIN
from .coordinator import RokuDataUpdateCoordinator
from .entity import RokuEntity
from .helpers import roku_exception_handler
async def async_setup_entry(
@@ -44,19 +44,19 @@ class RokuRemote(RokuEntity, RemoteEntity):
"""Return true if device is on."""
return not self.coordinator.data.state.standby
@roku_exception_handler
@roku_exception_handler()
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the device on."""
await self.coordinator.roku.remote("poweron")
await self.coordinator.async_request_refresh()
@roku_exception_handler
@roku_exception_handler(ignore_timeout=True)
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the device off."""
await self.coordinator.roku.remote("poweroff")
await self.coordinator.async_request_refresh()
@roku_exception_handler
@roku_exception_handler()
async def async_send_command(self, command: Iterable[str], **kwargs: Any) -> None:
"""Send a command to one device."""
num_repeats = kwargs[ATTR_NUM_REPEATS]

View File

@@ -12,11 +12,10 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import roku_exception_handler
from .const import DOMAIN
from .coordinator import RokuDataUpdateCoordinator
from .entity import RokuEntity
from .helpers import format_channel_name
from .helpers import format_channel_name, roku_exception_handler
@dataclass
@@ -163,7 +162,7 @@ class RokuSelectEntity(RokuEntity, SelectEntity):
"""Return a set of selectable options."""
return self.entity_description.options_fn(self.coordinator.data)
@roku_exception_handler
@roku_exception_handler()
async def async_select_option(self, option: str) -> None:
"""Set the option."""
await self.entity_description.set_fn(

View File

@@ -172,7 +172,7 @@ class SQLSensor(SensorEntity):
else:
self._attr_native_value = data
if not data:
if data is None:
_LOGGER.warning("%s returned no results", self._query)
sess.close()

View File

@@ -277,8 +277,6 @@ class TemplateFan(TemplateEntity, FanEntity):
"""Turn off the fan."""
await self._off_script.async_run(context=self._context)
self._state = STATE_OFF
self._percentage = 0
self._preset_mode = None
async def async_set_percentage(self, percentage: int) -> None:
"""Set the percentage speed of the fan."""

View File

@@ -14,7 +14,6 @@ from miio import (
AirHumidifierMiot,
AirHumidifierMjjsq,
AirPurifier,
AirPurifierMB4,
AirPurifierMiot,
CleaningDetails,
CleaningSummary,
@@ -23,10 +22,8 @@ from miio import (
DNDStatus,
Fan,
Fan1C,
FanMiot,
FanP5,
FanP9,
FanP10,
FanP11,
FanZA5,
RoborockVacuum,
Timer,
@@ -52,7 +49,6 @@ from .const import (
KEY_DEVICE,
MODEL_AIRFRESH_A1,
MODEL_AIRFRESH_T2017,
MODEL_AIRPURIFIER_3C,
MODEL_FAN_1C,
MODEL_FAN_P5,
MODEL_FAN_P9,
@@ -111,10 +107,10 @@ AIR_MONITOR_PLATFORMS = [Platform.AIR_QUALITY, Platform.SENSOR]
MODEL_TO_CLASS_MAP = {
MODEL_FAN_1C: Fan1C,
MODEL_FAN_P10: FanP10,
MODEL_FAN_P11: FanP11,
MODEL_FAN_P9: FanMiot,
MODEL_FAN_P10: FanMiot,
MODEL_FAN_P11: FanMiot,
MODEL_FAN_P5: FanP5,
MODEL_FAN_P9: FanP9,
MODEL_FAN_ZA5: FanZA5,
}
@@ -314,8 +310,6 @@ async def async_create_miio_device_and_coordinator(
device = AirHumidifier(host, token, model=model)
migrate = True
# Airpurifiers and Airfresh
elif model == MODEL_AIRPURIFIER_3C:
device = AirPurifierMB4(host, token)
elif model in MODELS_PURIFIER_MIOT:
device = AirPurifierMiot(host, token)
elif model.startswith("zhimi.airpurifier."):

View File

@@ -7,7 +7,7 @@ from .backports.enum import StrEnum
MAJOR_VERSION: Final = 2022
MINOR_VERSION: Final = 3
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, 9, 0)

View File

@@ -184,7 +184,7 @@ aioguardian==2021.11.0
aioharmony==0.2.9
# homeassistant.components.homekit_controller
aiohomekit==0.7.15
aiohomekit==0.7.16
# homeassistant.components.emulated_hue
# homeassistant.components.http
@@ -206,7 +206,7 @@ aiokafka==0.6.0
aiokef==0.2.16
# homeassistant.components.lifx
aiolifx==0.7.0
aiolifx==0.7.1
# homeassistant.components.lifx
aiolifx_effects==0.2.2
@@ -1360,7 +1360,7 @@ pyMetEireann==2021.8.0
pyMetno==0.9.0
# homeassistant.components.rfxtrx
pyRFXtrx==0.27.1
pyRFXtrx==0.28.0
# homeassistant.components.switchmate
# pySwitchmate==0.4.6
@@ -1486,7 +1486,7 @@ pydispatcher==2.0.5
pydoods==1.0.2
# homeassistant.components.android_ip_webcam
pydroid-ipcam==0.8
pydroid-ipcam==1.3.1
# homeassistant.components.ebox
pyebox==1.1.4
@@ -2121,7 +2121,7 @@ rjpl==0.3.6
rocketchat-API==0.6.1
# homeassistant.components.roku
rokuecp==0.14.1
rokuecp==0.15.0
# homeassistant.components.roomba
roombapy==1.6.5

View File

@@ -134,7 +134,7 @@ aioguardian==2021.11.0
aioharmony==0.2.9
# homeassistant.components.homekit_controller
aiohomekit==0.7.15
aiohomekit==0.7.16
# homeassistant.components.emulated_hue
# homeassistant.components.http
@@ -858,7 +858,7 @@ pyMetEireann==2021.8.0
pyMetno==0.9.0
# homeassistant.components.rfxtrx
pyRFXtrx==0.27.1
pyRFXtrx==0.28.0
# homeassistant.components.tibber
pyTibber==0.22.1
@@ -1313,7 +1313,7 @@ rflink==0.0.62
ring_doorbell==0.7.2
# homeassistant.components.roku
rokuecp==0.14.1
rokuecp==0.15.0
# homeassistant.components.roomba
roombapy==1.6.5

View File

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

View File

@@ -73,6 +73,155 @@ async def test_form_user_with_secure_elk_no_discovery(hass):
assert len(mock_setup_entry.mock_calls) == 1
async def test_form_user_with_insecure_elk_skip_discovery(hass):
"""Test we can setup a insecure elk with skipping discovery."""
with _patch_discovery(), _patch_elk():
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=DHCP_DISCOVERY
)
await hass.async_block_till_done()
with _patch_discovery(no_device=True):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
await hass.async_block_till_done()
assert result["type"] == "form"
assert result["errors"] == {}
assert result["step_id"] == "manual_connection"
mocked_elk = mock_elk(invalid_auth=False, sync_complete=True)
with _patch_discovery(), _patch_elk(elk=mocked_elk), patch(
"homeassistant.components.elkm1.async_setup", return_value=True
) as mock_setup, patch(
"homeassistant.components.elkm1.async_setup_entry",
return_value=True,
) as mock_setup_entry:
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
"protocol": "non-secure",
"address": "1.2.3.4",
"username": "test-username",
"password": "test-password",
"prefix": "",
},
)
await hass.async_block_till_done()
assert result2["type"] == "create_entry"
assert result2["title"] == "ElkM1"
assert result2["data"] == {
"auto_configure": True,
"host": "elk://1.2.3.4",
"password": "test-password",
"prefix": "",
"username": "test-username",
}
assert len(mock_setup.mock_calls) == 1
assert len(mock_setup_entry.mock_calls) == 1
async def test_form_user_with_insecure_elk_no_discovery(hass):
"""Test we can setup a insecure elk."""
with _patch_discovery(), _patch_elk():
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=DHCP_DISCOVERY
)
await hass.async_block_till_done()
with _patch_discovery(no_device=True):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
await hass.async_block_till_done()
assert result["type"] == "form"
assert result["errors"] == {}
assert result["step_id"] == "manual_connection"
mocked_elk = mock_elk(invalid_auth=False, sync_complete=True)
with _patch_discovery(no_device=True), _patch_elk(elk=mocked_elk), patch(
"homeassistant.components.elkm1.async_setup", return_value=True
) as mock_setup, patch(
"homeassistant.components.elkm1.async_setup_entry",
return_value=True,
) as mock_setup_entry:
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
"protocol": "non-secure",
"address": "1.2.3.4",
"username": "test-username",
"password": "test-password",
"prefix": "",
},
)
await hass.async_block_till_done()
assert result2["type"] == "create_entry"
assert result2["title"] == "ElkM1"
assert result2["data"] == {
"auto_configure": True,
"host": "elk://1.2.3.4",
"password": "test-password",
"prefix": "",
"username": "test-username",
}
assert len(mock_setup.mock_calls) == 1
assert len(mock_setup_entry.mock_calls) == 1
async def test_form_user_with_insecure_elk_times_out(hass):
"""Test we can setup a insecure elk that times out."""
with _patch_discovery(), _patch_elk():
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=DHCP_DISCOVERY
)
await hass.async_block_till_done()
with _patch_discovery(no_device=True):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
await hass.async_block_till_done()
assert result["type"] == "form"
assert result["errors"] == {}
assert result["step_id"] == "manual_connection"
mocked_elk = mock_elk(invalid_auth=False, sync_complete=False)
with patch(
"homeassistant.components.elkm1.config_flow.VALIDATE_TIMEOUT",
0,
), patch(
"homeassistant.components.elkm1.config_flow.LOGIN_TIMEOUT", 0
), _patch_discovery(), _patch_elk(
elk=mocked_elk
):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
"protocol": "non-secure",
"address": "1.2.3.4",
"username": "test-username",
"password": "test-password",
"prefix": "",
},
)
await hass.async_block_till_done()
assert result2["type"] == RESULT_TYPE_FORM
assert result2["errors"] == {"base": "cannot_connect"}
async def test_form_user_with_secure_elk_no_discovery_ip_already_configured(hass):
"""Test we abort when we try to configure the same ip."""
config_entry = MockConfigEntry(
@@ -262,7 +411,7 @@ async def test_form_user_with_secure_elk_with_discovery_pick_manual_direct_disco
assert result3["title"] == "ElkM1 ddeeff"
assert result3["data"] == {
"auto_configure": True,
"host": "elks://127.0.0.1:2601",
"host": "elks://127.0.0.1",
"password": "test-password",
"prefix": "",
"username": "test-username",
@@ -434,7 +583,7 @@ async def test_form_cannot_connect(hass):
)
assert result2["type"] == "form"
assert result2["errors"] == {CONF_HOST: "cannot_connect"}
assert result2["errors"] == {"base": "cannot_connect"}
async def test_unknown_exception(hass):

View File

@@ -11,7 +11,7 @@ from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component
from .const import MOCK_USER_DATA
from .const import MOCK_MESH_MASTER_MAC, MOCK_USER_DATA
from tests.common import MockConfigEntry
from tests.components.diagnostics import get_diagnostics_for_config_entry
@@ -69,6 +69,7 @@ async def test_entry_diagnostics(
"latest_firmware": None,
"mesh_role": "master",
"model": "FRITZ!Box 7530 AX",
"unique_id": MOCK_MESH_MASTER_MAC.replace("6F:12", "XX:XX"),
"update_available": False,
"wan_link_properties": {
"NewLayer1DownstreamMaxBitRate": 318557000,

View File

@@ -3,7 +3,7 @@ from datetime import timedelta
from unittest.mock import MagicMock, patch
import pytest
from rokuecp import RokuError
from rokuecp import RokuConnectionError, RokuConnectionTimeoutError, RokuError
from homeassistant.components.media_player import MediaPlayerDeviceClass
from homeassistant.components.media_player.const import (
@@ -164,10 +164,15 @@ async def test_tv_setup(
assert device_entry.suggested_area == "Living room"
@pytest.mark.parametrize(
"error",
[RokuConnectionTimeoutError, RokuConnectionError, RokuError],
)
async def test_availability(
hass: HomeAssistant,
mock_roku: MagicMock,
mock_config_entry: MockConfigEntry,
error: RokuError,
) -> None:
"""Test entity availability."""
now = dt_util.utcnow()
@@ -179,7 +184,7 @@ async def test_availability(
await hass.async_block_till_done()
with patch("homeassistant.util.dt.utcnow", return_value=future):
mock_roku.update.side_effect = RokuError
mock_roku.update.side_effect = error
async_fire_time_changed(hass, future)
await hass.async_block_till_done()
assert hass.states.get(MAIN_ENTITY_ID).state == STATE_UNAVAILABLE

View File

@@ -115,7 +115,9 @@ async def test_query_limit(hass: HomeAssistant) -> None:
assert state.attributes["value"] == 5
async def test_query_no_value(hass: HomeAssistant) -> None:
async def test_query_no_value(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
) -> None:
"""Test the SQL sensor with a query that returns no value."""
config = {
"sensor": {
@@ -137,6 +139,9 @@ async def test_query_no_value(hass: HomeAssistant) -> None:
state = hass.states.get("sensor.count_tables")
assert state.state == STATE_UNKNOWN
text = "SELECT 5 as value where 1=2 returned no results"
assert text in caplog.text
async def test_invalid_query(hass: HomeAssistant) -> None:
"""Test the SQL sensor for invalid queries."""