mirror of
https://github.com/home-assistant/core.git
synced 2025-08-04 13:15:18 +02:00
Merge pull request #68592 from home-assistant/rc
This commit is contained in:
@@ -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}.", "")
|
||||||
|
@@ -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.",
|
||||||
|
@@ -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"
|
|
||||||
}
|
}
|
@@ -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",
|
||||||
|
@@ -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.
|
||||||
|
@@ -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",
|
||||||
|
@@ -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)
|
||||||
]
|
]
|
||||||
|
@@ -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:
|
||||||
|
@@ -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"
|
||||||
|
@@ -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",
|
||||||
|
@@ -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": [
|
||||||
|
@@ -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()
|
||||||
|
|
||||||
|
@@ -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": [
|
||||||
|
@@ -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)
|
||||||
|
@@ -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}"
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
|
@@ -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"
|
||||||
|
@@ -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
|
||||||
|
@@ -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": [
|
||||||
{
|
{
|
||||||
|
@@ -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(
|
||||||
|
@@ -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)
|
||||||
|
Reference in New Issue
Block a user