Merge pull request #73504 from home-assistant/rc

pull/73964/head 2022.6.6
Paulus Schoutsen 2022-06-14 14:49:29 -07:00 committed by GitHub
commit c73eca5923
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 525 additions and 187 deletions

View File

@ -3,7 +3,7 @@
"name": "Bond",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/bond",
"requirements": ["bond-async==0.1.20"],
"requirements": ["bond-async==0.1.22"],
"zeroconf": ["_bond._tcp.local."],
"codeowners": ["@bdraco", "@prystupa", "@joshs85", "@marciogranzotto"],
"quality_scale": "platinum",

View File

@ -5,7 +5,7 @@ import logging
from typing import Any, cast
from aiohttp import ClientResponseError
from bond_async import Action, Bond
from bond_async import Action, Bond, BondType
from homeassistant.util.async_ import gather_with_concurrency
@ -224,4 +224,5 @@ class BondHub:
@property
def is_bridge(self) -> bool:
"""Return if the Bond is a Bond Bridge."""
return bool(self._bridge)
bondid = self._version["bondid"]
return bool(BondType.is_bridge_from_serial(bondid))

View File

@ -3,7 +3,7 @@
"name": "Hive",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/hive",
"requirements": ["pyhiveapi==0.5.9"],
"requirements": ["pyhiveapi==0.5.10"],
"codeowners": ["@Rendili", "@KJonline"],
"iot_class": "cloud_polling",
"loggers": ["apyhiveapi"]

View File

