Merge pull request #68592 from home-assistant/rc

This commit is contained in:
Paulus Schoutsen
2022-03-23 21:04:33 -07:00
committed by GitHub
23 changed files with 127 additions and 76 deletions

View File

@@ -19,6 +19,7 @@ from homeassistant.const import CONF_ADDRESS, CONF_NAME, CONF_PIN
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.util.network import is_ipv6_address
from .const import CONF_CREDENTIALS, CONF_IDENTIFIERS, CONF_START_OFF, DOMAIN from .const import CONF_CREDENTIALS, CONF_IDENTIFIERS, CONF_START_OFF, DOMAIN
@@ -166,6 +167,8 @@ class AppleTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
) -> data_entry_flow.FlowResult: ) -> data_entry_flow.FlowResult:
"""Handle device found via zeroconf.""" """Handle device found via zeroconf."""
host = discovery_info.host host = discovery_info.host
if is_ipv6_address(host):
return self.async_abort(reason="ipv6_not_supported")
self._async_abort_entries_match({CONF_ADDRESS: host}) self._async_abort_entries_match({CONF_ADDRESS: host})
service_type = discovery_info.type[:-1] # Remove leading . service_type = discovery_info.type[:-1] # Remove leading .
name = discovery_info.name.replace(f".{service_type}.", "") name = discovery_info.name.replace(f".{service_type}.", "")

View File

@@ -48,6 +48,7 @@
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]"
}, },
"abort": { "abort": {
"ipv6_not_supported": "IPv6 is not supported.",
"no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]", "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]",
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"device_did_not_pair": "No attempt to finish pairing process was made from the device.", "device_did_not_pair": "No attempt to finish pairing process was made from the device.",

View File

@@ -2,13 +2,12 @@
"config": { "config": {
"abort": { "abort": {
"already_configured": "Device is already configured", "already_configured": "Device is already configured",
"already_configured_device": "Device is already configured",
"already_in_progress": "Configuration flow is already in progress", "already_in_progress": "Configuration flow is already in progress",
"backoff": "Device does not accept pairing requests at this time (you might have entered an invalid PIN code too many times), try again later.", "backoff": "Device does not accept pairing requests at this time (you might have entered an invalid PIN code too many times), try again later.",
"device_did_not_pair": "No attempt to finish pairing process was made from the device.", "device_did_not_pair": "No attempt to finish pairing process was made from the device.",
"device_not_found": "Device was not found during discovery, please try adding it again.", "device_not_found": "Device was not found during discovery, please try adding it again.",
"inconsistent_device": "Expected protocols were not found during discovery. This normally indicates a problem with multicast DNS (Zeroconf). Please try adding the device again.", "inconsistent_device": "Expected protocols were not found during discovery. This normally indicates a problem with multicast DNS (Zeroconf). Please try adding the device again.",
"invalid_config": "The configuration for this device is incomplete. Please try adding it again.", "ipv6_not_supported": "IPv6 is not supported.",
"no_devices_found": "No devices found on the network", "no_devices_found": "No devices found on the network",
"reauth_successful": "Re-authentication was successful", "reauth_successful": "Re-authentication was successful",
"setup_failed": "Failed to set up device.", "setup_failed": "Failed to set up device.",
@@ -18,7 +17,6 @@
"already_configured": "Device is already configured", "already_configured": "Device is already configured",
"invalid_auth": "Invalid authentication", "invalid_auth": "Invalid authentication",
"no_devices_found": "No devices found on the network", "no_devices_found": "No devices found on the network",
"no_usable_service": "A device was found but could not identify any way to establish a connection to it. If you keep seeing this message, try specifying its IP address or restarting your Apple TV.",
"unknown": "Unexpected error" "unknown": "Unexpected error"
}, },
"flow_title": "{name} ({type})", "flow_title": "{name} ({type})",
@@ -72,6 +70,5 @@
"description": "Configure general device settings" "description": "Configure general device settings"
} }
} }
}, }
"title": "Apple TV"
} }

View File

