commit
c73eca5923
|
@ -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",
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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)
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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."""
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
),
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
[metadata]
|
||||
version = 2022.6.5
|
||||
version = 2022.6.6
|
||||
url = https://www.home-assistant.io/
|
||||
|
||||
[options]
|
||||
|
|
|
@ -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"],
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"}},
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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={}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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})
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
Loading…
Reference in New Issue