@ -3,7 +3,7 @@
"name": "Philips Hue",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/hue",
"requirements": ["aiohue==4.4.1"],
"requirements": ["aiohue==4.4.2"],
"ssdp": [
{
"manufacturer": "Royal Philips Electronics",

View File

@ -189,9 +189,10 @@ def async_subscribe_events(
def _forward_state_events_filtered(event: Event) -> None:
if event.data.get("old_state") is None or event.data.get("new_state") is None:
return
state: State = event.data["new_state"]
if _is_state_filtered(ent_reg, state) or (
entities_filter and not entities_filter(state.entity_id)
new_state: State = event.data["new_state"]
old_state: State = event.data["old_state"]
if _is_state_filtered(ent_reg, new_state, old_state) or (
entities_filter and not entities_filter(new_state.entity_id)
):
return
target(event)
@ -229,17 +230,20 @@ def is_sensor_continuous(ent_reg: er.EntityRegistry, entity_id: str) -> bool:
)
def _is_state_filtered(ent_reg: er.EntityRegistry, state: State) -> bool:
def _is_state_filtered(
ent_reg: er.EntityRegistry, new_state: State, old_state: State
) -> bool:
"""Check if the logbook should filter a state.
Used when we are in live mode to ensure
we only get significant changes (state.last_changed != state.last_updated)
"""
return bool(
split_entity_id(state.entity_id)[0] in ALWAYS_CONTINUOUS_DOMAINS
or state.last_changed != state.last_updated
or ATTR_UNIT_OF_MEASUREMENT in state.attributes
or is_sensor_continuous(ent_reg, state.entity_id)
new_state.state == old_state.state
or split_entity_id(new_state.entity_id)[0] in ALWAYS_CONTINUOUS_DOMAINS
or new_state.last_changed != new_state.last_updated
or ATTR_UNIT_OF_MEASUREMENT in new_state.attributes
or is_sensor_continuous(ent_reg, new_state.entity_id)
)

View File

@ -70,6 +70,7 @@ FAN_MODE_MAP = {
"OFF": FAN_OFF,
}
FAN_INV_MODE_MAP = {v: k for k, v in FAN_MODE_MAP.items()}
FAN_INV_MODES = list(FAN_INV_MODE_MAP)
MAX_FAN_DURATION = 43200 # 15 hours is the max in the SDM API
MIN_TEMP = 10
@ -99,7 +100,7 @@ class ThermostatEntity(ClimateEntity):
"""Initialize ThermostatEntity."""
self._device = device
self._device_info = NestDeviceInfo(device)
self._supported_features = 0
self._attr_supported_features = 0
@property
def should_poll(self) -> bool:
@ -124,7 +125,7 @@ class ThermostatEntity(ClimateEntity):
async def async_added_to_hass(self) -> None:
"""Run when entity is added to register update signal handler."""
self._supported_features = self._get_supported_features()
self._attr_supported_features = self._get_supported_features()
self.async_on_remove(
self._device.add_update_listener(self.async_write_ha_state)
)
@ -198,8 +199,6 @@ class ThermostatEntity(ClimateEntity):
trait = self._device.traits[ThermostatModeTrait.NAME]
if trait.mode in THERMOSTAT_MODE_MAP:
hvac_mode = THERMOSTAT_MODE_MAP[trait.mode]
if hvac_mode == HVACMode.OFF and self.fan_mode == FAN_ON:
hvac_mode = HVACMode.FAN_ONLY
return hvac_mode
@property
@ -209,8 +208,6 @@ class ThermostatEntity(ClimateEntity):
for mode in self._get_device_hvac_modes:
if mode in THERMOSTAT_MODE_MAP:
supported_modes.append(THERMOSTAT_MODE_MAP[mode])
if self.supported_features & ClimateEntityFeature.FAN_MODE:
supported_modes.append(HVACMode.FAN_ONLY)
return supported_modes
@property
@ -252,7 +249,10 @@ class ThermostatEntity(ClimateEntity):
@property
def fan_mode(self) -> str:
"""Return the current fan mode."""
if FanTrait.NAME in self._device.traits:
if (
self.supported_features & ClimateEntityFeature.FAN_MODE
and FanTrait.NAME in self._device.traits
):
trait = self._device.traits[FanTrait.NAME]
return FAN_MODE_MAP.get(trait.timer_mode, FAN_OFF)
return FAN_OFF
@ -260,15 +260,12 @@ class ThermostatEntity(ClimateEntity):
@property
def fan_modes(self) -> list[str]:
"""Return the list of available fan modes."""
modes = []
if FanTrait.NAME in self._device.traits:
modes = list(FAN_INV_MODE_MAP)
return modes
@property
def supported_features(self) -> int:
"""Bitmap of supported features."""
return self._supported_features
if (
self.supported_features & ClimateEntityFeature.FAN_MODE
and FanTrait.NAME in self._device.traits
):
return FAN_INV_MODES
return []
def _get_supported_features(self) -> int:
"""Compute the bitmap of supported features from the current state."""
@ -290,10 +287,6 @@ class ThermostatEntity(ClimateEntity):
"""Set new target hvac mode."""
if hvac_mode not in self.hvac_modes:
raise ValueError(f"Unsupported hvac_mode '{hvac_mode}'")
if hvac_mode == HVACMode.FAN_ONLY:
# Turn the fan on but also turn off the hvac if it is on
await self.async_set_fan_mode(FAN_ON)
hvac_mode = HVACMode.OFF
api_mode = THERMOSTAT_INV_MODE_MAP[hvac_mode]
trait = self._device.traits[ThermostatModeTrait.NAME]
try:
@ -338,6 +331,10 @@ class ThermostatEntity(ClimateEntity):
"""Set new target fan mode."""
if fan_mode not in self.fan_modes:
raise ValueError(f"Unsupported fan_mode '{fan_mode}'")
if fan_mode == FAN_ON and self.hvac_mode == HVACMode.OFF:
raise ValueError(
"Cannot turn on fan, please set an HVAC mode (e.g. heat/cool) first"
)
trait = self._device.traits[FanTrait.NAME]
duration = None
if fan_mode != FAN_OFF:

View File

@ -134,7 +134,7 @@ class OverkizNumber(OverkizDescriptiveEntity, NumberEntity):
"""Return the entity value to represent the entity state."""
if state := self.device.states.get(self.entity_description.key):
if self.entity_description.inverted:
return self._attr_max_value - cast(float, state.value)
return self.max_value - cast(float, state.value)
return cast(float, state.value)
@ -143,7 +143,7 @@ class OverkizNumber(OverkizDescriptiveEntity, NumberEntity):
async def async_set_value(self, value: float) -> None:
"""Set new value."""
if self.entity_description.inverted:
value = self._attr_max_value - value
value = self.max_value - value
await self.executor.async_execute_command(
self.entity_description.command, value

View File

@ -7,6 +7,7 @@ from typing import Any
from aiohttp import CookieJar
from pyunifiprotect import NotAuthorized, NvrError, ProtectApiClient
from pyunifiprotect.data import NVR
from unifi_discovery import async_console_is_alive
import voluptuous as vol
from homeassistant import config_entries
@ -21,7 +22,10 @@ from homeassistant.const import (
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers.aiohttp_client import async_create_clientsession
from homeassistant.helpers.aiohttp_client import (
async_create_clientsession,
async_get_clientsession,
)
from homeassistant.helpers.typing import DiscoveryInfoType
from homeassistant.loader import async_get_integration
from homeassistant.util.network import is_ip_address
@ -36,11 +40,17 @@ from .const import (
MIN_REQUIRED_PROTECT_V,
OUTDATED_LOG_MESSAGE,
)
from .data import async_last_update_was_successful
from .discovery import async_start_discovery
from .utils import _async_resolve, _async_short_mac, _async_unifi_mac_from_hass
_LOGGER = logging.getLogger(__name__)
ENTRY_FAILURE_STATES = (
config_entries.ConfigEntryState.SETUP_ERROR,
config_entries.ConfigEntryState.SETUP_RETRY,
)
async def async_local_user_documentation_url(hass: HomeAssistant) -> str:
"""Get the documentation url for creating a local user."""
@ -53,6 +63,25 @@ def _host_is_direct_connect(host: str) -> bool:
return host.endswith(".ui.direct")
async def _async_console_is_offline(
hass: HomeAssistant,
entry: config_entries.ConfigEntry,
) -> bool:
"""Check if a console is offline.
We define offline by the config entry
is in a failure/retry state or the updates
are failing and the console is unreachable
since protect may be updating.
"""
return bool(
entry.state in ENTRY_FAILURE_STATES
or not async_last_update_was_successful(hass, entry)
) and not await async_console_is_alive(
async_get_clientsession(hass, verify_ssl=False), entry.data[CONF_HOST]
)
class ProtectFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a UniFi Protect config flow."""
@ -110,6 +139,7 @@ class ProtectFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
not entry_has_direct_connect
and is_ip_address(entry_host)
and entry_host != source_ip
and await _async_console_is_offline(self.hass, entry)
):
new_host = source_ip
if new_host:

View File

@ -20,11 +20,21 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
from homeassistant.helpers.event import async_track_time_interval
from .const import CONF_DISABLE_RTSP, DEVICES_THAT_ADOPT, DEVICES_WITH_ENTITIES
from .const import CONF_DISABLE_RTSP, DEVICES_THAT_ADOPT, DEVICES_WITH_ENTITIES, DOMAIN
_LOGGER = logging.getLogger(__name__)
@callback
def async_last_update_was_successful(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Check if the last update was successful for a config entry."""
return bool(
DOMAIN in hass.data
and entry.entry_id in hass.data[DOMAIN]
and hass.data[DOMAIN][entry.entry_id].last_update_success
)
class ProtectData:
"""Coordinate updates."""

View File

@ -3,7 +3,7 @@
"name": "UniFi Protect",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/unifiprotect",
"requirements": ["pyunifiprotect==3.9.2", "unifi-discovery==1.1.3"],
"requirements": ["pyunifiprotect==3.9.2", "unifi-discovery==1.1.4"],
"dependencies": ["http"],
"codeowners": ["@briis", "@AngellusMortis", "@bdraco"],
"quality_scale": "platinum",

View File

@ -72,11 +72,12 @@ CONFIG_SCHEMA = vol.Schema(
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the Withings component."""
conf = config.get(DOMAIN, {})
if not (conf := config.get(DOMAIN, {})):
if not (conf := config.get(DOMAIN)):
# Apply the defaults.
conf = CONFIG_SCHEMA({DOMAIN: {}})[DOMAIN]
hass.data[DOMAIN] = {const.CONFIG: conf}
return True
# Make the config available to the oauth2 config flow.
hass.data[DOMAIN] = {const.CONFIG: conf}
# Setup the oauth2 config flow.

View File

@ -96,7 +96,10 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
self.hass.config_entries.async_update_entry(
entry, data={**entry.data, CONF_HOST: self._discovered_ip}
)
reload = True
reload = entry.state in (
ConfigEntryState.SETUP_RETRY,
ConfigEntryState.LOADED,
)
if reload:
self.hass.async_create_task(
self.hass.config_entries.async_reload(entry.entry_id)

View File

@ -14,6 +14,7 @@ from zwave_js_server.const import (
InclusionStrategy,
LogLevel,
Protocols,
ProvisioningEntryStatus,
QRCodeVersion,
SecurityClass,
ZwaveFeature,
@ -148,6 +149,8 @@ MAX_INCLUSION_REQUEST_INTERVAL = "max_inclusion_request_interval"
UUID = "uuid"
SUPPORTED_PROTOCOLS = "supported_protocols"
ADDITIONAL_PROPERTIES = "additional_properties"
STATUS = "status"
REQUESTED_SECURITY_CLASSES = "requested_security_classes"
FEATURE = "feature"
UNPROVISION = "unprovision"
@ -160,19 +163,22 @@ def convert_planned_provisioning_entry(info: dict) -> ProvisioningEntry:
"""Handle provisioning entry dict to ProvisioningEntry."""
return ProvisioningEntry(
dsk=info[DSK],
security_classes=[SecurityClass(sec_cls) for sec_cls in info[SECURITY_CLASSES]],
security_classes=info[SECURITY_CLASSES],
status=info[STATUS],
requested_security_classes=info.get(REQUESTED_SECURITY_CLASSES),
additional_properties={
k: v for k, v in info.items() if k not in (DSK, SECURITY_CLASSES)
k: v
for k, v in info.items()
if k not in (DSK, SECURITY_CLASSES, STATUS, REQUESTED_SECURITY_CLASSES)
},
)
def convert_qr_provisioning_information(info: dict) -> QRProvisioningInformation:
"""Convert QR provisioning information dict to QRProvisioningInformation."""
protocols = [Protocols(proto) for proto in info.get(SUPPORTED_PROTOCOLS, [])]
return QRProvisioningInformation(
version=QRCodeVersion(info[VERSION]),
security_classes=[SecurityClass(sec_cls) for sec_cls in info[SECURITY_CLASSES]],
version=info[VERSION],
security_classes=info[SECURITY_CLASSES],
dsk=info[DSK],
generic_device_class=info[GENERIC_DEVICE_CLASS],
specific_device_class=info[SPECIFIC_DEVICE_CLASS],
@ -183,7 +189,9 @@ def convert_qr_provisioning_information(info: dict) -> QRProvisioningInformation
application_version=info[APPLICATION_VERSION],
max_inclusion_request_interval=info.get(MAX_INCLUSION_REQUEST_INTERVAL),
uuid=info.get(UUID),
supported_protocols=protocols if protocols else None,
supported_protocols=info.get(SUPPORTED_PROTOCOLS),
status=info[STATUS],
requested_security_classes=info.get(REQUESTED_SECURITY_CLASSES),
additional_properties=info.get(ADDITIONAL_PROPERTIES, {}),
)
@ -197,6 +205,12 @@ PLANNED_PROVISIONING_ENTRY_SCHEMA = vol.All(
cv.ensure_list,
[vol.Coerce(SecurityClass)],
),
vol.Optional(STATUS, default=ProvisioningEntryStatus.ACTIVE): vol.Coerce(
ProvisioningEntryStatus
),
vol.Optional(REQUESTED_SECURITY_CLASSES): vol.All(
cv.ensure_list, [vol.Coerce(SecurityClass)]
),
},
# Provisioning entries can have extra keys for SmartStart
extra=vol.ALLOW_EXTRA,
@ -226,6 +240,12 @@ QR_PROVISIONING_INFORMATION_SCHEMA = vol.All(
cv.ensure_list,
[vol.Coerce(Protocols)],
),
vol.Optional(STATUS, default=ProvisioningEntryStatus.ACTIVE): vol.Coerce(
ProvisioningEntryStatus
),
vol.Optional(REQUESTED_SECURITY_CLASSES): vol.All(
cv.ensure_list, [vol.Coerce(SecurityClass)]
),
vol.Optional(ADDITIONAL_PROPERTIES): dict,
}
),

View File

@ -7,7 +7,7 @@ from .backports.enum import StrEnum
MAJOR_VERSION: Final = 2022
MINOR_VERSION: Final = 6
PATCH_VERSION: Final = "5"
PATCH_VERSION: Final = "6"
__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

@ -169,7 +169,7 @@ aiohomekit==0.7.17
aiohttp_cors==0.7.0
# homeassistant.components.hue
aiohue==4.4.1
aiohue==4.4.2
# homeassistant.components.imap
aioimaplib==0.9.0
@ -417,7 +417,7 @@ blockchain==1.4.4
# bluepy==1.3.0
# homeassistant.components.bond
bond-async==0.1.20
bond-async==0.1.22
# homeassistant.components.bosch_shc
boschshcpy==0.2.30
@ -1538,7 +1538,7 @@ pyheos==0.7.2
pyhik==0.3.0
# homeassistant.components.hive
pyhiveapi==0.5.9
pyhiveapi==0.5.10
# homeassistant.components.homematic
pyhomematic==0.1.77
@ -2358,7 +2358,7 @@ twitchAPI==2.5.2
uasiren==0.0.1
# homeassistant.components.unifiprotect
unifi-discovery==1.1.3
unifi-discovery==1.1.4
# homeassistant.components.unifiled
unifiled==0.11

View File

@ -153,7 +153,7 @@ aiohomekit==0.7.17
aiohttp_cors==0.7.0
# homeassistant.components.hue
aiohue==4.4.1
aiohue==4.4.2
# homeassistant.components.apache_kafka
aiokafka==0.6.0
@ -318,7 +318,7 @@ blebox_uniapi==1.3.3
blinkpy==0.19.0
# homeassistant.components.bond
bond-async==0.1.20
bond-async==0.1.22
# homeassistant.components.bosch_shc
boschshcpy==0.2.30
@ -1029,7 +1029,7 @@ pyhaversion==22.4.1
pyheos==0.7.2
# homeassistant.components.hive
pyhiveapi==0.5.9
pyhiveapi==0.5.10
# homeassistant.components.homematic
pyhomematic==0.1.77
@ -1546,7 +1546,7 @@ twitchAPI==2.5.2
uasiren==0.0.1
# homeassistant.components.unifiprotect
unifi-discovery==1.1.3
unifi-discovery==1.1.4
# homeassistant.components.upb
upb_lib==0.4.12

View File

@ -1,5 +1,5 @@
[metadata]
version = 2022.6.5
version = 2022.6.6
url = https://www.home-assistant.io/
[options]

View File

@ -113,7 +113,7 @@ def patch_bond_version(
return nullcontext()
if return_value is None:
return_value = {"bondid": "test-bond-id"}
return_value = {"bondid": "ZXXX12345"}
return patch(
"homeassistant.components.bond.Bond.version",
@ -246,3 +246,12 @@ async def help_test_entity_available(
async_fire_time_changed(hass, utcnow() + timedelta(seconds=30))
await hass.async_block_till_done()
assert hass.states.get(entity_id).state != STATE_UNAVAILABLE
def ceiling_fan(name: str):
"""Create a ceiling fan with given name."""
return {
"name": name,
"type": DeviceType.CEILING_FAN,
"actions": ["SetSpeed", "SetDirection"],
}

View File

@ -35,7 +35,7 @@ async def test_user_form(hass: core.HomeAssistant):
assert result["errors"] == {}
with patch_bond_version(
return_value={"bondid": "test-bond-id"}
return_value={"bondid": "ZXXX12345"}
), patch_bond_device_ids(
return_value=["f6776c11", "f6776c12"]
), patch_bond_bridge(), patch_bond_device_properties(), patch_bond_device(), _patch_async_setup_entry() as mock_setup_entry:
@ -64,7 +64,7 @@ async def test_user_form_with_non_bridge(hass: core.HomeAssistant):
assert result["errors"] == {}
with patch_bond_version(
return_value={"bondid": "test-bond-id"}
return_value={"bondid": "KXXX12345"}
), patch_bond_device_ids(
return_value=["f6776c11"]
), patch_bond_device_properties(), patch_bond_device(
@ -96,7 +96,7 @@ async def test_user_form_invalid_auth(hass: core.HomeAssistant):
)
with patch_bond_version(
return_value={"bond_id": "test-bond-id"}
return_value={"bond_id": "ZXXX12345"}
), patch_bond_bridge(), patch_bond_device_ids(
side_effect=ClientResponseError(Mock(), Mock(), status=401),
):
@ -203,7 +203,7 @@ async def test_zeroconf_form(hass: core.HomeAssistant):
host="test-host",
addresses=["test-host"],
hostname="mock_hostname",
name="test-bond-id.some-other-tail-info",
name="ZXXX12345.some-other-tail-info",
port=None,
properties={},
type="mock_type",
@ -213,7 +213,7 @@ async def test_zeroconf_form(hass: core.HomeAssistant):
assert result["errors"] == {}
with patch_bond_version(
return_value={"bondid": "test-bond-id"}
return_value={"bondid": "ZXXX12345"}
), patch_bond_bridge(), patch_bond_device_ids(), _patch_async_setup_entry() as mock_setup_entry:
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
@ -241,7 +241,7 @@ async def test_zeroconf_form_token_unavailable(hass: core.HomeAssistant):
host="test-host",
addresses=["test-host"],
hostname="mock_hostname",
name="test-bond-id.some-other-tail-info",
name="ZXXX12345.some-other-tail-info",
port=None,
properties={},
type="mock_type",
@ -270,7 +270,7 @@ async def test_zeroconf_form_token_unavailable(hass: core.HomeAssistant):
async def test_zeroconf_form_with_token_available(hass: core.HomeAssistant):
"""Test we get the discovery form when we can get the token."""
with patch_bond_version(return_value={"bondid": "test-bond-id"}), patch_bond_token(
with patch_bond_version(return_value={"bondid": "ZXXX12345"}), patch_bond_token(
return_value={"token": "discovered-token"}
), patch_bond_bridge(
return_value={"name": "discovered-name"}
@ -282,7 +282,7 @@ async def test_zeroconf_form_with_token_available(hass: core.HomeAssistant):
host="test-host",
addresses=["test-host"],
hostname="mock_hostname",
name="test-bond-id.some-other-tail-info",
name="ZXXX12345.some-other-tail-info",
port=None,
properties={},
type="mock_type",
@ -323,7 +323,7 @@ async def test_zeroconf_form_with_token_available_name_unavailable(
host="test-host",
addresses=["test-host"],
hostname="mock_hostname",
name="test-bond-id.some-other-tail-info",
name="ZXXX12345.some-other-tail-info",
port=None,
properties={},
type="mock_type",
@ -341,7 +341,7 @@ async def test_zeroconf_form_with_token_available_name_unavailable(
await hass.async_block_till_done()
assert result2["type"] == "create_entry"
assert result2["title"] == "test-bond-id"
assert result2["title"] == "ZXXX12345"
assert result2["data"] == {
CONF_HOST: "test-host",
CONF_ACCESS_TOKEN: "discovered-token",
@ -472,7 +472,7 @@ async def test_zeroconf_form_unexpected_error(hass: core.HomeAssistant):
host="test-host",
addresses=["test-host"],
hostname="mock_hostname",
name="test-bond-id.some-other-tail-info",
name="ZXXX12345.some-other-tail-info",
port=None,
properties={},
type="mock_type",
@ -497,7 +497,7 @@ async def _help_test_form_unexpected_error(
)
with patch_bond_version(
return_value={"bond_id": "test-bond-id"}
return_value={"bond_id": "ZXXX12345"}
), patch_bond_device_ids(side_effect=error):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input

View File

@ -39,5 +39,5 @@ async def test_diagnostics(hass, hass_client):
"data": {"access_token": "**REDACTED**", "host": "some host"},
"title": "Mock Title",
},
"hub": {"version": {"bondid": "test-bond-id"}},
"hub": {"version": {"bondid": "ZXXX12345"}},
}

View File

@ -7,13 +7,15 @@ from bond_async import DeviceType
import pytest
from homeassistant.components.bond.const import DOMAIN
from homeassistant.components.fan import DOMAIN as FAN_DOMAIN
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST
from homeassistant.const import ATTR_ASSUMED_STATE, CONF_ACCESS_TOKEN, CONF_HOST
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
from homeassistant.setup import async_setup_component
from .common import (
ceiling_fan,
patch_bond_bridge,
patch_bond_device,
patch_bond_device_ids,
@ -23,6 +25,7 @@ from .common import (
patch_setup_entry,
patch_start_bpup,
setup_bond_entity,
setup_platform,
)
from tests.common import MockConfigEntry
@ -81,7 +84,7 @@ async def test_async_setup_entry_sets_up_hub_and_supported_domains(hass: HomeAss
with patch_bond_bridge(), patch_bond_version(
return_value={
"bondid": "test-bond-id",
"bondid": "ZXXX12345",
"target": "test-model",
"fw_ver": "test-version",
"mcu_ver": "test-hw-version",
@ -99,11 +102,11 @@ async def test_async_setup_entry_sets_up_hub_and_supported_domains(hass: HomeAss
assert config_entry.entry_id in hass.data[DOMAIN]
assert config_entry.state is ConfigEntryState.LOADED
assert config_entry.unique_id == "test-bond-id"
assert config_entry.unique_id == "ZXXX12345"
# verify hub device is registered correctly
device_registry = dr.async_get(hass)
hub = device_registry.async_get_device(identifiers={(DOMAIN, "test-bond-id")})
hub = device_registry.async_get_device(identifiers={(DOMAIN, "ZXXX12345")})
assert hub.name == "bond-name"
assert hub.manufacturer == "Olibra"
assert hub.model == "test-model"
@ -151,7 +154,7 @@ async def test_old_identifiers_are_removed(hass: HomeAssistant):
)
old_identifers = (DOMAIN, "device_id")
new_identifiers = (DOMAIN, "test-bond-id", "device_id")
new_identifiers = (DOMAIN, "ZXXX12345", "device_id")
device_registry = dr.async_get(hass)
device_registry.async_get_or_create(
config_entry_id=config_entry.entry_id,
@ -164,7 +167,7 @@ async def test_old_identifiers_are_removed(hass: HomeAssistant):
with patch_bond_bridge(), patch_bond_version(
return_value={
"bondid": "test-bond-id",
"bondid": "ZXXX12345",
"target": "test-model",
"fw_ver": "test-version",
}
@ -185,7 +188,7 @@ async def test_old_identifiers_are_removed(hass: HomeAssistant):
assert config_entry.entry_id in hass.data[DOMAIN]
assert config_entry.state is ConfigEntryState.LOADED
assert config_entry.unique_id == "test-bond-id"
assert config_entry.unique_id == "ZXXX12345"
# verify the device info is cleaned up
assert device_registry.async_get_device(identifiers={old_identifers}) is None
@ -205,7 +208,7 @@ async def test_smart_by_bond_device_suggested_area(hass: HomeAssistant):
side_effect=ClientResponseError(Mock(), Mock(), status=404)
), patch_bond_version(
return_value={
"bondid": "test-bond-id",
"bondid": "KXXX12345",
"target": "test-model",
"fw_ver": "test-version",
}
@ -227,10 +230,10 @@ async def test_smart_by_bond_device_suggested_area(hass: HomeAssistant):
assert config_entry.entry_id in hass.data[DOMAIN]
assert config_entry.state is ConfigEntryState.LOADED
assert config_entry.unique_id == "test-bond-id"
assert config_entry.unique_id == "KXXX12345"
device_registry = dr.async_get(hass)
device = device_registry.async_get_device(identifiers={(DOMAIN, "test-bond-id")})
device = device_registry.async_get_device(identifiers={(DOMAIN, "KXXX12345")})
assert device is not None
assert device.suggested_area == "Den"
@ -251,7 +254,7 @@ async def test_bridge_device_suggested_area(hass: HomeAssistant):
}
), patch_bond_version(
return_value={
"bondid": "test-bond-id",
"bondid": "ZXXX12345",
"target": "test-model",
"fw_ver": "test-version",
}
@ -273,9 +276,21 @@ async def test_bridge_device_suggested_area(hass: HomeAssistant):
assert config_entry.entry_id in hass.data[DOMAIN]
assert config_entry.state is ConfigEntryState.LOADED
assert config_entry.unique_id == "test-bond-id"
assert config_entry.unique_id == "ZXXX12345"
device_registry = dr.async_get(hass)
device = device_registry.async_get_device(identifiers={(DOMAIN, "test-bond-id")})
device = device_registry.async_get_device(identifiers={(DOMAIN, "ZXXX12345")})
assert device is not None
assert device.suggested_area == "Office"
async def test_smart_by_bond_v3_firmware(hass: HomeAssistant) -> None:
"""Test we can detect smart by bond with the v3 firmware."""
await setup_platform(
hass,
FAN_DOMAIN,
ceiling_fan("name-1"),
bond_version={"bondid": "KXXXX12345", "target": "breck-northstar"},
bond_device_id="test-device-id",
)
assert ATTR_ASSUMED_STATE not in hass.states.get("fan.name_1").attributes

View File

@ -249,7 +249,7 @@ async def test_sbb_trust_state(hass: core.HomeAssistant):
"""Assumed state should be False if device is a Smart by Bond."""
version = {
"model": "MR123A",
"bondid": "test-bond-id",
"bondid": "KXXX12345",
}
await setup_platform(
hass, LIGHT_DOMAIN, ceiling_fan("name-1"), bond_version=version, bridge={}

View File

@ -2404,3 +2404,117 @@ async def test_subscribe_entities_some_have_uom_multiple(
# Check our listener got unsubscribed
assert sum(hass.bus.async_listeners().values()) == init_count
@patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0)
async def test_logbook_stream_ignores_forced_updates(
hass, recorder_mock, hass_ws_client
):
"""Test logbook live stream ignores forced updates."""
now = dt_util.utcnow()
await asyncio.gather(
*[
async_setup_component(hass, comp, {})
for comp in ("homeassistant", "logbook", "automation", "script")
]
)
await hass.async_block_till_done()
init_count = sum(hass.bus.async_listeners().values())
hass.states.async_set("binary_sensor.is_light", STATE_ON)
hass.states.async_set("binary_sensor.is_light", STATE_OFF)
state: State = hass.states.get("binary_sensor.is_light")
await hass.async_block_till_done()
await async_wait_recording_done(hass)
websocket_client = await hass_ws_client()
await websocket_client.send_json(
{"id": 7, "type": "logbook/event_stream", "start_time": now.isoformat()}
)
msg = await asyncio.wait_for(websocket_client.receive_json(), 2)
assert msg["id"] == 7
assert msg["type"] == TYPE_RESULT
assert msg["success"]
msg = await asyncio.wait_for(websocket_client.receive_json(), 2)
assert msg["id"] == 7
assert msg["type"] == "event"
assert msg["event"]["events"] == [
{
"entity_id": "binary_sensor.is_light",
"state": "off",
"when": state.last_updated.timestamp(),
}
]
assert msg["event"]["start_time"] == now.timestamp()
assert msg["event"]["end_time"] > msg["event"]["start_time"]
assert msg["event"]["partial"] is True
await hass.async_block_till_done()
msg = await asyncio.wait_for(websocket_client.receive_json(), 2)
assert msg["id"] == 7
assert msg["type"] == "event"
assert "partial" not in msg["event"]["events"]
assert msg["event"]["events"] == []
hass.states.async_set("binary_sensor.is_light", STATE_ON)
hass.states.async_set("binary_sensor.is_light", STATE_OFF)
msg = await asyncio.wait_for(websocket_client.receive_json(), 2)
assert msg["id"] == 7
assert msg["type"] == "event"
assert "partial" not in msg["event"]["events"]
assert msg["event"]["events"] == [
{
"entity_id": "binary_sensor.is_light",
"state": STATE_ON,
"when": ANY,
},
{
"entity_id": "binary_sensor.is_light",
"state": STATE_OFF,
"when": ANY,
},
]
# Now we force an update to make sure we ignore
# forced updates when the state has not actually changed
hass.states.async_set("binary_sensor.is_light", STATE_ON)
for _ in range(3):
hass.states.async_set("binary_sensor.is_light", STATE_OFF, force_update=True)
msg = await asyncio.wait_for(websocket_client.receive_json(), 2)
assert msg["id"] == 7
assert msg["type"] == "event"
assert "partial" not in msg["event"]["events"]
assert msg["event"]["events"] == [
{
"entity_id": "binary_sensor.is_light",
"state": STATE_ON,
"when": ANY,
},
# We should only get the first one and ignore
# the other forced updates since the state
# has not actually changed
{
"entity_id": "binary_sensor.is_light",
"state": STATE_OFF,
"when": ANY,
},
]
await websocket_client.send_json(
{"id": 8, "type": "unsubscribe_events", "subscription": 7}
)
msg = await asyncio.wait_for(websocket_client.receive_json(), 2)
assert msg["id"] == 8
assert msg["type"] == TYPE_RESULT
assert msg["success"]
# Check our listener got unsubscribed
assert sum(hass.bus.async_listeners().values()) == init_count

View File

@ -33,15 +33,15 @@ from homeassistant.components.climate.const import (
FAN_ON,
HVAC_MODE_COOL,
HVAC_MODE_DRY,
HVAC_MODE_FAN_ONLY,
HVAC_MODE_HEAT,
HVAC_MODE_HEAT_COOL,
HVAC_MODE_OFF,
PRESET_ECO,
PRESET_NONE,
PRESET_SLEEP,
ClimateEntityFeature,
)
from homeassistant.const import ATTR_TEMPERATURE
from homeassistant.const import ATTR_SUPPORTED_FEATURES, ATTR_TEMPERATURE
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
@ -794,7 +794,7 @@ async def test_thermostat_fan_off(
"sdm.devices.traits.ThermostatHvac": {"status": "OFF"},
"sdm.devices.traits.ThermostatMode": {
"availableModes": ["HEAT", "COOL", "HEATCOOL", "OFF"],
"mode": "OFF",
"mode": "COOL",
},
"sdm.devices.traits.Temperature": {
"ambientTemperatureCelsius": 16.2,
@ -806,18 +806,22 @@ async def test_thermostat_fan_off(
assert len(hass.states.async_all()) == 1
thermostat = hass.states.get("climate.my_thermostat")
assert thermostat is not None
assert thermostat.state == HVAC_MODE_OFF
assert thermostat.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_OFF
assert thermostat.state == HVAC_MODE_COOL
assert thermostat.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_IDLE
assert thermostat.attributes[ATTR_CURRENT_TEMPERATURE] == 16.2
assert set(thermostat.attributes[ATTR_HVAC_MODES]) == {
HVAC_MODE_HEAT,
HVAC_MODE_COOL,
HVAC_MODE_HEAT_COOL,
HVAC_MODE_FAN_ONLY,
HVAC_MODE_OFF,
}
assert thermostat.attributes[ATTR_FAN_MODE] == FAN_OFF
assert thermostat.attributes[ATTR_FAN_MODES] == [FAN_ON, FAN_OFF]
assert thermostat.attributes[ATTR_SUPPORTED_FEATURES] == (
ClimateEntityFeature.TARGET_TEMPERATURE
| ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
| ClimateEntityFeature.FAN_MODE
)
async def test_thermostat_fan_on(
@ -837,7 +841,7 @@ async def test_thermostat_fan_on(
},
"sdm.devices.traits.ThermostatMode": {
"availableModes": ["HEAT", "COOL", "HEATCOOL", "OFF"],
"mode": "OFF",
"mode": "COOL",
},
"sdm.devices.traits.Temperature": {
"ambientTemperatureCelsius": 16.2,
@ -849,18 +853,22 @@ async def test_thermostat_fan_on(
assert len(hass.states.async_all()) == 1
thermostat = hass.states.get("climate.my_thermostat")
assert thermostat is not None
assert thermostat.state == HVAC_MODE_FAN_ONLY
assert thermostat.state == HVAC_MODE_COOL
assert thermostat.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_IDLE
assert thermostat.attributes[ATTR_CURRENT_TEMPERATURE] == 16.2
assert set(thermostat.attributes[ATTR_HVAC_MODES]) == {
HVAC_MODE_HEAT,
HVAC_MODE_COOL,
HVAC_MODE_HEAT_COOL,
HVAC_MODE_FAN_ONLY,
HVAC_MODE_OFF,
}
assert thermostat.attributes[ATTR_FAN_MODE] == FAN_ON
assert thermostat.attributes[ATTR_FAN_MODES] == [FAN_ON, FAN_OFF]
assert thermostat.attributes[ATTR_SUPPORTED_FEATURES] == (
ClimateEntityFeature.TARGET_TEMPERATURE
| ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
| ClimateEntityFeature.FAN_MODE
)
async def test_thermostat_cool_with_fan(
@ -895,11 +903,15 @@ async def test_thermostat_cool_with_fan(
HVAC_MODE_HEAT,
HVAC_MODE_COOL,
HVAC_MODE_HEAT_COOL,
HVAC_MODE_FAN_ONLY,
HVAC_MODE_OFF,
}
assert thermostat.attributes[ATTR_FAN_MODE] == FAN_ON
assert thermostat.attributes[ATTR_FAN_MODES] == [FAN_ON, FAN_OFF]
assert thermostat.attributes[ATTR_SUPPORTED_FEATURES] == (
ClimateEntityFeature.TARGET_TEMPERATURE
| ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
| ClimateEntityFeature.FAN_MODE
)
async def test_thermostat_set_fan(
@ -907,6 +919,68 @@ async def test_thermostat_set_fan(
setup_platform: PlatformSetup,
auth: FakeAuth,
create_device: CreateDevice,
) -> None:
"""Test a thermostat enabling the fan."""
create_device.create(
{
"sdm.devices.traits.Fan": {
"timerMode": "ON",
"timerTimeout": "2019-05-10T03:22:54Z",
},
"sdm.devices.traits.ThermostatHvac": {
"status": "OFF",
},
"sdm.devices.traits.ThermostatMode": {
"availableModes": ["HEAT", "COOL", "HEATCOOL", "OFF"],
"mode": "HEAT",
},
}
)
await setup_platform()
assert len(hass.states.async_all()) == 1
thermostat = hass.states.get("climate.my_thermostat")
assert thermostat is not None
assert thermostat.state == HVAC_MODE_HEAT
assert thermostat.attributes[ATTR_FAN_MODE] == FAN_ON
assert thermostat.attributes[ATTR_FAN_MODES] == [FAN_ON, FAN_OFF]
assert thermostat.attributes[ATTR_SUPPORTED_FEATURES] == (
ClimateEntityFeature.TARGET_TEMPERATURE
| ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
| ClimateEntityFeature.FAN_MODE
)
# Turn off fan mode
await common.async_set_fan_mode(hass, FAN_OFF)
await hass.async_block_till_done()
assert auth.method == "post"
assert auth.url == DEVICE_COMMAND
assert auth.json == {
"command": "sdm.devices.commands.Fan.SetTimer",
"params": {"timerMode": "OFF"},
}
# Turn on fan mode
await common.async_set_fan_mode(hass, FAN_ON)
await hass.async_block_till_done()
assert auth.method == "post"
assert auth.url == DEVICE_COMMAND
assert auth.json == {
"command": "sdm.devices.commands.Fan.SetTimer",
"params": {
"duration": "43200s",
"timerMode": "ON",
},
}
async def test_thermostat_set_fan_when_off(
hass: HomeAssistant,
setup_platform: PlatformSetup,
auth: FakeAuth,
create_device: CreateDevice,
) -> None:
"""Test a thermostat enabling the fan."""
create_device.create(
@ -929,34 +1003,18 @@ async def test_thermostat_set_fan(
assert len(hass.states.async_all()) == 1
thermostat = hass.states.get("climate.my_thermostat")
assert thermostat is not None
assert thermostat.state == HVAC_MODE_FAN_ONLY
assert thermostat.state == HVAC_MODE_OFF
assert thermostat.attributes[ATTR_FAN_MODE] == FAN_ON
assert thermostat.attributes[ATTR_FAN_MODES] == [FAN_ON, FAN_OFF]
assert thermostat.attributes[ATTR_SUPPORTED_FEATURES] == (
ClimateEntityFeature.TARGET_TEMPERATURE
| ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
| ClimateEntityFeature.FAN_MODE
)
# Turn off fan mode
await common.async_set_fan_mode(hass, FAN_OFF)
await hass.async_block_till_done()
assert auth.method == "post"
assert auth.url == DEVICE_COMMAND
assert auth.json == {
"command": "sdm.devices.commands.Fan.SetTimer",
"params": {"timerMode": "OFF"},
}
# Turn on fan mode
await common.async_set_fan_mode(hass, FAN_ON)
await hass.async_block_till_done()
assert auth.method == "post"
assert auth.url == DEVICE_COMMAND
assert auth.json == {
"command": "sdm.devices.commands.Fan.SetTimer",
"params": {
"duration": "43200s",
"timerMode": "ON",
},
}
# Fan cannot be turned on when HVAC is off
with pytest.raises(ValueError):
await common.async_set_fan_mode(hass, FAN_ON, entity_id="climate.my_thermostat")
async def test_thermostat_fan_empty(
@ -994,6 +1052,10 @@ async def test_thermostat_fan_empty(
}
assert ATTR_FAN_MODE not in thermostat.attributes
assert ATTR_FAN_MODES not in thermostat.attributes
assert thermostat.attributes[ATTR_SUPPORTED_FEATURES] == (
ClimateEntityFeature.TARGET_TEMPERATURE
| ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
)
# Ignores set_fan_mode since it is lacking SUPPORT_FAN_MODE
await common.async_set_fan_mode(hass, FAN_ON)
@ -1018,7 +1080,7 @@ async def test_thermostat_invalid_fan_mode(
"sdm.devices.traits.ThermostatHvac": {"status": "OFF"},
"sdm.devices.traits.ThermostatMode": {
"availableModes": ["HEAT", "COOL", "HEATCOOL", "OFF"],
"mode": "OFF",
"mode": "COOL",
},
"sdm.devices.traits.Temperature": {
"ambientTemperatureCelsius": 16.2,
@ -1030,14 +1092,13 @@ async def test_thermostat_invalid_fan_mode(
assert len(hass.states.async_all()) == 1
thermostat = hass.states.get("climate.my_thermostat")
assert thermostat is not None
assert thermostat.state == HVAC_MODE_FAN_ONLY
assert thermostat.state == HVAC_MODE_COOL
assert thermostat.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_IDLE
assert thermostat.attributes[ATTR_CURRENT_TEMPERATURE] == 16.2
assert set(thermostat.attributes[ATTR_HVAC_MODES]) == {
HVAC_MODE_HEAT,
HVAC_MODE_COOL,
HVAC_MODE_HEAT_COOL,
HVAC_MODE_FAN_ONLY,
HVAC_MODE_OFF,
}
assert thermostat.attributes[ATTR_FAN_MODE] == FAN_ON
@ -1048,58 +1109,6 @@ async def test_thermostat_invalid_fan_mode(
await hass.async_block_till_done()
async def test_thermostat_set_hvac_fan_only(
hass: HomeAssistant,
setup_platform: PlatformSetup,
auth: FakeAuth,
create_device: CreateDevice,
) -> None:
"""Test a thermostat enabling the fan via hvac_mode."""
create_device.create(
{
"sdm.devices.traits.Fan": {
"timerMode": "OFF",
"timerTimeout": "2019-05-10T03:22:54Z",
},
"sdm.devices.traits.ThermostatHvac": {
"status": "OFF",
},
"sdm.devices.traits.ThermostatMode": {
"availableModes": ["HEAT", "COOL", "HEATCOOL", "OFF"],
"mode": "OFF",
},
}
)
await setup_platform()
assert len(hass.states.async_all()) == 1
thermostat = hass.states.get("climate.my_thermostat")
assert thermostat is not None
assert thermostat.state == HVAC_MODE_OFF
assert thermostat.attributes[ATTR_FAN_MODE] == FAN_OFF
assert thermostat.attributes[ATTR_FAN_MODES] == [FAN_ON, FAN_OFF]
await common.async_set_hvac_mode(hass, HVAC_MODE_FAN_ONLY)
await hass.async_block_till_done()
assert len(auth.captured_requests) == 2
(method, url, json, headers) = auth.captured_requests.pop(0)
assert method == "post"
assert url == DEVICE_COMMAND
assert json == {
"command": "sdm.devices.commands.Fan.SetTimer",
"params": {"duration": "43200s", "timerMode": "ON"},
}
(method, url, json, headers) = auth.captured_requests.pop(0)
assert method == "post"
assert url == DEVICE_COMMAND
assert json == {
"command": "sdm.devices.commands.ThermostatMode.SetMode",
"params": {"mode": "OFF"},
}
async def test_thermostat_target_temp(
hass: HomeAssistant,
setup_platform: PlatformSetup,
@ -1397,7 +1406,7 @@ async def test_thermostat_hvac_mode_failure(
"sdm.devices.traits.ThermostatHvac": {"status": "OFF"},
"sdm.devices.traits.ThermostatMode": {
"availableModes": ["HEAT", "COOL", "HEATCOOL", "OFF"],
"mode": "OFF",
"mode": "COOL",
},
"sdm.devices.traits.Fan": {
"timerMode": "OFF",
@ -1416,8 +1425,8 @@ async def test_thermostat_hvac_mode_failure(
assert len(hass.states.async_all()) == 1
thermostat = hass.states.get("climate.my_thermostat")
assert thermostat is not None
assert thermostat.state == HVAC_MODE_OFF
assert thermostat.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_OFF
assert thermostat.state == HVAC_MODE_COOL
assert thermostat.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_IDLE
auth.responses = [aiohttp.web.Response(status=HTTPStatus.BAD_REQUEST)]
with pytest.raises(HomeAssistantError):

View File

@ -402,7 +402,10 @@ async def test_discovered_by_unifi_discovery_direct_connect_updated_but_not_usin
)
mock_config.add_to_hass(hass)
with _patch_discovery():
with _patch_discovery(), patch(
"homeassistant.components.unifiprotect.config_flow.async_console_is_alive",
return_value=False,
):
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY},
@ -415,6 +418,41 @@ async def test_discovered_by_unifi_discovery_direct_connect_updated_but_not_usin
assert mock_config.data[CONF_HOST] == "127.0.0.1"
async def test_discovered_by_unifi_discovery_does_not_update_ip_when_console_is_still_online(
hass: HomeAssistant, mock_nvr: NVR
) -> None:
"""Test a discovery from unifi-discovery does not update the ip unless the console at the old ip is offline."""
mock_config = MockConfigEntry(
domain=DOMAIN,
data={
"host": "1.2.2.2",
"username": "test-username",
"password": "test-password",
"id": "UnifiProtect",
"port": 443,
"verify_ssl": False,
},
version=2,
unique_id=DEVICE_MAC_ADDRESS.replace(":", "").upper(),
)
mock_config.add_to_hass(hass)
with _patch_discovery(), patch(
"homeassistant.components.unifiprotect.config_flow.async_console_is_alive",
return_value=True,
):
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY},
data=UNIFI_DISCOVERY_DICT,
)
await hass.async_block_till_done()
assert result["type"] == RESULT_TYPE_ABORT
assert result["reason"] == "already_configured"
assert mock_config.data[CONF_HOST] == "1.2.2.2"
async def test_discovered_host_not_updated_if_existing_is_a_hostname(
hass: HomeAssistant, mock_nvr: NVR
) -> None:

View File

@ -739,7 +739,7 @@ async def test_discovered_zeroconf(hass):
async def test_discovery_updates_ip(hass: HomeAssistant):
"""Test discovery updtes ip."""
"""Test discovery updates ip."""
config_entry = MockConfigEntry(
domain=DOMAIN, data={CONF_HOST: "1.2.2.3"}, unique_id=ID
)
@ -761,6 +761,35 @@ async def test_discovery_updates_ip(hass: HomeAssistant):
assert config_entry.data[CONF_HOST] == IP_ADDRESS
async def test_discovery_updates_ip_no_reload_setup_in_progress(hass: HomeAssistant):
"""Test discovery updates ip does not reload if setup is an an error state."""
config_entry = MockConfigEntry(
domain=DOMAIN,
data={CONF_HOST: "1.2.2.3"},
unique_id=ID,
state=config_entries.ConfigEntryState.SETUP_ERROR,
)
config_entry.add_to_hass(hass)
mocked_bulb = _mocked_bulb()
with patch(
f"{MODULE}.async_setup_entry", return_value=True
) as mock_setup_entry, _patch_discovery(), _patch_discovery_interval(), patch(
f"{MODULE_CONFIG_FLOW}.AsyncBulb", return_value=mocked_bulb
):
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_ZEROCONF},
data=ZEROCONF_DATA,
)
await hass.async_block_till_done()
assert result["type"] == RESULT_TYPE_ABORT
assert result["reason"] == "already_configured"
assert config_entry.data[CONF_HOST] == IP_ADDRESS
assert len(mock_setup_entry.mock_calls) == 0
async def test_discovery_adds_missing_ip_id_only(hass: HomeAssistant):
"""Test discovery adds missing ip."""
config_entry = MockConfigEntry(domain=DOMAIN, data={CONF_ID: ID})

View File

@ -10,6 +10,7 @@ from zwave_js_server.const import (
InclusionStrategy,
LogLevel,
Protocols,
ProvisioningEntryStatus,
QRCodeVersion,
SecurityClass,
ZwaveFeature,
@ -63,8 +64,10 @@ from homeassistant.components.zwave_js.api import (
PROPERTY_KEY,
QR_CODE_STRING,
QR_PROVISIONING_INFORMATION,
REQUESTED_SECURITY_CLASSES,
SECURITY_CLASSES,
SPECIFIC_DEVICE_CLASS,
STATUS,
TYPE,
UNPROVISION,
VALUE,
@ -619,13 +622,68 @@ async def test_add_node(
client.async_send_command.reset_mock()
client.async_send_command.return_value = {"success": True}
# Test S2 QR code string
# Test S2 QR provisioning information
await ws_client.send_json(
{
ID: 4,
TYPE: "zwave_js/add_node",
ENTRY_ID: entry.entry_id,
INCLUSION_STRATEGY: InclusionStrategy.SECURITY_S2.value,
QR_PROVISIONING_INFORMATION: {
VERSION: 0,
SECURITY_CLASSES: [0],
DSK: "test",
GENERIC_DEVICE_CLASS: 1,
SPECIFIC_DEVICE_CLASS: 1,
INSTALLER_ICON_TYPE: 1,
MANUFACTURER_ID: 1,
PRODUCT_TYPE: 1,
PRODUCT_ID: 1,
APPLICATION_VERSION: "test",
STATUS: 1,
REQUESTED_SECURITY_CLASSES: [0],
},
}
)
msg = await ws_client.receive_json()
assert msg["success"]
assert len(client.async_send_command.call_args_list) == 1
assert client.async_send_command.call_args[0][0] == {
"command": "controller.begin_inclusion",
"options": {
"strategy": InclusionStrategy.SECURITY_S2,
"provisioning": QRProvisioningInformation(
version=QRCodeVersion.S2,
security_classes=[SecurityClass.S2_UNAUTHENTICATED],
dsk="test",
generic_device_class=1,
specific_device_class=1,
installer_icon_type=1,
manufacturer_id=1,
product_type=1,
product_id=1,
application_version="test",
max_inclusion_request_interval=None,
uuid=None,
supported_protocols=None,
status=ProvisioningEntryStatus.INACTIVE,
requested_security_classes=[SecurityClass.S2_UNAUTHENTICATED],
).to_dict(),
},
}
client.async_send_command.reset_mock()
client.async_send_command.return_value = {"success": True}
# Test S2 QR code string
await ws_client.send_json(
{
ID: 5,
TYPE: "zwave_js/add_node",
ENTRY_ID: entry.entry_id,
INCLUSION_STRATEGY: InclusionStrategy.SECURITY_S2.value,
QR_CODE_STRING: "90testtesttesttesttesttesttesttesttesttesttesttesttest",
}
)
@ -648,7 +706,7 @@ async def test_add_node(
# Test Smart Start QR provisioning information with S2 inclusion strategy fails
await ws_client.send_json(
{
ID: 5,
ID: 6,
TYPE: "zwave_js/add_node",
ENTRY_ID: entry.entry_id,
INCLUSION_STRATEGY: InclusionStrategy.SECURITY_S2.value,
@ -678,7 +736,7 @@ async def test_add_node(
# Test QR provisioning information with S0 inclusion strategy fails
await ws_client.send_json(
{
ID: 5,
ID: 7,
TYPE: "zwave_js/add_node",
ENTRY_ID: entry.entry_id,
INCLUSION_STRATEGY: InclusionStrategy.SECURITY_S0,
@ -708,7 +766,7 @@ async def test_add_node(
# Test ValueError is caught as failure
await ws_client.send_json(
{
ID: 6,
ID: 8,
TYPE: "zwave_js/add_node",
ENTRY_ID: entry.entry_id,
INCLUSION_STRATEGY: InclusionStrategy.DEFAULT.value,
@ -728,7 +786,7 @@ async def test_add_node(
):
await ws_client.send_json(
{
ID: 7,
ID: 9,
TYPE: "zwave_js/add_node",
ENTRY_ID: entry.entry_id,
}
@ -744,7 +802,7 @@ async def test_add_node(
await hass.async_block_till_done()
await ws_client.send_json(
{ID: 8, TYPE: "zwave_js/add_node", ENTRY_ID: entry.entry_id}
{ID: 10, TYPE: "zwave_js/add_node", ENTRY_ID: entry.entry_id}
)
msg = await ws_client.receive_json()