@@ -2,7 +2,7 @@
"domain": "emulated_kasa", "domain": "emulated_kasa",
"name": "Emulated Kasa", "name": "Emulated Kasa",
"documentation": "https://www.home-assistant.io/integrations/emulated_kasa", "documentation": "https://www.home-assistant.io/integrations/emulated_kasa",
"requirements": ["sense_energy==0.10.2"], "requirements": ["sense_energy==0.10.3"],
"codeowners": ["@kbickar"], "codeowners": ["@kbickar"],
"quality_scale": "internal", "quality_scale": "internal",
"iot_class": "local_push", "iot_class": "local_push",

View File

@@ -6,6 +6,7 @@ from collections.abc import Callable
import logging import logging
from typing import Any from typing import Any
import aiohttp
from aiohttp import client_exceptions from aiohttp import client_exceptions
from aiohue import HueBridgeV1, HueBridgeV2, LinkButtonNotPressed, Unauthorized from aiohue import HueBridgeV1, HueBridgeV2, LinkButtonNotPressed, Unauthorized
from aiohue.errors import AiohueException, BridgeBusy from aiohue.errors import AiohueException, BridgeBusy
@@ -14,7 +15,7 @@ import async_timeout
from homeassistant import core from homeassistant import core
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import CONF_API_KEY, CONF_HOST, Platform from homeassistant.const import CONF_API_KEY, CONF_HOST, Platform
from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError
from homeassistant.helpers import aiohttp_client from homeassistant.helpers import aiohttp_client
from .const import CONF_API_VERSION, DOMAIN from .const import CONF_API_VERSION, DOMAIN
@@ -116,22 +117,23 @@ class HueBridge:
self.authorized = True self.authorized = True
return True return True
async def async_request_call( async def async_request_call(self, task: Callable, *args, **kwargs) -> Any:
self, task: Callable, *args, allowed_errors: list[str] | None = None, **kwargs """Send request to the Hue bridge."""
) -> Any:
"""Send request to the Hue bridge, optionally omitting error(s)."""
try: try:
return await task(*args, **kwargs) return await task(*args, **kwargs)
except AiohueException as err: except AiohueException as err:
# The (new) Hue api can be a bit fanatic with throwing errors # The (new) Hue api can be a bit fanatic with throwing errors so
# some of which we accept in certain conditions # we have some logic to treat some responses as warning only.
# handle that here. Note that these errors are strings and do not have msg = f"Request failed: {err}"
# an identifier or something. if "may not have effect" in str(err):
if allowed_errors is not None and str(err) in allowed_errors:
# log only # log only
self.logger.debug("Ignored error/warning from Hue API: %s", str(err)) self.logger.debug(msg)
return None return None
raise err raise HomeAssistantError(msg) from err
except aiohttp.ClientError as err:
raise HomeAssistantError(
f"Request failed due connection error: {err}"
) from err
async def async_reset(self) -> bool: async def async_reset(self) -> bool:
"""Reset this bridge to default state. """Reset this bridge to default state.

View File

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

View File

@@ -37,21 +37,6 @@ from .helpers import (
normalize_hue_transition, normalize_hue_transition,
) )
ALLOWED_ERRORS = [
"device (groupedLight) has communication issues, command (on) may not have effect",
'device (groupedLight) is "soft off", command (on) may not have effect',
"device (light) has communication issues, command (on) may not have effect",
'device (light) is "soft off", command (on) may not have effect',
"device (grouped_light) has communication issues, command (.on) may not have effect",
'device (grouped_light) is "soft off", command (.on) may not have effect'
"device (grouped_light) has communication issues, command (.on.on) may not have effect",
'device (grouped_light) is "soft off", command (.on.on) may not have effect'
"device (light) has communication issues, command (.on) may not have effect",
'device (light) is "soft off", command (.on) may not have effect',
"device (light) has communication issues, command (.on.on) may not have effect",
'device (light) is "soft off", command (.on.on) may not have effect',
]
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
@@ -183,10 +168,7 @@ class GroupedHueLight(HueBaseEntity, LightEntity):
and flash is None and flash is None
): ):
await self.bridge.async_request_call( await self.bridge.async_request_call(
self.controller.set_state, self.controller.set_state, id=self.resource.id, on=True
id=self.resource.id,
on=True,
allowed_errors=ALLOWED_ERRORS,
) )
return return
@@ -202,7 +184,6 @@ class GroupedHueLight(HueBaseEntity, LightEntity):
color_xy=xy_color if light.supports_color else None, color_xy=xy_color if light.supports_color else None,
color_temp=color_temp if light.supports_color_temperature else None, color_temp=color_temp if light.supports_color_temperature else None,
transition_time=transition, transition_time=transition,
allowed_errors=ALLOWED_ERRORS,
) )
for light in self.controller.get_lights(self.resource.id) for light in self.controller.get_lights(self.resource.id)
] ]
@@ -222,10 +203,7 @@ class GroupedHueLight(HueBaseEntity, LightEntity):
# To set other features, you'll have to control the attached lights # To set other features, you'll have to control the attached lights
if transition is None: if transition is None:
await self.bridge.async_request_call( await self.bridge.async_request_call(
self.controller.set_state, self.controller.set_state, id=self.resource.id, on=False
id=self.resource.id,
on=False,
allowed_errors=ALLOWED_ERRORS,
) )
return return
@@ -237,7 +215,6 @@ class GroupedHueLight(HueBaseEntity, LightEntity):
light.id, light.id,
on=False, on=False,
transition_time=transition, transition_time=transition,
allowed_errors=ALLOWED_ERRORS,
) )
for light in self.controller.get_lights(self.resource.id) for light in self.controller.get_lights(self.resource.id)
] ]

