mirror of
https://github.com/home-assistant/core.git
synced 2025-07-21 12:17:07 +00:00
2024.5.4 (#117631)
This commit is contained in:
commit
3dc3de95fa
@ -82,6 +82,9 @@ class HomeassistantAnalyticsConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
except HomeassistantAnalyticsConnectionError:
|
||||
LOGGER.exception("Error connecting to Home Assistant analytics")
|
||||
return self.async_abort(reason="cannot_connect")
|
||||
except Exception: # pylint: disable=broad-except
|
||||
LOGGER.exception("Unexpected error")
|
||||
return self.async_abort(reason="unknown")
|
||||
|
||||
options = [
|
||||
SelectOptionDict(
|
||||
|
@ -13,7 +13,8 @@
|
||||
}
|
||||
},
|
||||
"abort": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"error": {
|
||||
"no_integration_selected": "You must select at least one integration to track"
|
||||
|
@ -15,6 +15,7 @@ class AuroraEntity(CoordinatorEntity[AuroraDataUpdateCoordinator]):
|
||||
"""Implementation of the base Aurora Entity."""
|
||||
|
||||
_attr_attribution = ATTRIBUTION
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
|
@ -2,13 +2,13 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, TypedDict, TypeVar
|
||||
from typing import Any, TypedDict, TypeVar, cast
|
||||
|
||||
from pydeconz.interfaces.groups import GroupHandler
|
||||
from pydeconz.interfaces.lights import LightHandler
|
||||
from pydeconz.models import ResourceType
|
||||
from pydeconz.models.event import EventType
|
||||
from pydeconz.models.group import Group
|
||||
from pydeconz.models.group import Group, TypedGroupAction
|
||||
from pydeconz.models.light.light import Light, LightAlert, LightColorMode, LightEffect
|
||||
|
||||
from homeassistant.components.light import (
|
||||
@ -105,6 +105,23 @@ class SetStateAttributes(TypedDict, total=False):
|
||||
xy: tuple[float, float]
|
||||
|
||||
|
||||
def update_color_state(
|
||||
group: Group, lights: list[Light], override: bool = False
|
||||
) -> None:
|
||||
"""Sync group color state with light."""
|
||||
data = {
|
||||
attribute: light_attribute
|
||||
for light in lights
|
||||
for attribute in ("bri", "ct", "hue", "sat", "xy", "colormode", "effect")
|
||||
if (light_attribute := light.raw["state"].get(attribute)) is not None
|
||||
}
|
||||
|
||||
if override:
|
||||
group.raw["action"] = cast(TypedGroupAction, data)
|
||||
else:
|
||||
group.update(cast(dict[str, dict[str, Any]], {"action": data}))
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
@ -148,11 +165,12 @@ async def async_setup_entry(
|
||||
if (group := hub.api.groups[group_id]) and not group.lights:
|
||||
return
|
||||
|
||||
first = True
|
||||
for light_id in group.lights:
|
||||
if (light := hub.api.lights.lights.get(light_id)) and light.reachable:
|
||||
group.update_color_state(light, update_all_attributes=first)
|
||||
first = False
|
||||
lights = [
|
||||
light
|
||||
for light_id in group.lights
|
||||
if (light := hub.api.lights.lights.get(light_id)) and light.reachable
|
||||
]
|
||||
update_color_state(group, lights, True)
|
||||
|
||||
async_add_entities([DeconzGroup(group, hub)])
|
||||
|
||||
@ -326,7 +344,7 @@ class DeconzLight(DeconzBaseLight[Light]):
|
||||
if self._device.reachable and "attr" not in self._device.changed_keys:
|
||||
for group in self.hub.api.groups.values():
|
||||
if self._device.resource_id in group.lights:
|
||||
group.update_color_state(self._device)
|
||||
update_color_state(group, [self._device])
|
||||
|
||||
|
||||
class DeconzGroup(DeconzBaseLight[Group]):
|
||||
|
@ -41,6 +41,11 @@ class DuotecnoEntity(Entity):
|
||||
"""When a unit has an update."""
|
||||
self.async_write_ha_state()
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Available state for the unit."""
|
||||
return self._unit.is_available()
|
||||
|
||||
|
||||
_T = TypeVar("_T", bound="DuotecnoEntity")
|
||||
_P = ParamSpec("_P")
|
||||
|
@ -7,5 +7,5 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["pyduotecno", "pyduotecno-node", "pyduotecno-unit"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["pyDuotecno==2024.3.2"]
|
||||
"requirements": ["pyDuotecno==2024.5.0"]
|
||||
}
|
||||
|
@ -69,18 +69,21 @@ async def async_setup_services(hass: HomeAssistant) -> None:
|
||||
async def async_set_config(call: ServiceCall) -> None:
|
||||
"""Set a Fully Kiosk Browser config value on the device."""
|
||||
for coordinator in await collect_coordinators(call.data[ATTR_DEVICE_ID]):
|
||||
key = call.data[ATTR_KEY]
|
||||
value = call.data[ATTR_VALUE]
|
||||
|
||||
# Fully API has different methods for setting string and bool values.
|
||||
# check if call.data[ATTR_VALUE] is a bool
|
||||
if isinstance(call.data[ATTR_VALUE], bool) or call.data[
|
||||
ATTR_VALUE
|
||||
].lower() in ("true", "false"):
|
||||
await coordinator.fully.setConfigurationBool(
|
||||
call.data[ATTR_KEY], call.data[ATTR_VALUE]
|
||||
)
|
||||
if isinstance(value, bool) or (
|
||||
isinstance(value, str) and value.lower() in ("true", "false")
|
||||
):
|
||||
await coordinator.fully.setConfigurationBool(key, value)
|
||||
else:
|
||||
await coordinator.fully.setConfigurationString(
|
||||
call.data[ATTR_KEY], call.data[ATTR_VALUE]
|
||||
)
|
||||
# Convert any int values to string
|
||||
if isinstance(value, int):
|
||||
value = str(value)
|
||||
|
||||
await coordinator.fully.setConfigurationString(key, value)
|
||||
|
||||
# Register all the above services
|
||||
service_mapping = [
|
||||
@ -111,7 +114,7 @@ async def async_setup_services(hass: HomeAssistant) -> None:
|
||||
{
|
||||
vol.Required(ATTR_DEVICE_ID): cv.ensure_list,
|
||||
vol.Required(ATTR_KEY): cv.string,
|
||||
vol.Required(ATTR_VALUE): vol.Any(str, bool),
|
||||
vol.Required(ATTR_VALUE): vol.Any(str, bool, int),
|
||||
}
|
||||
)
|
||||
),
|
||||
|
@ -169,7 +169,9 @@ class GoogleAssistantConversationAgent(conversation.AbstractConversationAgent):
|
||||
self.language = user_input.language
|
||||
self.assistant = TextAssistant(credentials, self.language)
|
||||
|
||||
resp = self.assistant.assist(user_input.text)
|
||||
resp = await self.hass.async_add_executor_job(
|
||||
self.assistant.assist, user_input.text
|
||||
)
|
||||
text_response = resp[0] or "<empty response>"
|
||||
|
||||
intent_response = intent.IntentResponse(language=user_input.language)
|
||||
|
@ -79,7 +79,7 @@ async def async_send_text_commands(
|
||||
) as assistant:
|
||||
command_response_list = []
|
||||
for command in commands:
|
||||
resp = assistant.assist(command)
|
||||
resp = await hass.async_add_executor_job(assistant.assist, command)
|
||||
text_response = resp[0]
|
||||
_LOGGER.debug("command: %s\nresponse: %s", command, text_response)
|
||||
audio_response = resp[2]
|
||||
|
@ -182,11 +182,11 @@ class GoogleGenerativeAIAgent(conversation.AbstractConversationAgent):
|
||||
conversation_id = ulid.ulid_now()
|
||||
messages = [{}, {}]
|
||||
|
||||
intent_response = intent.IntentResponse(language=user_input.language)
|
||||
try:
|
||||
prompt = self._async_generate_prompt(raw_prompt)
|
||||
except TemplateError as err:
|
||||
_LOGGER.error("Error rendering prompt: %s", err)
|
||||
intent_response = intent.IntentResponse(language=user_input.language)
|
||||
intent_response.async_set_error(
|
||||
intent.IntentResponseErrorCode.UNKNOWN,
|
||||
f"Sorry, I had a problem with my template: {err}",
|
||||
@ -210,7 +210,6 @@ class GoogleGenerativeAIAgent(conversation.AbstractConversationAgent):
|
||||
genai_types.StopCandidateException,
|
||||
) as err:
|
||||
_LOGGER.error("Error sending message: %s", err)
|
||||
intent_response = intent.IntentResponse(language=user_input.language)
|
||||
intent_response.async_set_error(
|
||||
intent.IntentResponseErrorCode.UNKNOWN,
|
||||
f"Sorry, I had a problem talking to Google Generative AI: {err}",
|
||||
@ -220,9 +219,15 @@ class GoogleGenerativeAIAgent(conversation.AbstractConversationAgent):
|
||||
)
|
||||
|
||||
_LOGGER.debug("Response: %s", chat_response.parts)
|
||||
if not chat_response.parts:
|
||||
intent_response.async_set_error(
|
||||
intent.IntentResponseErrorCode.UNKNOWN,
|
||||
"Sorry, I had a problem talking to Google Generative AI. Likely blocked",
|
||||
)
|
||||
return conversation.ConversationResult(
|
||||
response=intent_response, conversation_id=conversation_id
|
||||
)
|
||||
self.history[conversation_id] = chat.history
|
||||
|
||||
intent_response = intent.IntentResponse(language=user_input.language)
|
||||
intent_response.async_set_speech(chat_response.text)
|
||||
return conversation.ConversationResult(
|
||||
response=intent_response, conversation_id=conversation_id
|
||||
|
@ -57,6 +57,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
|
||||
|
||||
if "amc:api" not in entry.data["token"]["scope"]:
|
||||
# We raise ConfigEntryAuthFailed here because the websocket can't be used
|
||||
# without the scope. So only polling would be possible.
|
||||
raise ConfigEntryAuthFailed
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
return True
|
||||
|
||||
|
@ -13,7 +13,9 @@ from homeassistant.helpers import config_entry_oauth2_flow
|
||||
from .const import DOMAIN, NAME
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_USER_ID = "user_id"
|
||||
HUSQVARNA_DEV_PORTAL_URL = "https://developer.husqvarnagroup.cloud/applications"
|
||||
|
||||
|
||||
class HusqvarnaConfigFlowHandler(
|
||||
@ -29,8 +31,14 @@ class HusqvarnaConfigFlowHandler(
|
||||
async def async_oauth_create_entry(self, data: dict[str, Any]) -> ConfigFlowResult:
|
||||
"""Create an entry for the flow."""
|
||||
token = data[CONF_TOKEN]
|
||||
if "amc:api" not in token["scope"] and not self.reauth_entry:
|
||||
return self.async_abort(reason="missing_amc_scope")
|
||||
user_id = token[CONF_USER_ID]
|
||||
if self.reauth_entry:
|
||||
if "amc:api" not in token["scope"]:
|
||||
return self.async_update_reload_and_abort(
|
||||
self.reauth_entry, data=data, reason="missing_amc_scope"
|
||||
)
|
||||
if self.reauth_entry.unique_id != user_id:
|
||||
return self.async_abort(reason="wrong_account")
|
||||
return self.async_update_reload_and_abort(self.reauth_entry, data=data)
|
||||
@ -56,6 +64,9 @@ class HusqvarnaConfigFlowHandler(
|
||||
self.reauth_entry = self.hass.config_entries.async_get_entry(
|
||||
self.context["entry_id"]
|
||||
)
|
||||
if self.reauth_entry is not None:
|
||||
if "amc:api" not in self.reauth_entry.data["token"]["scope"]:
|
||||
return await self.async_step_missing_scope()
|
||||
return await self.async_step_reauth_confirm()
|
||||
|
||||
async def async_step_reauth_confirm(
|
||||
@ -65,3 +76,19 @@ class HusqvarnaConfigFlowHandler(
|
||||
if user_input is None:
|
||||
return self.async_show_form(step_id="reauth_confirm")
|
||||
return await self.async_step_user()
|
||||
|
||||
async def async_step_missing_scope(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Confirm reauth for missing scope."""
|
||||
if user_input is None and self.reauth_entry is not None:
|
||||
token_structured = structure_token(
|
||||
self.reauth_entry.data["token"]["access_token"]
|
||||
)
|
||||
return self.async_show_form(
|
||||
step_id="missing_scope",
|
||||
description_placeholders={
|
||||
"application_url": f"{HUSQVARNA_DEV_PORTAL_URL}/{token_structured.client_id}"
|
||||
},
|
||||
)
|
||||
return await self.async_step_user()
|
||||
|
@ -4,12 +4,17 @@ import asyncio
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from aioautomower.exceptions import ApiException, HusqvarnaWSServerHandshakeError
|
||||
from aioautomower.exceptions import (
|
||||
ApiException,
|
||||
AuthException,
|
||||
HusqvarnaWSServerHandshakeError,
|
||||
)
|
||||
from aioautomower.model import MowerAttributes
|
||||
from aioautomower.session import AutomowerSession
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import DOMAIN
|
||||
@ -46,6 +51,8 @@ class AutomowerDataUpdateCoordinator(DataUpdateCoordinator[dict[str, MowerAttrib
|
||||
return await self.api.get_status()
|
||||
except ApiException as err:
|
||||
raise UpdateFailed(err) from err
|
||||
except AuthException as err:
|
||||
raise ConfigEntryAuthFailed(err) from err
|
||||
|
||||
@callback
|
||||
def callback(self, ws_data: dict[str, MowerAttributes]) -> None:
|
||||
|
@ -5,6 +5,10 @@
|
||||
"title": "[%key:common::config_flow::title::reauth%]",
|
||||
"description": "The Husqvarna Automower integration needs to re-authenticate your account"
|
||||
},
|
||||
"missing_scope": {
|
||||
"title": "Your account is missing some API connections",
|
||||
"description": "For the best experience with this integration both the `Authentication API` and the `Automower Connect API` should be connected. Please make sure that both of them are connected to your account in the [Husqvarna Developer Portal]({application_url})."
|
||||
},
|
||||
"pick_implementation": {
|
||||
"title": "[%key:common::config_flow::title::oauth2_pick_implementation%]"
|
||||
}
|
||||
@ -22,7 +26,8 @@
|
||||
"oauth_unauthorized": "[%key:common::config_flow::abort::oauth2_unauthorized%]",
|
||||
"oauth_failed": "[%key:common::config_flow::abort::oauth2_failed%]",
|
||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
|
||||
"wrong_account": "You can only reauthenticate this entry with the same Husqvarna account."
|
||||
"wrong_account": "You can only reauthenticate this entry with the same Husqvarna account.",
|
||||
"missing_amc_scope": "The `Authentication API` and the `Automower Connect API` are not connected to your application in the Husqvarna Developer Portal."
|
||||
},
|
||||
"create_entry": {
|
||||
"default": "[%key:common::config_flow::create_entry::authenticated%]"
|
||||
|
@ -22,6 +22,7 @@ from .const import (
|
||||
CONF_CAT,
|
||||
CONF_DIM_STEPS,
|
||||
CONF_HOUSECODE,
|
||||
CONF_HUB_VERSION,
|
||||
CONF_SUBCAT,
|
||||
CONF_UNITCODE,
|
||||
HOUSECODES,
|
||||
@ -143,6 +144,7 @@ def build_hub_schema(
|
||||
schema = {
|
||||
vol.Required(CONF_HOST, default=host): str,
|
||||
vol.Required(CONF_PORT, default=port): int,
|
||||
vol.Required(CONF_HUB_VERSION, default=hub_version): int,
|
||||
}
|
||||
if hub_version == 2:
|
||||
schema[vol.Required(CONF_USERNAME, default=username)] = str
|
||||
|
@ -480,7 +480,13 @@ class KodiEntity(MediaPlayerEntity):
|
||||
self._reset_state()
|
||||
return
|
||||
|
||||
self._players = await self._kodi.get_players()
|
||||
try:
|
||||
self._players = await self._kodi.get_players()
|
||||
except (TransportError, ProtocolError):
|
||||
if not self._connection.can_subscribe:
|
||||
self._reset_state()
|
||||
return
|
||||
raise
|
||||
|
||||
if self._kodi_is_off:
|
||||
self._reset_state()
|
||||
|
@ -96,7 +96,7 @@ class LutronCasetaTiltOnlyBlind(LutronCasetaDeviceUpdatableEntity, CoverEntity):
|
||||
|
||||
async def async_set_cover_tilt_position(self, **kwargs: Any) -> None:
|
||||
"""Move the blind to a specific tilt."""
|
||||
self._smartbridge.set_tilt(self.device_id, kwargs[ATTR_TILT_POSITION])
|
||||
await self._smartbridge.set_tilt(self.device_id, kwargs[ATTR_TILT_POSITION])
|
||||
|
||||
|
||||
PYLUTRON_TYPE_TO_CLASSES = {
|
||||
|
@ -83,8 +83,18 @@ if TYPE_CHECKING:
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
MIN_BUFFER_SIZE = 131072 # Minimum buffer size to use if preferred size fails
|
||||
PREFERRED_BUFFER_SIZE = 2097152 # Set receive buffer size to 2MB
|
||||
|
||||
DISCOVERY_COOLDOWN = 5
|
||||
INITIAL_SUBSCRIBE_COOLDOWN = 3.0
|
||||
# The initial subscribe cooldown controls how long to wait to group
|
||||
# subscriptions together. This is to avoid making too many subscribe
|
||||
# requests in a short period of time. If the number is too low, the
|
||||
# system will be flooded with subscribe requests. If the number is too
|
||||
# high, we risk being flooded with responses to the subscribe requests
|
||||
# which can exceed the receive buffer size of the socket. To mitigate
|
||||
# this, we increase the receive buffer size of the socket as well.
|
||||
INITIAL_SUBSCRIBE_COOLDOWN = 0.5
|
||||
SUBSCRIBE_COOLDOWN = 0.1
|
||||
UNSUBSCRIBE_COOLDOWN = 0.1
|
||||
TIMEOUT_ACK = 10
|
||||
@ -429,6 +439,7 @@ class MQTT:
|
||||
hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, self._async_ha_stop),
|
||||
)
|
||||
)
|
||||
self._socket_buffersize: int | None = None
|
||||
|
||||
@callback
|
||||
def _async_ha_started(self, _hass: HomeAssistant) -> None:
|
||||
@ -529,6 +540,29 @@ class MQTT:
|
||||
self.hass, self._misc_loop(), name="mqtt misc loop"
|
||||
)
|
||||
|
||||
def _increase_socket_buffer_size(self, sock: SocketType) -> None:
|
||||
"""Increase the socket buffer size."""
|
||||
new_buffer_size = PREFERRED_BUFFER_SIZE
|
||||
while True:
|
||||
try:
|
||||
# Some operating systems do not allow us to set the preferred
|
||||
# buffer size. In that case we try some other size options.
|
||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, new_buffer_size)
|
||||
except OSError as err:
|
||||
if new_buffer_size <= MIN_BUFFER_SIZE:
|
||||
_LOGGER.warning(
|
||||
"Unable to increase the socket buffer size to %s; "
|
||||
"The connection may be unstable if the MQTT broker "
|
||||
"sends data at volume or a large amount of subscriptions "
|
||||
"need to be processed: %s",
|
||||
new_buffer_size,
|
||||
err,
|
||||
)
|
||||
return
|
||||
new_buffer_size //= 2
|
||||
else:
|
||||
return
|
||||
|
||||
def _on_socket_open(
|
||||
self, client: mqtt.Client, userdata: Any, sock: SocketType
|
||||
) -> None:
|
||||
@ -545,6 +579,7 @@ class MQTT:
|
||||
fileno = sock.fileno()
|
||||
_LOGGER.debug("%s: connection opened %s", self.config_entry.title, fileno)
|
||||
if fileno > -1:
|
||||
self._increase_socket_buffer_size(sock)
|
||||
self.loop.add_reader(sock, partial(self._async_reader_callback, client))
|
||||
self._async_start_misc_loop()
|
||||
|
||||
|
@ -24,7 +24,7 @@ class OndiloIcoCoordinator(DataUpdateCoordinator[list[dict[str, Any]]]):
|
||||
hass,
|
||||
logger=_LOGGER,
|
||||
name=DOMAIN,
|
||||
update_interval=timedelta(minutes=5),
|
||||
update_interval=timedelta(minutes=20),
|
||||
)
|
||||
self.api = api
|
||||
|
||||
|
@ -55,7 +55,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
use_tls = entry.data[CONF_SSL]
|
||||
verify_tls = entry.data[CONF_VERIFY_SSL]
|
||||
location = entry.data[CONF_LOCATION]
|
||||
api_key = entry.data.get(CONF_API_KEY)
|
||||
api_key = entry.data.get(CONF_API_KEY, "")
|
||||
|
||||
# remove obsolet CONF_STATISTICS_ONLY from entry.data
|
||||
if CONF_STATISTICS_ONLY in entry.data:
|
||||
|
@ -1,9 +1,10 @@
|
||||
"""Base entity for poolsense integration."""
|
||||
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity import EntityDescription
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import ATTRIBUTION
|
||||
from .const import ATTRIBUTION, DOMAIN
|
||||
from .coordinator import PoolSenseDataUpdateCoordinator
|
||||
|
||||
|
||||
@ -11,6 +12,7 @@ class PoolSenseEntity(CoordinatorEntity[PoolSenseDataUpdateCoordinator]):
|
||||
"""Implements a common class elements representing the PoolSense component."""
|
||||
|
||||
_attr_attribution = ATTRIBUTION
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@ -21,5 +23,8 @@ class PoolSenseEntity(CoordinatorEntity[PoolSenseDataUpdateCoordinator]):
|
||||
"""Initialize poolsense sensor."""
|
||||
super().__init__(coordinator)
|
||||
self.entity_description = description
|
||||
self._attr_name = f"PoolSense {description.name}"
|
||||
self._attr_unique_id = f"{email}-{description.key}"
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, email)},
|
||||
model="PoolSense",
|
||||
)
|
||||
|
@ -85,6 +85,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
|
||||
try:
|
||||
await host.update_states()
|
||||
except CredentialsInvalidError as err:
|
||||
await host.stop()
|
||||
raise ConfigEntryAuthFailed(err) from err
|
||||
except ReolinkError as err:
|
||||
raise UpdateFailed(str(err)) from err
|
||||
|
@ -18,5 +18,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/reolink",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["reolink_aio"],
|
||||
"requirements": ["reolink-aio==0.8.9"]
|
||||
"requirements": ["reolink-aio==0.8.10"]
|
||||
}
|
||||
|
@ -8,7 +8,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/sonos",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["soco"],
|
||||
"requirements": ["soco==0.30.3", "sonos-websocket==0.1.3"],
|
||||
"requirements": ["soco==0.30.4", "sonos-websocket==0.1.3"],
|
||||
"ssdp": [
|
||||
{
|
||||
"st": "urn:schemas-upnp-org:device:ZonePlayer:1"
|
||||
|
@ -92,7 +92,7 @@ SQUEEZEBOX_MODE = {
|
||||
}
|
||||
|
||||
|
||||
async def start_server_discovery(hass):
|
||||
async def start_server_discovery(hass: HomeAssistant) -> None:
|
||||
"""Start a server discovery task."""
|
||||
|
||||
def _discovered_server(server):
|
||||
@ -110,8 +110,9 @@ async def start_server_discovery(hass):
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
if DISCOVERY_TASK not in hass.data[DOMAIN]:
|
||||
_LOGGER.debug("Adding server discovery task for squeezebox")
|
||||
hass.data[DOMAIN][DISCOVERY_TASK] = hass.async_create_task(
|
||||
async_discover(_discovered_server)
|
||||
hass.data[DOMAIN][DISCOVERY_TASK] = hass.async_create_background_task(
|
||||
async_discover(_discovered_server),
|
||||
name="squeezebox server discovery",
|
||||
)
|
||||
|
||||
|
||||
|
@ -24,7 +24,6 @@ SCAN_INTERVAL = timedelta(minutes=1)
|
||||
|
||||
ATTR_LAST_UPDATED = "time_last_updated"
|
||||
|
||||
SIGNAL_UPDATE_ENTITY = "tellduslive_update"
|
||||
TELLDUS_DISCOVERY_NEW = "telldus_new_{}_{}"
|
||||
|
||||
CLOUD_NAME = "Cloud API"
|
||||
|
@ -46,14 +46,14 @@ class TelldusLiveCover(TelldusLiveEntity, CoverEntity):
|
||||
def close_cover(self, **kwargs: Any) -> None:
|
||||
"""Close the cover."""
|
||||
self.device.down()
|
||||
self._update_callback()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def open_cover(self, **kwargs: Any) -> None:
|
||||
"""Open the cover."""
|
||||
self.device.up()
|
||||
self._update_callback()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def stop_cover(self, **kwargs: Any) -> None:
|
||||
"""Stop the cover."""
|
||||
self.device.stop()
|
||||
self._update_callback()
|
||||
self.schedule_update_ha_state()
|
||||
|
@ -11,7 +11,6 @@ from homeassistant.const import (
|
||||
ATTR_MODEL,
|
||||
ATTR_VIA_DEVICE,
|
||||
)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity import Entity
|
||||
@ -33,25 +32,16 @@ class TelldusLiveEntity(Entity):
|
||||
"""Initialize the entity."""
|
||||
self._id = device_id
|
||||
self._client = client
|
||||
self._async_unsub_dispatcher_connect = None
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Call when entity is added to hass."""
|
||||
_LOGGER.debug("Created device %s", self)
|
||||
self._async_unsub_dispatcher_connect = async_dispatcher_connect(
|
||||
self.hass, SIGNAL_UPDATE_ENTITY, self._update_callback
|
||||
self.async_on_remove(
|
||||
async_dispatcher_connect(
|
||||
self.hass, SIGNAL_UPDATE_ENTITY, self.async_write_ha_state
|
||||
)
|
||||
)
|
||||
|
||||
async def async_will_remove_from_hass(self):
|
||||
"""Disconnect dispatcher listener when removed."""
|
||||
if self._async_unsub_dispatcher_connect:
|
||||
self._async_unsub_dispatcher_connect()
|
||||
|
||||
@callback
|
||||
def _update_callback(self):
|
||||
"""Return the property of the device might have changed."""
|
||||
self.async_write_ha_state()
|
||||
|
||||
@property
|
||||
def device_id(self):
|
||||
"""Return the id of the device."""
|
||||
|
@ -50,7 +50,7 @@ class TelldusLiveLight(TelldusLiveEntity, LightEntity):
|
||||
def changed(self):
|
||||
"""Define a property of the device that might have changed."""
|
||||
self._last_brightness = self.brightness
|
||||
self._update_callback()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
@property
|
||||
def brightness(self):
|
||||
|
@ -45,9 +45,9 @@ class TelldusLiveSwitch(TelldusLiveEntity, SwitchEntity):
|
||||
def turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the switch on."""
|
||||
self.device.turn_on()
|
||||
self._update_callback()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn the switch off."""
|
||||
self.device.turn_off()
|
||||
self._update_callback()
|
||||
self.schedule_update_ha_state()
|
||||
|
@ -7,6 +7,6 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["wled==0.17.0"],
|
||||
"requirements": ["wled==0.17.1"],
|
||||
"zeroconf": ["_wled._tcp.local."]
|
||||
}
|
||||
|
@ -41,7 +41,6 @@ from zwave_js_server.const.command_class.thermostat import (
|
||||
THERMOSTAT_SETPOINT_PROPERTY,
|
||||
)
|
||||
from zwave_js_server.exceptions import UnknownValueData
|
||||
from zwave_js_server.model.device_class import DeviceClassItem
|
||||
from zwave_js_server.model.node import Node as ZwaveNode
|
||||
from zwave_js_server.model.value import (
|
||||
ConfigurationValue,
|
||||
@ -1180,14 +1179,22 @@ def async_discover_single_value(
|
||||
continue
|
||||
|
||||
# check device_class_generic
|
||||
if value.node.device_class and not check_device_class(
|
||||
value.node.device_class.generic, schema.device_class_generic
|
||||
if schema.device_class_generic and (
|
||||
not value.node.device_class
|
||||
or not any(
|
||||
value.node.device_class.generic.label == val
|
||||
for val in schema.device_class_generic
|
||||
)
|
||||
):
|
||||
continue
|
||||
|
||||
# check device_class_specific
|
||||
if value.node.device_class and not check_device_class(
|
||||
value.node.device_class.specific, schema.device_class_specific
|
||||
if schema.device_class_specific and (
|
||||
not value.node.device_class
|
||||
or not any(
|
||||
value.node.device_class.specific.label == val
|
||||
for val in schema.device_class_specific
|
||||
)
|
||||
):
|
||||
continue
|
||||
|
||||
@ -1379,15 +1386,3 @@ def check_value(value: ZwaveValue, schema: ZWaveValueDiscoverySchema) -> bool:
|
||||
if schema.stateful is not None and value.metadata.stateful != schema.stateful:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
@callback
|
||||
def check_device_class(
|
||||
device_class: DeviceClassItem, required_value: set[str] | None
|
||||
) -> bool:
|
||||
"""Check if device class id or label matches."""
|
||||
if required_value is None:
|
||||
return True
|
||||
if any(device_class.label == val for val in required_value):
|
||||
return True
|
||||
return False
|
||||
|
@ -23,7 +23,7 @@ if TYPE_CHECKING:
|
||||
APPLICATION_NAME: Final = "HomeAssistant"
|
||||
MAJOR_VERSION: Final = 2024
|
||||
MINOR_VERSION: Final = 5
|
||||
PATCH_VERSION: Final = "3"
|
||||
PATCH_VERSION: Final = "4"
|
||||
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
||||
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
|
||||
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0)
|
||||
|
@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "homeassistant"
|
||||
version = "2024.5.3"
|
||||
version = "2024.5.4"
|
||||
license = {text = "Apache-2.0"}
|
||||
description = "Open-source home automation platform running on Python 3."
|
||||
readme = "README.rst"
|
||||
|
@ -1649,7 +1649,7 @@ pyCEC==0.5.2
|
||||
pyControl4==1.1.0
|
||||
|
||||
# homeassistant.components.duotecno
|
||||
pyDuotecno==2024.3.2
|
||||
pyDuotecno==2024.5.0
|
||||
|
||||
# homeassistant.components.electrasmart
|
||||
pyElectra==1.2.0
|
||||
@ -2439,7 +2439,7 @@ renault-api==0.2.2
|
||||
renson-endura-delta==1.7.1
|
||||
|
||||
# homeassistant.components.reolink
|
||||
reolink-aio==0.8.9
|
||||
reolink-aio==0.8.10
|
||||
|
||||
# homeassistant.components.idteck_prox
|
||||
rfk101py==0.0.1
|
||||
@ -2572,7 +2572,7 @@ smhi-pkg==1.0.16
|
||||
snapcast==2.3.6
|
||||
|
||||
# homeassistant.components.sonos
|
||||
soco==0.30.3
|
||||
soco==0.30.4
|
||||
|
||||
# homeassistant.components.solaredge_local
|
||||
solaredge-local==0.2.3
|
||||
@ -2866,7 +2866,7 @@ wiffi==1.1.2
|
||||
wirelesstagpy==0.8.1
|
||||
|
||||
# homeassistant.components.wled
|
||||
wled==0.17.0
|
||||
wled==0.17.1
|
||||
|
||||
# homeassistant.components.wolflink
|
||||
wolf-comm==0.0.7
|
||||
|
@ -1305,7 +1305,7 @@ pyCEC==0.5.2
|
||||
pyControl4==1.1.0
|
||||
|
||||
# homeassistant.components.duotecno
|
||||
pyDuotecno==2024.3.2
|
||||
pyDuotecno==2024.5.0
|
||||
|
||||
# homeassistant.components.electrasmart
|
||||
pyElectra==1.2.0
|
||||
@ -1897,7 +1897,7 @@ renault-api==0.2.2
|
||||
renson-endura-delta==1.7.1
|
||||
|
||||
# homeassistant.components.reolink
|
||||
reolink-aio==0.8.9
|
||||
reolink-aio==0.8.10
|
||||
|
||||
# homeassistant.components.rflink
|
||||
rflink==0.0.66
|
||||
@ -1991,7 +1991,7 @@ smhi-pkg==1.0.16
|
||||
snapcast==2.3.6
|
||||
|
||||
# homeassistant.components.sonos
|
||||
soco==0.30.3
|
||||
soco==0.30.4
|
||||
|
||||
# homeassistant.components.solax
|
||||
solax==3.1.0
|
||||
@ -2222,7 +2222,7 @@ whois==0.9.27
|
||||
wiffi==1.1.2
|
||||
|
||||
# homeassistant.components.wled
|
||||
wled==0.17.0
|
||||
wled==0.17.1
|
||||
|
||||
# homeassistant.components.wolflink
|
||||
wolf-comm==0.0.7
|
||||
|
@ -6,12 +6,12 @@ from unittest.mock import AsyncMock
|
||||
import pytest
|
||||
from python_homeassistant_analytics import HomeassistantAnalyticsConnectionError
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.analytics_insights.const import (
|
||||
CONF_TRACKED_CUSTOM_INTEGRATIONS,
|
||||
CONF_TRACKED_INTEGRATIONS,
|
||||
DOMAIN,
|
||||
)
|
||||
from homeassistant.config_entries import SOURCE_USER
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
|
||||
@ -61,7 +61,7 @@ async def test_form(
|
||||
) -> None:
|
||||
"""Test we get the form."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
|
||||
@ -96,7 +96,7 @@ async def test_submitting_empty_form(
|
||||
) -> None:
|
||||
"""Test we can't submit an empty form."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
|
||||
@ -128,20 +128,28 @@ async def test_submitting_empty_form(
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("exception", "reason"),
|
||||
[
|
||||
(HomeassistantAnalyticsConnectionError, "cannot_connect"),
|
||||
(Exception, "unknown"),
|
||||
],
|
||||
)
|
||||
async def test_form_cannot_connect(
|
||||
hass: HomeAssistant, mock_analytics_client: AsyncMock
|
||||
hass: HomeAssistant,
|
||||
mock_analytics_client: AsyncMock,
|
||||
exception: Exception,
|
||||
reason: str,
|
||||
) -> None:
|
||||
"""Test we handle cannot connect error."""
|
||||
|
||||
mock_analytics_client.get_integrations.side_effect = (
|
||||
HomeassistantAnalyticsConnectionError
|
||||
)
|
||||
mock_analytics_client.get_integrations.side_effect = exception
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "cannot_connect"
|
||||
assert result["reason"] == reason
|
||||
|
||||
|
||||
async def test_form_already_configured(
|
||||
@ -159,7 +167,7 @@ async def test_form_already_configured(
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "single_instance_allowed"
|
||||
|
@ -1522,4 +1522,4 @@ async def test_verify_group_color_mode_fallback(
|
||||
)
|
||||
group_state = hass.states.get("light.opbergruimte")
|
||||
assert group_state.state == STATE_ON
|
||||
assert group_state.attributes[ATTR_COLOR_MODE] is ColorMode.UNKNOWN
|
||||
assert group_state.attributes[ATTR_COLOR_MODE] is ColorMode.BRIGHTNESS
|
||||
|
@ -71,6 +71,22 @@ async def test_services(
|
||||
|
||||
mock_fully_kiosk.setConfigurationString.assert_called_once_with(key, value)
|
||||
|
||||
key = "test_key"
|
||||
value = 1234
|
||||
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_SET_CONFIG,
|
||||
{
|
||||
ATTR_DEVICE_ID: [device_entry.id],
|
||||
ATTR_KEY: key,
|
||||
ATTR_VALUE: value,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
mock_fully_kiosk.setConfigurationString.assert_called_with(key, str(value))
|
||||
|
||||
key = "test_key"
|
||||
value = "true"
|
||||
await hass.services.async_call(
|
||||
|
@ -95,29 +95,59 @@ async def test_default_prompt(
|
||||
suggested_area="Test Area 2",
|
||||
)
|
||||
with patch("google.generativeai.GenerativeModel") as mock_model:
|
||||
mock_model.return_value.start_chat.return_value = AsyncMock()
|
||||
mock_chat = AsyncMock()
|
||||
mock_model.return_value.start_chat.return_value = mock_chat
|
||||
chat_response = MagicMock()
|
||||
mock_chat.send_message_async.return_value = chat_response
|
||||
chat_response.parts = ["Hi there!"]
|
||||
chat_response.text = "Hi there!"
|
||||
result = await conversation.async_converse(
|
||||
hass, "hello", None, Context(), agent_id=mock_config_entry.entry_id
|
||||
)
|
||||
|
||||
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE
|
||||
assert result.response.as_dict()["speech"]["plain"]["speech"] == "Hi there!"
|
||||
assert [tuple(mock_call) for mock_call in mock_model.mock_calls] == snapshot
|
||||
|
||||
|
||||
async def test_error_handling(
|
||||
hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_init_component
|
||||
) -> None:
|
||||
"""Test that the default prompt works."""
|
||||
"""Test that client errors are caught."""
|
||||
with patch("google.generativeai.GenerativeModel") as mock_model:
|
||||
mock_chat = AsyncMock()
|
||||
mock_model.return_value.start_chat.return_value = mock_chat
|
||||
mock_chat.send_message_async.side_effect = ClientError("")
|
||||
mock_chat.send_message_async.side_effect = ClientError("some error")
|
||||
result = await conversation.async_converse(
|
||||
hass, "hello", None, Context(), agent_id=mock_config_entry.entry_id
|
||||
)
|
||||
|
||||
assert result.response.response_type == intent.IntentResponseType.ERROR, result
|
||||
assert result.response.error_code == "unknown", result
|
||||
assert result.response.as_dict()["speech"]["plain"]["speech"] == (
|
||||
"Sorry, I had a problem talking to Google Generative AI: None some error"
|
||||
)
|
||||
|
||||
|
||||
async def test_blocked_response(
|
||||
hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_init_component
|
||||
) -> None:
|
||||
"""Test response was blocked."""
|
||||
with patch("google.generativeai.GenerativeModel") as mock_model:
|
||||
mock_chat = AsyncMock()
|
||||
mock_model.return_value.start_chat.return_value = mock_chat
|
||||
chat_response = MagicMock()
|
||||
mock_chat.send_message_async.return_value = chat_response
|
||||
chat_response.parts = []
|
||||
result = await conversation.async_converse(
|
||||
hass, "hello", None, Context(), agent_id=mock_config_entry.entry_id
|
||||
)
|
||||
|
||||
assert result.response.response_type == intent.IntentResponseType.ERROR, result
|
||||
assert result.response.error_code == "unknown", result
|
||||
assert result.response.as_dict()["speech"]["plain"]["speech"] == (
|
||||
"Sorry, I had a problem talking to Google Generative AI. Likely blocked"
|
||||
)
|
||||
|
||||
|
||||
async def test_template_error(
|
||||
|
@ -22,7 +22,7 @@ from tests.common import MockConfigEntry, load_fixture, load_json_value_fixture
|
||||
|
||||
|
||||
@pytest.fixture(name="jwt")
|
||||
def load_jwt_fixture():
|
||||
def load_jwt_fixture() -> str:
|
||||
"""Load Fixture data."""
|
||||
return load_fixture("jwt", DOMAIN)
|
||||
|
||||
@ -33,8 +33,14 @@ def mock_expires_at() -> float:
|
||||
return time.time() + 3600
|
||||
|
||||
|
||||
@pytest.fixture(name="scope")
|
||||
def mock_scope() -> str:
|
||||
"""Fixture to set correct scope for the token."""
|
||||
return "iam:read amc:api"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_config_entry(jwt, expires_at: int) -> MockConfigEntry:
|
||||
def mock_config_entry(jwt: str, expires_at: int, scope: str) -> MockConfigEntry:
|
||||
"""Return the default mocked config entry."""
|
||||
return MockConfigEntry(
|
||||
version=1,
|
||||
@ -44,7 +50,7 @@ def mock_config_entry(jwt, expires_at: int) -> MockConfigEntry:
|
||||
"auth_implementation": DOMAIN,
|
||||
"token": {
|
||||
"access_token": jwt,
|
||||
"scope": "iam:read amc:api",
|
||||
"scope": scope,
|
||||
"expires_in": 86399,
|
||||
"refresh_token": "3012bc9f-7a65-4240-b817-9154ffdcc30f",
|
||||
"provider": "husqvarna",
|
||||
|
@ -2,6 +2,8 @@
|
||||
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.husqvarna_automower.const import (
|
||||
DOMAIN,
|
||||
@ -21,12 +23,21 @@ from tests.test_util.aiohttp import AiohttpClientMocker
|
||||
from tests.typing import ClientSessionGenerator
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("new_scope", "amount"),
|
||||
[
|
||||
("iam:read amc:api", 1),
|
||||
("iam:read", 0),
|
||||
],
|
||||
)
|
||||
async def test_full_flow(
|
||||
hass: HomeAssistant,
|
||||
hass_client_no_auth,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
current_request_with_host,
|
||||
jwt,
|
||||
jwt: str,
|
||||
new_scope: str,
|
||||
amount: int,
|
||||
) -> None:
|
||||
"""Check full flow."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
@ -56,7 +67,7 @@ async def test_full_flow(
|
||||
OAUTH2_TOKEN,
|
||||
json={
|
||||
"access_token": jwt,
|
||||
"scope": "iam:read amc:api",
|
||||
"scope": new_scope,
|
||||
"expires_in": 86399,
|
||||
"refresh_token": "mock-refresh-token",
|
||||
"provider": "husqvarna",
|
||||
@ -72,8 +83,8 @@ async def test_full_flow(
|
||||
) as mock_setup:
|
||||
await hass.config_entries.flow.async_configure(result["flow_id"])
|
||||
|
||||
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
|
||||
assert len(mock_setup.mock_calls) == 1
|
||||
assert len(hass.config_entries.async_entries(DOMAIN)) == amount
|
||||
assert len(mock_setup.mock_calls) == amount
|
||||
|
||||
|
||||
async def test_config_non_unique_profile(
|
||||
@ -129,6 +140,14 @@ async def test_config_non_unique_profile(
|
||||
assert result["reason"] == "already_configured"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("scope", "step_id", "reason", "new_scope"),
|
||||
[
|
||||
("iam:read amc:api", "reauth_confirm", "reauth_successful", "iam:read amc:api"),
|
||||
("iam:read", "missing_scope", "reauth_successful", "iam:read amc:api"),
|
||||
("iam:read", "missing_scope", "missing_amc_scope", "iam:read"),
|
||||
],
|
||||
)
|
||||
async def test_reauth(
|
||||
hass: HomeAssistant,
|
||||
hass_client_no_auth: ClientSessionGenerator,
|
||||
@ -136,7 +155,10 @@ async def test_reauth(
|
||||
mock_config_entry: MockConfigEntry,
|
||||
current_request_with_host: None,
|
||||
mock_automower_client: AsyncMock,
|
||||
jwt,
|
||||
jwt: str,
|
||||
step_id: str,
|
||||
new_scope: str,
|
||||
reason: str,
|
||||
) -> None:
|
||||
"""Test the reauthentication case updates the existing config entry."""
|
||||
|
||||
@ -148,7 +170,7 @@ async def test_reauth(
|
||||
flows = hass.config_entries.flow.async_progress()
|
||||
assert len(flows) == 1
|
||||
result = flows[0]
|
||||
assert result["step_id"] == "reauth_confirm"
|
||||
assert result["step_id"] == step_id
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
|
||||
state = config_entry_oauth2_flow._encode_jwt(
|
||||
@ -172,7 +194,7 @@ async def test_reauth(
|
||||
OAUTH2_TOKEN,
|
||||
json={
|
||||
"access_token": "mock-updated-token",
|
||||
"scope": "iam:read amc:api",
|
||||
"scope": new_scope,
|
||||
"expires_in": 86399,
|
||||
"refresh_token": "mock-refresh-token",
|
||||
"provider": "husqvarna",
|
||||
@ -191,7 +213,7 @@ async def test_reauth(
|
||||
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
|
||||
|
||||
assert result.get("type") is FlowResultType.ABORT
|
||||
assert result.get("reason") == "reauth_successful"
|
||||
assert result.get("reason") == reason
|
||||
|
||||
assert mock_config_entry.unique_id == USER_ID
|
||||
assert "token" in mock_config_entry.data
|
||||
@ -200,6 +222,12 @@ async def test_reauth(
|
||||
assert mock_config_entry.data["token"].get("refresh_token") == "mock-refresh-token"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("user_id", "reason"),
|
||||
[
|
||||
("wrong_user_id", "wrong_account"),
|
||||
],
|
||||
)
|
||||
async def test_reauth_wrong_account(
|
||||
hass: HomeAssistant,
|
||||
hass_client_no_auth: ClientSessionGenerator,
|
||||
@ -208,6 +236,9 @@ async def test_reauth_wrong_account(
|
||||
current_request_with_host: None,
|
||||
mock_automower_client: AsyncMock,
|
||||
jwt,
|
||||
user_id: str,
|
||||
reason: str,
|
||||
scope: str,
|
||||
) -> None:
|
||||
"""Test the reauthentication aborts, if user tries to reauthenticate with another account."""
|
||||
|
||||
@ -247,7 +278,7 @@ async def test_reauth_wrong_account(
|
||||
"expires_in": 86399,
|
||||
"refresh_token": "mock-refresh-token",
|
||||
"provider": "husqvarna",
|
||||
"user_id": "wrong-user-id",
|
||||
"user_id": user_id,
|
||||
"token_type": "Bearer",
|
||||
"expires_at": 1697753347,
|
||||
},
|
||||
@ -262,7 +293,7 @@ async def test_reauth_wrong_account(
|
||||
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
|
||||
|
||||
assert result.get("type") is FlowResultType.ABORT
|
||||
assert result.get("reason") == "wrong_account"
|
||||
assert result.get("reason") == reason
|
||||
|
||||
assert mock_config_entry.unique_id == USER_ID
|
||||
assert "token" in mock_config_entry.data
|
||||
|
@ -5,7 +5,11 @@ import http
|
||||
import time
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from aioautomower.exceptions import ApiException, HusqvarnaWSServerHandshakeError
|
||||
from aioautomower.exceptions import (
|
||||
ApiException,
|
||||
AuthException,
|
||||
HusqvarnaWSServerHandshakeError,
|
||||
)
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
@ -39,6 +43,26 @@ async def test_load_unload_entry(
|
||||
assert entry.state is ConfigEntryState.NOT_LOADED
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("scope"),
|
||||
[
|
||||
("iam:read"),
|
||||
],
|
||||
)
|
||||
async def test_load_missing_scope(
|
||||
hass: HomeAssistant,
|
||||
mock_automower_client: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test if the entry starts a reauth with the missing token scope."""
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR
|
||||
flows = hass.config_entries.flow.async_progress()
|
||||
assert len(flows) == 1
|
||||
result = flows[0]
|
||||
assert result["step_id"] == "missing_scope"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("expires_at", "status", "expected_state"),
|
||||
[
|
||||
@ -75,19 +99,25 @@ async def test_expired_token_refresh_failure(
|
||||
assert mock_config_entry.state is expected_state
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("exception", "entry_state"),
|
||||
[
|
||||
(ApiException, ConfigEntryState.SETUP_RETRY),
|
||||
(AuthException, ConfigEntryState.SETUP_ERROR),
|
||||
],
|
||||
)
|
||||
async def test_update_failed(
|
||||
hass: HomeAssistant,
|
||||
mock_automower_client: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
exception: Exception,
|
||||
entry_state: ConfigEntryState,
|
||||
) -> None:
|
||||
"""Test load and unload entry."""
|
||||
getattr(mock_automower_client, "get_status").side_effect = ApiException(
|
||||
"Test error"
|
||||
)
|
||||
"""Test update failed."""
|
||||
mock_automower_client.get_status.side_effect = exception("Test error")
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
entry = hass.config_entries.async_entries(DOMAIN)[0]
|
||||
|
||||
assert entry.state is ConfigEntryState.SETUP_RETRY
|
||||
assert entry.state is entry_state
|
||||
|
||||
|
||||
async def test_websocket_not_available(
|
||||
|
@ -4382,6 +4382,34 @@ async def test_server_sock_connect_and_disconnect(
|
||||
assert len(calls) == 0
|
||||
|
||||
|
||||
@patch("homeassistant.components.mqtt.client.INITIAL_SUBSCRIBE_COOLDOWN", 0.0)
|
||||
@patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.0)
|
||||
@patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0)
|
||||
async def test_server_sock_buffer_size(
|
||||
hass: HomeAssistant,
|
||||
mqtt_client_mock: MqttMockPahoClient,
|
||||
mqtt_mock_entry: MqttMockHAClientGenerator,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test handling the socket buffer size fails."""
|
||||
mqtt_mock = await mqtt_mock_entry()
|
||||
await hass.async_block_till_done()
|
||||
assert mqtt_mock.connected is True
|
||||
|
||||
mqtt_client_mock.loop_misc.return_value = paho_mqtt.MQTT_ERR_SUCCESS
|
||||
|
||||
client, server = socket.socketpair(
|
||||
family=socket.AF_UNIX, type=socket.SOCK_STREAM, proto=0
|
||||
)
|
||||
client.setblocking(False)
|
||||
server.setblocking(False)
|
||||
with patch.object(client, "setsockopt", side_effect=OSError("foo")):
|
||||
mqtt_client_mock.on_socket_open(mqtt_client_mock, None, client)
|
||||
mqtt_client_mock.on_socket_register_write(mqtt_client_mock, None, client)
|
||||
await hass.async_block_till_done()
|
||||
assert "Unable to increase the socket buffer size" in caplog.text
|
||||
|
||||
|
||||
@patch("homeassistant.components.mqtt.client.INITIAL_SUBSCRIBE_COOLDOWN", 0.0)
|
||||
@patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.0)
|
||||
@patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0)
|
||||
|
@ -1,7 +1,7 @@
|
||||
"""Test pi_hole component."""
|
||||
|
||||
import logging
|
||||
from unittest.mock import AsyncMock
|
||||
from unittest.mock import ANY, AsyncMock
|
||||
|
||||
from hole.exceptions import HoleError
|
||||
import pytest
|
||||
@ -12,12 +12,20 @@ from homeassistant.components.pi_hole.const import (
|
||||
SERVICE_DISABLE,
|
||||
SERVICE_DISABLE_ATTR_DURATION,
|
||||
)
|
||||
from homeassistant.const import ATTR_ENTITY_ID, CONF_HOST, CONF_NAME
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
CONF_HOST,
|
||||
CONF_LOCATION,
|
||||
CONF_NAME,
|
||||
CONF_SSL,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from . import (
|
||||
API_KEY,
|
||||
CONFIG_DATA,
|
||||
CONFIG_DATA_DEFAULTS,
|
||||
CONFIG_ENTRY_WITHOUT_API_KEY,
|
||||
SWITCH_ENTITY_ID,
|
||||
_create_mocked_hole,
|
||||
_patch_init_hole,
|
||||
@ -26,6 +34,29 @@ from . import (
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("config_entry_data", "expected_api_token"),
|
||||
[(CONFIG_DATA_DEFAULTS, API_KEY), (CONFIG_ENTRY_WITHOUT_API_KEY, "")],
|
||||
)
|
||||
async def test_setup_api(
|
||||
hass: HomeAssistant, config_entry_data: dict, expected_api_token: str
|
||||
) -> None:
|
||||
"""Tests the API object is created with the expected parameters."""
|
||||
mocked_hole = _create_mocked_hole()
|
||||
config_entry_data = {**config_entry_data, CONF_STATISTICS_ONLY: True}
|
||||
entry = MockConfigEntry(domain=pi_hole.DOMAIN, data=config_entry_data)
|
||||
entry.add_to_hass(hass)
|
||||
with _patch_init_hole(mocked_hole) as patched_init_hole:
|
||||
assert await hass.config_entries.async_setup(entry.entry_id)
|
||||
patched_init_hole.assert_called_once_with(
|
||||
config_entry_data[CONF_HOST],
|
||||
ANY,
|
||||
api_token=expected_api_token,
|
||||
location=config_entry_data[CONF_LOCATION],
|
||||
tls=config_entry_data[CONF_SSL],
|
||||
)
|
||||
|
||||
|
||||
async def test_setup_with_defaults(hass: HomeAssistant) -> None:
|
||||
"""Tests component setup with default config."""
|
||||
mocked_hole = _create_mocked_hole()
|
||||
|
@ -5,7 +5,7 @@ from typing import Any
|
||||
from unittest.mock import AsyncMock, MagicMock, Mock, patch
|
||||
|
||||
import pytest
|
||||
from reolink_aio.exceptions import ReolinkError
|
||||
from reolink_aio.exceptions import CredentialsInvalidError, ReolinkError
|
||||
|
||||
from homeassistant.components.reolink import FIRMWARE_UPDATE_INTERVAL, const
|
||||
from homeassistant.config import async_process_ha_core_config
|
||||
@ -50,6 +50,11 @@ pytestmark = pytest.mark.usefixtures("reolink_connect", "reolink_platforms")
|
||||
AsyncMock(side_effect=ReolinkError("Test error")),
|
||||
ConfigEntryState.SETUP_RETRY,
|
||||
),
|
||||
(
|
||||
"get_states",
|
||||
AsyncMock(side_effect=CredentialsInvalidError("Test error")),
|
||||
ConfigEntryState.SETUP_ERROR,
|
||||
),
|
||||
(
|
||||
"supported",
|
||||
Mock(return_value=False),
|
||||
|
@ -675,6 +675,12 @@ def central_scene_node_state_fixture():
|
||||
return json.loads(load_fixture("zwave_js/central_scene_node_state.json"))
|
||||
|
||||
|
||||
@pytest.fixture(name="light_device_class_is_null_state", scope="package")
|
||||
def light_device_class_is_null_state_fixture():
|
||||
"""Load node with device class is None state fixture data."""
|
||||
return json.loads(load_fixture("zwave_js/light_device_class_is_null_state.json"))
|
||||
|
||||
|
||||
# model fixtures
|
||||
|
||||
|
||||
@ -1325,3 +1331,11 @@ def central_scene_node_fixture(client, central_scene_node_state):
|
||||
node = Node(client, copy.deepcopy(central_scene_node_state))
|
||||
client.driver.controller.nodes[node.node_id] = node
|
||||
return node
|
||||
|
||||
|
||||
@pytest.fixture(name="light_device_class_is_null")
|
||||
def light_device_class_is_null_fixture(client, light_device_class_is_null_state):
|
||||
"""Mock a node when device class is null."""
|
||||
node = Node(client, copy.deepcopy(light_device_class_is_null_state))
|
||||
client.driver.controller.nodes[node.node_id] = node
|
||||
return node
|
||||
|
10611
tests/components/zwave_js/fixtures/light_device_class_is_null_state.json
Normal file
10611
tests/components/zwave_js/fixtures/light_device_class_is_null_state.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -305,3 +305,15 @@ async def test_indicator_test(
|
||||
"propertyKey": "Switch",
|
||||
}
|
||||
assert args["value"] is False
|
||||
|
||||
|
||||
async def test_light_device_class_is_null(
|
||||
hass: HomeAssistant, client, light_device_class_is_null, integration
|
||||
) -> None:
|
||||
"""Test that a Multilevel Switch CC value with a null device class is discovered as a light.
|
||||
|
||||
Tied to #117121.
|
||||
"""
|
||||
node = light_device_class_is_null
|
||||
assert node.device_class is None
|
||||
assert hass.states.get("light.bar_display_cases")
|
||||
|
Loading…
x
Reference in New Issue
Block a user