View File

@@ -36,15 +36,6 @@ from .helpers import (
normalize_hue_transition, normalize_hue_transition,
) )
ALLOWED_ERRORS = [
"device (light) has communication issues, command (on) may not have effect",
'device (light) is "soft off", command (on) may not have effect',
"device (light) has communication issues, command (.on) may not have effect",
'device (light) is "soft off", command (.on) may not have effect',
"device (light) has communication issues, command (.on.on) may not have effect",
'device (light) is "soft off", command (.on.on) may not have effect',
]
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
@@ -182,7 +173,6 @@ class HueLight(HueBaseEntity, LightEntity):
color_xy=xy_color, color_xy=xy_color,
color_temp=color_temp, color_temp=color_temp,
transition_time=transition, transition_time=transition,
allowed_errors=ALLOWED_ERRORS,
) )
async def async_turn_off(self, **kwargs: Any) -> None: async def async_turn_off(self, **kwargs: Any) -> None:
@@ -202,7 +192,6 @@ class HueLight(HueBaseEntity, LightEntity):
id=self.resource.id, id=self.resource.id,
on=False, on=False,
transition_time=transition, transition_time=transition,
allowed_errors=ALLOWED_ERRORS,
) )
async def async_set_flash(self, flash: str) -> None: async def async_set_flash(self, flash: str) -> None:

View File

@@ -12,7 +12,7 @@ from homeassistant import config_entries
from homeassistant.const import CONF_CODE, CONF_EMAIL, CONF_PASSWORD, CONF_TIMEOUT from homeassistant.const import CONF_CODE, CONF_EMAIL, CONF_PASSWORD, CONF_TIMEOUT
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import ACTIVE_UPDATE_RATE, DEFAULT_TIMEOUT, DOMAIN, SENSE_TIMEOUT_EXCEPTIONS from .const import ACTIVE_UPDATE_RATE, DEFAULT_TIMEOUT, DOMAIN, SENSE_CONNECT_EXCEPTIONS
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@@ -76,7 +76,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
await self.validate_input(user_input) await self.validate_input(user_input)
except SenseMFARequiredException: except SenseMFARequiredException:
return await self.async_step_validation() return await self.async_step_validation()
except SENSE_TIMEOUT_EXCEPTIONS: except SENSE_CONNECT_EXCEPTIONS:
errors["base"] = "cannot_connect" errors["base"] = "cannot_connect"
except SenseAuthenticationException: except SenseAuthenticationException:
errors["base"] = "invalid_auth" errors["base"] = "invalid_auth"
@@ -93,7 +93,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
if user_input: if user_input:
try: try:
await self._gateway.validate_mfa(user_input[CONF_CODE]) await self._gateway.validate_mfa(user_input[CONF_CODE])
except SENSE_TIMEOUT_EXCEPTIONS: except SENSE_CONNECT_EXCEPTIONS:
errors["base"] = "cannot_connect" errors["base"] = "cannot_connect"
except SenseAuthenticationException: except SenseAuthenticationException:
errors["base"] = "invalid_auth" errors["base"] = "invalid_auth"

View File

@@ -3,8 +3,11 @@
import asyncio import asyncio
import socket import socket
from sense_energy import SenseAPITimeoutException from sense_energy import (
from sense_energy.sense_exceptions import SenseWebsocketException SenseAPIException,
SenseAPITimeoutException,
SenseWebsocketException,
)
DOMAIN = "sense" DOMAIN = "sense"
DEFAULT_TIMEOUT = 10 DEFAULT_TIMEOUT = 10
@@ -40,6 +43,11 @@ ICON = "mdi:flash"
SENSE_TIMEOUT_EXCEPTIONS = (asyncio.TimeoutError, SenseAPITimeoutException) SENSE_TIMEOUT_EXCEPTIONS = (asyncio.TimeoutError, SenseAPITimeoutException)
SENSE_EXCEPTIONS = (socket.gaierror, SenseWebsocketException) SENSE_EXCEPTIONS = (socket.gaierror, SenseWebsocketException)
SENSE_CONNECT_EXCEPTIONS = (
asyncio.TimeoutError,
SenseAPITimeoutException,
SenseAPIException,
)
MDI_ICONS = { MDI_ICONS = {
"ac": "air-conditioner", "ac": "air-conditioner",

View File

@@ -2,7 +2,7 @@
"domain": "sense", "domain": "sense",
"name": "Sense", "name": "Sense",
"documentation": "https://www.home-assistant.io/integrations/sense", "documentation": "https://www.home-assistant.io/integrations/sense",
"requirements": ["sense_energy==0.10.2"], "requirements": ["sense_energy==0.10.3"],
"codeowners": ["@kbickar"], "codeowners": ["@kbickar"],
"config_flow": true, "config_flow": true,
"dhcp": [ "dhcp": [

View File

@@ -63,6 +63,7 @@ from .media import SonosMedia
from .statistics import ActivityStatistics, EventStatistics from .statistics import ActivityStatistics, EventStatistics
NEVER_TIME = -1200.0 NEVER_TIME = -1200.0
RESUB_COOLDOWN_SECONDS = 10.0
EVENT_CHARGING = { EVENT_CHARGING = {
"CHARGING": True, "CHARGING": True,
"NOT_CHARGING": False, "NOT_CHARGING": False,
@@ -126,6 +127,7 @@ class SonosSpeaker:
self._last_event_cache: dict[str, Any] = {} self._last_event_cache: dict[str, Any] = {}
self.activity_stats: ActivityStatistics = ActivityStatistics(self.zone_name) self.activity_stats: ActivityStatistics = ActivityStatistics(self.zone_name)
self.event_stats: EventStatistics = EventStatistics(self.zone_name) self.event_stats: EventStatistics = EventStatistics(self.zone_name)
self._resub_cooldown_expires_at: float | None = None
# Scheduled callback handles # Scheduled callback handles
self._poll_timer: Callable | None = None self._poll_timer: Callable | None = None
@@ -502,6 +504,16 @@ class SonosSpeaker:
@callback @callback
def speaker_activity(self, source): def speaker_activity(self, source):
"""Track the last activity on this speaker, set availability and resubscribe.""" """Track the last activity on this speaker, set availability and resubscribe."""
if self._resub_cooldown_expires_at:
if time.monotonic() < self._resub_cooldown_expires_at:
_LOGGER.debug(
"Activity on %s from %s while in cooldown, ignoring",
self.zone_name,
source,
)
return
self._resub_cooldown_expires_at = None
_LOGGER.debug("Activity on %s from %s", self.zone_name, source) _LOGGER.debug("Activity on %s from %s", self.zone_name, source)
self._last_activity = time.monotonic() self._last_activity = time.monotonic()
self.activity_stats.activity(source, self._last_activity) self.activity_stats.activity(source, self._last_activity)
@@ -542,6 +554,10 @@ class SonosSpeaker:
if not self.available: if not self.available:
return return
if self._resub_cooldown_expires_at is None and not self.hass.is_stopping:
self._resub_cooldown_expires_at = time.monotonic() + RESUB_COOLDOWN_SECONDS
_LOGGER.debug("Starting resubscription cooldown for %s", self.zone_name)
self.available = False self.available = False
self.async_write_entity_states() self.async_write_entity_states()

View File

@@ -2,7 +2,7 @@
"domain": "synology_dsm", "domain": "synology_dsm",
"name": "Synology DSM", "name": "Synology DSM",
"documentation": "https://www.home-assistant.io/integrations/synology_dsm", "documentation": "https://www.home-assistant.io/integrations/synology_dsm",
"requirements": ["py-synologydsm-api==1.0.6"], "requirements": ["py-synologydsm-api==1.0.7"],
"codeowners": ["@hacf-fr", "@Quentame", "@mib1185"], "codeowners": ["@hacf-fr", "@Quentame", "@mib1185"],
"config_flow": true, "config_flow": true,
"ssdp": [ "ssdp": [

View File

@@ -7,7 +7,7 @@ from .backports.enum import StrEnum
MAJOR_VERSION: Final = 2022 MAJOR_VERSION: Final = 2022
MINOR_VERSION: Final = 3 MINOR_VERSION: Final = 3
PATCH_VERSION: Final = "6" PATCH_VERSION: Final = "7"
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__: Final = f"{__short_version__}.{PATCH_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0)

View File

@@ -218,9 +218,12 @@ def async_prepare_call_from_config(
if CONF_ENTITY_ID in target: if CONF_ENTITY_ID in target:
registry = entity_registry.async_get(hass) registry = entity_registry.async_get(hass)
target[CONF_ENTITY_ID] = entity_registry.async_resolve_entity_ids( entity_ids = cv.comp_entity_ids_or_uuids(target[CONF_ENTITY_ID])
registry, cv.comp_entity_ids_or_uuids(target[CONF_ENTITY_ID]) if entity_ids not in (ENTITY_MATCH_ALL, ENTITY_MATCH_NONE):
) entity_ids = entity_registry.async_resolve_entity_ids(
registry, entity_ids
)
target[CONF_ENTITY_ID] = entity_ids
except TemplateError as ex: except TemplateError as ex:
raise HomeAssistantError( raise HomeAssistantError(
f"Error rendering service target template: {ex}" f"Error rendering service target template: {ex}"

View File

@@ -191,7 +191,7 @@ aiohomekit==0.7.16
aiohttp_cors==0.7.0 aiohttp_cors==0.7.0
# homeassistant.components.hue # homeassistant.components.hue
aiohue==4.3.0 aiohue==4.4.1
# homeassistant.components.homewizard # homeassistant.components.homewizard
aiohwenergy==0.8.0 aiohwenergy==0.8.0
@@ -1338,7 +1338,7 @@ py-nightscout==1.2.2
py-schluter==0.1.7 py-schluter==0.1.7
# homeassistant.components.synology_dsm # homeassistant.components.synology_dsm
py-synologydsm-api==1.0.6 py-synologydsm-api==1.0.7
# homeassistant.components.zabbix # homeassistant.components.zabbix
py-zabbix==1.1.7 py-zabbix==1.1.7
@@ -2179,7 +2179,7 @@ sense-hat==2.2.0
# homeassistant.components.emulated_kasa # homeassistant.components.emulated_kasa
# homeassistant.components.sense # homeassistant.components.sense
sense_energy==0.10.2 sense_energy==0.10.3
# homeassistant.components.sentry # homeassistant.components.sentry
sentry-sdk==1.5.5 sentry-sdk==1.5.5

View File

@@ -141,7 +141,7 @@ aiohomekit==0.7.16
aiohttp_cors==0.7.0 aiohttp_cors==0.7.0
# homeassistant.components.hue # homeassistant.components.hue
aiohue==4.3.0 aiohue==4.4.1
# homeassistant.components.homewizard # homeassistant.components.homewizard
aiohwenergy==0.8.0 aiohwenergy==0.8.0
@@ -842,7 +842,7 @@ py-melissa-climate==2.1.4
py-nightscout==1.2.2 py-nightscout==1.2.2
# homeassistant.components.synology_dsm # homeassistant.components.synology_dsm
py-synologydsm-api==1.0.6 py-synologydsm-api==1.0.7
# homeassistant.components.seventeentrack # homeassistant.components.seventeentrack
py17track==2021.12.2 py17track==2021.12.2
@@ -1344,7 +1344,7 @@ screenlogicpy==0.5.4
# homeassistant.components.emulated_kasa # homeassistant.components.emulated_kasa
# homeassistant.components.sense # homeassistant.components.sense
sense_energy==0.10.2 sense_energy==0.10.3
# homeassistant.components.sentry # homeassistant.components.sentry
sentry-sdk==1.5.5 sentry-sdk==1.5.5

View File

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

View File

@@ -1066,3 +1066,22 @@ async def test_option_start_off(hass):
assert result2["type"] == "create_entry" assert result2["type"] == "create_entry"
assert config_entry.options[CONF_START_OFF] assert config_entry.options[CONF_START_OFF]
async def test_zeroconf_rejects_ipv6(hass):
"""Test zeroconf discovery rejects ipv6."""
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_ZEROCONF},
data=zeroconf.ZeroconfServiceInfo(
host="fd00::b27c:63bb:cc85:4ea0",
addresses=["fd00::b27c:63bb:cc85:4ea0"],
hostname="mock_hostname",
port=None,
type="_touch-able._tcp.local.",
name="dmapid._touch-able._tcp.local.",
properties={"CtlN": "Apple TV"},
),
)
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
assert result["reason"] == "ipv6_not_supported"

View File

@@ -60,7 +60,7 @@ def create_mock_bridge(hass, api_version=1):
bridge.async_initialize_bridge = async_initialize_bridge bridge.async_initialize_bridge = async_initialize_bridge
async def async_request_call(task, *args, allowed_errors=None, **kwargs): async def async_request_call(task, *args, **kwargs):
await task(*args, **kwargs) await task(*args, **kwargs)
bridge.async_request_call = async_request_call bridge.async_request_call = async_request_call

View File

@@ -460,7 +460,7 @@
"model_id": "BSB002", "model_id": "BSB002",
"product_archetype": "bridge_v2", "product_archetype": "bridge_v2",
"product_name": "Philips hue", "product_name": "Philips hue",
"software_version": "1.48.1948086000" "software_version": "1.50.1950111030"
}, },
"services": [ "services": [
{ {

View File

@@ -3,6 +3,7 @@ from unittest.mock import AsyncMock, patch
import pytest import pytest
from sense_energy import ( from sense_energy import (
SenseAPIException,
SenseAPITimeoutException, SenseAPITimeoutException,
SenseAuthenticationException, SenseAuthenticationException,
SenseMFARequiredException, SenseMFARequiredException,
@@ -189,7 +190,7 @@ async def test_form_mfa_required_exception(hass, mock_sense):
assert result3["errors"] == {"base": "unknown"} assert result3["errors"] == {"base": "unknown"}
async def test_form_cannot_connect(hass): async def test_form_timeout(hass):
"""Test we handle cannot connect error.""" """Test we handle cannot connect error."""
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER} DOMAIN, context={"source": config_entries.SOURCE_USER}
@@ -208,6 +209,25 @@ async def test_form_cannot_connect(hass):
assert result2["errors"] == {"base": "cannot_connect"} assert result2["errors"] == {"base": "cannot_connect"}
async def test_form_cannot_connect(hass):
"""Test we handle cannot connect error."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
with patch(
"sense_energy.ASyncSenseable.authenticate",
side_effect=SenseAPIException,
):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{"timeout": "6", "email": "test-email", "password": "test-password"},
)
assert result2["type"] == "form"
assert result2["errors"] == {"base": "cannot_connect"}
async def test_form_unknown_exception(hass): async def test_form_unknown_exception(hass):
"""Test we handle unknown error.""" """Test we handle unknown error."""
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(

View File

@@ -397,6 +397,22 @@ async def test_service_call_entry_id(hass):
assert dict(calls[0].data) == {"entity_id": ["hello.world"]} assert dict(calls[0].data) == {"entity_id": ["hello.world"]}
@pytest.mark.parametrize("target", ("all", "none"))
async def test_service_call_all_none(hass, target):
"""Test service call targeting all."""
calls = async_mock_service(hass, "test_domain", "test_service")
config = {
"service": "test_domain.test_service",
"target": {"entity_id": target},
}
await service.async_call_from_config(hass, config)
await hass.async_block_till_done()
assert dict(calls[0].data) == {"entity_id": target}
async def test_extract_entity_ids(hass): async def test_extract_entity_ids(hass):
"""Test extract_entity_ids method.""" """Test extract_entity_ids method."""
hass.states.async_set("light.Bowl", STATE_ON) hass.states.async_set("light.Bowl", STATE_ON)