mirror of
https://github.com/home-assistant/core.git
synced 2025-07-21 20:27:08 +00:00
2024.5.4 (#117631)
This commit is contained in:
commit
3dc3de95fa
@ -82,6 +82,9 @@ class HomeassistantAnalyticsConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
except HomeassistantAnalyticsConnectionError:
|
except HomeassistantAnalyticsConnectionError:
|
||||||
LOGGER.exception("Error connecting to Home Assistant analytics")
|
LOGGER.exception("Error connecting to Home Assistant analytics")
|
||||||
return self.async_abort(reason="cannot_connect")
|
return self.async_abort(reason="cannot_connect")
|
||||||
|
except Exception: # pylint: disable=broad-except
|
||||||
|
LOGGER.exception("Unexpected error")
|
||||||
|
return self.async_abort(reason="unknown")
|
||||||
|
|
||||||
options = [
|
options = [
|
||||||
SelectOptionDict(
|
SelectOptionDict(
|
||||||
|
@ -13,7 +13,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"abort": {
|
"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": {
|
"error": {
|
||||||
"no_integration_selected": "You must select at least one integration to track"
|
"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."""
|
"""Implementation of the base Aurora Entity."""
|
||||||
|
|
||||||
_attr_attribution = ATTRIBUTION
|
_attr_attribution = ATTRIBUTION
|
||||||
|
_attr_has_entity_name = True
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
|
@ -2,13 +2,13 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
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.groups import GroupHandler
|
||||||
from pydeconz.interfaces.lights import LightHandler
|
from pydeconz.interfaces.lights import LightHandler
|
||||||
from pydeconz.models import ResourceType
|
from pydeconz.models import ResourceType
|
||||||
from pydeconz.models.event import EventType
|
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 pydeconz.models.light.light import Light, LightAlert, LightColorMode, LightEffect
|
||||||
|
|
||||||
from homeassistant.components.light import (
|
from homeassistant.components.light import (
|
||||||
@ -105,6 +105,23 @@ class SetStateAttributes(TypedDict, total=False):
|
|||||||
xy: tuple[float, float]
|
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(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
config_entry: ConfigEntry,
|
config_entry: ConfigEntry,
|
||||||
@ -148,11 +165,12 @@ async def async_setup_entry(
|
|||||||
if (group := hub.api.groups[group_id]) and not group.lights:
|
if (group := hub.api.groups[group_id]) and not group.lights:
|
||||||
return
|
return
|
||||||
|
|
||||||
first = True
|
lights = [
|
||||||
for light_id in group.lights:
|
light
|
||||||
if (light := hub.api.lights.lights.get(light_id)) and light.reachable:
|
for light_id in group.lights
|
||||||
group.update_color_state(light, update_all_attributes=first)
|
if (light := hub.api.lights.lights.get(light_id)) and light.reachable
|
||||||
first = False
|
]
|
||||||
|
update_color_state(group, lights, True)
|
||||||
|
|
||||||
async_add_entities([DeconzGroup(group, hub)])
|
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:
|
if self._device.reachable and "attr" not in self._device.changed_keys:
|
||||||
for group in self.hub.api.groups.values():
|
for group in self.hub.api.groups.values():
|
||||||
if self._device.resource_id in group.lights:
|
if self._device.resource_id in group.lights:
|
||||||
group.update_color_state(self._device)
|
update_color_state(group, [self._device])
|
||||||
|
|
||||||
|
|
||||||
class DeconzGroup(DeconzBaseLight[Group]):
|
class DeconzGroup(DeconzBaseLight[Group]):
|
||||||
|
@ -41,6 +41,11 @@ class DuotecnoEntity(Entity):
|
|||||||
"""When a unit has an update."""
|
"""When a unit has an update."""
|
||||||
self.async_write_ha_state()
|
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")
|
_T = TypeVar("_T", bound="DuotecnoEntity")
|
||||||
_P = ParamSpec("_P")
|
_P = ParamSpec("_P")
|
||||||
|
@ -7,5 +7,5 @@
|
|||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"loggers": ["pyduotecno", "pyduotecno-node", "pyduotecno-unit"],
|
"loggers": ["pyduotecno", "pyduotecno-node", "pyduotecno-unit"],
|
||||||
"quality_scale": "silver",
|
"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:
|
async def async_set_config(call: ServiceCall) -> None:
|
||||||
"""Set a Fully Kiosk Browser config value on the device."""
|
"""Set a Fully Kiosk Browser config value on the device."""
|
||||||
for coordinator in await collect_coordinators(call.data[ATTR_DEVICE_ID]):
|
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.
|
# Fully API has different methods for setting string and bool values.
|
||||||
# check if call.data[ATTR_VALUE] is a bool
|
# check if call.data[ATTR_VALUE] is a bool
|
||||||
if isinstance(call.data[ATTR_VALUE], bool) or call.data[
|
if isinstance(value, bool) or (
|
||||||
ATTR_VALUE
|
isinstance(value, str) and value.lower() in ("true", "false")
|
||||||
].lower() in ("true", "false"):
|
):
|
||||||
await coordinator.fully.setConfigurationBool(
|
await coordinator.fully.setConfigurationBool(key, value)
|
||||||
call.data[ATTR_KEY], call.data[ATTR_VALUE]
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
await coordinator.fully.setConfigurationString(
|
# Convert any int values to string
|
||||||
call.data[ATTR_KEY], call.data[ATTR_VALUE]
|
if isinstance(value, int):
|
||||||
)
|
value = str(value)
|
||||||
|
|
||||||
|
await coordinator.fully.setConfigurationString(key, value)
|
||||||
|
|
||||||
# Register all the above services
|
# Register all the above services
|
||||||
service_mapping = [
|
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_DEVICE_ID): cv.ensure_list,
|
||||||
vol.Required(ATTR_KEY): cv.string,
|
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.language = user_input.language
|
||||||
self.assistant = TextAssistant(credentials, self.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>"
|
text_response = resp[0] or "<empty response>"
|
||||||
|
|
||||||
intent_response = intent.IntentResponse(language=user_input.language)
|
intent_response = intent.IntentResponse(language=user_input.language)
|
||||||
|
@ -79,7 +79,7 @@ async def async_send_text_commands(
|
|||||||
) as assistant:
|
) as assistant:
|
||||||
command_response_list = []
|
command_response_list = []
|
||||||
for command in commands:
|
for command in commands:
|
||||||
resp = assistant.assist(command)
|
resp = await hass.async_add_executor_job(assistant.assist, command)
|
||||||
text_response = resp[0]
|
text_response = resp[0]
|
||||||
_LOGGER.debug("command: %s\nresponse: %s", command, text_response)
|
_LOGGER.debug("command: %s\nresponse: %s", command, text_response)
|
||||||
audio_response = resp[2]
|
audio_response = resp[2]
|
||||||
|
@ -182,11 +182,11 @@ class GoogleGenerativeAIAgent(conversation.AbstractConversationAgent):
|
|||||||
conversation_id = ulid.ulid_now()
|
conversation_id = ulid.ulid_now()
|
||||||
messages = [{}, {}]
|
messages = [{}, {}]
|
||||||
|
|
||||||
|
intent_response = intent.IntentResponse(language=user_input.language)
|
||||||
try:
|
try:
|
||||||
prompt = self._async_generate_prompt(raw_prompt)
|
prompt = self._async_generate_prompt(raw_prompt)
|
||||||
except TemplateError as err:
|
except TemplateError as err:
|
||||||
_LOGGER.error("Error rendering prompt: %s", err)
|
_LOGGER.error("Error rendering prompt: %s", err)
|
||||||
intent_response = intent.IntentResponse(language=user_input.language)
|
|
||||||
intent_response.async_set_error(
|
intent_response.async_set_error(
|
||||||
intent.IntentResponseErrorCode.UNKNOWN,
|
intent.IntentResponseErrorCode.UNKNOWN,
|
||||||
f"Sorry, I had a problem with my template: {err}",
|
f"Sorry, I had a problem with my template: {err}",
|
||||||
@ -210,7 +210,6 @@ class GoogleGenerativeAIAgent(conversation.AbstractConversationAgent):
|
|||||||
genai_types.StopCandidateException,
|
genai_types.StopCandidateException,
|
||||||
) as err:
|
) as err:
|
||||||
_LOGGER.error("Error sending message: %s", err)
|
_LOGGER.error("Error sending message: %s", err)
|
||||||
intent_response = intent.IntentResponse(language=user_input.language)
|
|
||||||
intent_response.async_set_error(
|
intent_response.async_set_error(
|
||||||
intent.IntentResponseErrorCode.UNKNOWN,
|
intent.IntentResponseErrorCode.UNKNOWN,
|
||||||
f"Sorry, I had a problem talking to Google Generative AI: {err}",
|
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)
|
_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
|
self.history[conversation_id] = chat.history
|
||||||
|
|
||||||
intent_response = intent.IntentResponse(language=user_input.language)
|
|
||||||
intent_response.async_set_speech(chat_response.text)
|
intent_response.async_set_speech(chat_response.text)
|
||||||
return conversation.ConversationResult(
|
return conversation.ConversationResult(
|
||||||
response=intent_response, conversation_id=conversation_id
|
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
|
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)
|
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@ -13,7 +13,9 @@ from homeassistant.helpers import config_entry_oauth2_flow
|
|||||||
from .const import DOMAIN, NAME
|
from .const import DOMAIN, NAME
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
CONF_USER_ID = "user_id"
|
CONF_USER_ID = "user_id"
|
||||||
|
HUSQVARNA_DEV_PORTAL_URL = "https://developer.husqvarnagroup.cloud/applications"
|
||||||
|
|
||||||
|
|
||||||
class HusqvarnaConfigFlowHandler(
|
class HusqvarnaConfigFlowHandler(
|
||||||
@ -29,8 +31,14 @@ class HusqvarnaConfigFlowHandler(
|
|||||||
async def async_oauth_create_entry(self, data: dict[str, Any]) -> ConfigFlowResult:
|
async def async_oauth_create_entry(self, data: dict[str, Any]) -> ConfigFlowResult:
|
||||||
"""Create an entry for the flow."""
|
"""Create an entry for the flow."""
|
||||||
token = data[CONF_TOKEN]
|
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]
|
user_id = token[CONF_USER_ID]
|
||||||
if self.reauth_entry:
|
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:
|
if self.reauth_entry.unique_id != user_id:
|
||||||
return self.async_abort(reason="wrong_account")
|
return self.async_abort(reason="wrong_account")
|
||||||
return self.async_update_reload_and_abort(self.reauth_entry, data=data)
|
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.reauth_entry = self.hass.config_entries.async_get_entry(
|
||||||
self.context["entry_id"]
|
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()
|
return await self.async_step_reauth_confirm()
|
||||||
|
|
||||||
async def async_step_reauth_confirm(
|
async def async_step_reauth_confirm(
|
||||||
@ -65,3 +76,19 @@ class HusqvarnaConfigFlowHandler(
|
|||||||
if user_input is None:
|
if user_input is None:
|
||||||
return self.async_show_form(step_id="reauth_confirm")
|
return self.async_show_form(step_id="reauth_confirm")
|
||||||
return await self.async_step_user()
|
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
|
from datetime import timedelta
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from aioautomower.exceptions import ApiException, HusqvarnaWSServerHandshakeError
|
from aioautomower.exceptions import (
|
||||||
|
ApiException,
|
||||||
|
AuthException,
|
||||||
|
HusqvarnaWSServerHandshakeError,
|
||||||
|
)
|
||||||
from aioautomower.model import MowerAttributes
|
from aioautomower.model import MowerAttributes
|
||||||
from aioautomower.session import AutomowerSession
|
from aioautomower.session import AutomowerSession
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
|
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||||
|
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
@ -46,6 +51,8 @@ class AutomowerDataUpdateCoordinator(DataUpdateCoordinator[dict[str, MowerAttrib
|
|||||||
return await self.api.get_status()
|
return await self.api.get_status()
|
||||||
except ApiException as err:
|
except ApiException as err:
|
||||||
raise UpdateFailed(err) from err
|
raise UpdateFailed(err) from err
|
||||||
|
except AuthException as err:
|
||||||
|
raise ConfigEntryAuthFailed(err) from err
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def callback(self, ws_data: dict[str, MowerAttributes]) -> None:
|
def callback(self, ws_data: dict[str, MowerAttributes]) -> None:
|
||||||
|
@ -5,6 +5,10 @@
|
|||||||
"title": "[%key:common::config_flow::title::reauth%]",
|
"title": "[%key:common::config_flow::title::reauth%]",
|
||||||
"description": "The Husqvarna Automower integration needs to re-authenticate your account"
|
"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": {
|
"pick_implementation": {
|
||||||
"title": "[%key:common::config_flow::title::oauth2_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_unauthorized": "[%key:common::config_flow::abort::oauth2_unauthorized%]",
|
||||||
"oauth_failed": "[%key:common::config_flow::abort::oauth2_failed%]",
|
"oauth_failed": "[%key:common::config_flow::abort::oauth2_failed%]",
|
||||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
|
"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": {
|
"create_entry": {
|
||||||
"default": "[%key:common::config_flow::create_entry::authenticated%]"
|
"default": "[%key:common::config_flow::create_entry::authenticated%]"
|
||||||
|
@ -22,6 +22,7 @@ from .const import (
|
|||||||
CONF_CAT,
|
CONF_CAT,
|
||||||
CONF_DIM_STEPS,
|
CONF_DIM_STEPS,
|
||||||
CONF_HOUSECODE,
|
CONF_HOUSECODE,
|
||||||
|
CONF_HUB_VERSION,
|
||||||
CONF_SUBCAT,
|
CONF_SUBCAT,
|
||||||
CONF_UNITCODE,
|
CONF_UNITCODE,
|
||||||
HOUSECODES,
|
HOUSECODES,
|
||||||
@ -143,6 +144,7 @@ def build_hub_schema(
|
|||||||
schema = {
|
schema = {
|
||||||
vol.Required(CONF_HOST, default=host): str,
|
vol.Required(CONF_HOST, default=host): str,
|
||||||
vol.Required(CONF_PORT, default=port): int,
|
vol.Required(CONF_PORT, default=port): int,
|
||||||
|
vol.Required(CONF_HUB_VERSION, default=hub_version): int,
|
||||||
}
|
}
|
||||||
if hub_version == 2:
|
if hub_version == 2:
|
||||||
schema[vol.Required(CONF_USERNAME, default=username)] = str
|
schema[vol.Required(CONF_USERNAME, default=username)] = str
|
||||||
|
@ -480,7 +480,13 @@ class KodiEntity(MediaPlayerEntity):
|
|||||||
self._reset_state()
|
self._reset_state()
|
||||||
return
|
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:
|
if self._kodi_is_off:
|
||||||
self._reset_state()
|
self._reset_state()
|
||||||
|
@ -96,7 +96,7 @@ class LutronCasetaTiltOnlyBlind(LutronCasetaDeviceUpdatableEntity, CoverEntity):
|
|||||||
|
|
||||||
async def async_set_cover_tilt_position(self, **kwargs: Any) -> None:
|
async def async_set_cover_tilt_position(self, **kwargs: Any) -> None:
|
||||||
"""Move the blind to a specific tilt."""
|
"""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 = {
|
PYLUTRON_TYPE_TO_CLASSES = {
|
||||||
|
@ -83,8 +83,18 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_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
|
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
|
SUBSCRIBE_COOLDOWN = 0.1
|
||||||
UNSUBSCRIBE_COOLDOWN = 0.1
|
UNSUBSCRIBE_COOLDOWN = 0.1
|
||||||
TIMEOUT_ACK = 10
|
TIMEOUT_ACK = 10
|
||||||
@ -429,6 +439,7 @@ class MQTT:
|
|||||||
hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, self._async_ha_stop),
|
hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, self._async_ha_stop),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
self._socket_buffersize: int | None = None
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _async_ha_started(self, _hass: HomeAssistant) -> None:
|
def _async_ha_started(self, _hass: HomeAssistant) -> None:
|
||||||
@ -529,6 +540,29 @@ class MQTT:
|
|||||||
self.hass, self._misc_loop(), name="mqtt misc loop"
|
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(
|
def _on_socket_open(
|
||||||
self, client: mqtt.Client, userdata: Any, sock: SocketType
|
self, client: mqtt.Client, userdata: Any, sock: SocketType
|
||||||
) -> None:
|
) -> None:
|
||||||
@ -545,6 +579,7 @@ class MQTT:
|
|||||||
fileno = sock.fileno()
|
fileno = sock.fileno()
|
||||||
_LOGGER.debug("%s: connection opened %s", self.config_entry.title, fileno)
|
_LOGGER.debug("%s: connection opened %s", self.config_entry.title, fileno)
|
||||||
if fileno > -1:
|
if fileno > -1:
|
||||||
|
self._increase_socket_buffer_size(sock)
|
||||||
self.loop.add_reader(sock, partial(self._async_reader_callback, client))
|
self.loop.add_reader(sock, partial(self._async_reader_callback, client))
|
||||||
self._async_start_misc_loop()
|
self._async_start_misc_loop()
|
||||||
|
|
||||||
|
@ -24,7 +24,7 @@ class OndiloIcoCoordinator(DataUpdateCoordinator[list[dict[str, Any]]]):
|
|||||||
hass,
|
hass,
|
||||||
logger=_LOGGER,
|
logger=_LOGGER,
|
||||||
name=DOMAIN,
|
name=DOMAIN,
|
||||||
update_interval=timedelta(minutes=5),
|
update_interval=timedelta(minutes=20),
|
||||||
)
|
)
|
||||||
self.api = api
|
self.api = api
|
||||||
|
|
||||||
|
@ -55,7 +55,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
use_tls = entry.data[CONF_SSL]
|
use_tls = entry.data[CONF_SSL]
|
||||||
verify_tls = entry.data[CONF_VERIFY_SSL]
|
verify_tls = entry.data[CONF_VERIFY_SSL]
|
||||||
location = entry.data[CONF_LOCATION]
|
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
|
# remove obsolet CONF_STATISTICS_ONLY from entry.data
|
||||||
if CONF_STATISTICS_ONLY in entry.data:
|
if CONF_STATISTICS_ONLY in entry.data:
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
"""Base entity for poolsense integration."""
|
"""Base entity for poolsense integration."""
|
||||||
|
|
||||||
|
from homeassistant.helpers.device_registry import DeviceInfo
|
||||||
from homeassistant.helpers.entity import EntityDescription
|
from homeassistant.helpers.entity import EntityDescription
|
||||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
|
|
||||||
from .const import ATTRIBUTION
|
from .const import ATTRIBUTION, DOMAIN
|
||||||
from .coordinator import PoolSenseDataUpdateCoordinator
|
from .coordinator import PoolSenseDataUpdateCoordinator
|
||||||
|
|
||||||
|
|
||||||
@ -11,6 +12,7 @@ class PoolSenseEntity(CoordinatorEntity[PoolSenseDataUpdateCoordinator]):
|
|||||||
"""Implements a common class elements representing the PoolSense component."""
|
"""Implements a common class elements representing the PoolSense component."""
|
||||||
|
|
||||||
_attr_attribution = ATTRIBUTION
|
_attr_attribution = ATTRIBUTION
|
||||||
|
_attr_has_entity_name = True
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@ -21,5 +23,8 @@ class PoolSenseEntity(CoordinatorEntity[PoolSenseDataUpdateCoordinator]):
|
|||||||
"""Initialize poolsense sensor."""
|
"""Initialize poolsense sensor."""
|
||||||
super().__init__(coordinator)
|
super().__init__(coordinator)
|
||||||
self.entity_description = description
|
self.entity_description = description
|
||||||
self._attr_name = f"PoolSense {description.name}"
|
|
||||||
self._attr_unique_id = f"{email}-{description.key}"
|
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:
|
try:
|
||||||
await host.update_states()
|
await host.update_states()
|
||||||
except CredentialsInvalidError as err:
|
except CredentialsInvalidError as err:
|
||||||
|
await host.stop()
|
||||||
raise ConfigEntryAuthFailed(err) from err
|
raise ConfigEntryAuthFailed(err) from err
|
||||||
except ReolinkError as err:
|
except ReolinkError as err:
|
||||||
raise UpdateFailed(str(err)) from err
|
raise UpdateFailed(str(err)) from err
|
||||||
|
@ -18,5 +18,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/reolink",
|
"documentation": "https://www.home-assistant.io/integrations/reolink",
|
||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"loggers": ["reolink_aio"],
|
"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",
|
"documentation": "https://www.home-assistant.io/integrations/sonos",
|
||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"loggers": ["soco"],
|
"loggers": ["soco"],
|
||||||
"requirements": ["soco==0.30.3", "sonos-websocket==0.1.3"],
|
"requirements": ["soco==0.30.4", "sonos-websocket==0.1.3"],
|
||||||
"ssdp": [
|
"ssdp": [
|
||||||
{
|
{
|
||||||
"st": "urn:schemas-upnp-org:device:ZonePlayer:1"
|
"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."""
|
"""Start a server discovery task."""
|
||||||
|
|
||||||
def _discovered_server(server):
|
def _discovered_server(server):
|
||||||
@ -110,8 +110,9 @@ async def start_server_discovery(hass):
|
|||||||
hass.data.setdefault(DOMAIN, {})
|
hass.data.setdefault(DOMAIN, {})
|
||||||
if DISCOVERY_TASK not in hass.data[DOMAIN]:
|
if DISCOVERY_TASK not in hass.data[DOMAIN]:
|
||||||
_LOGGER.debug("Adding server discovery task for squeezebox")
|
_LOGGER.debug("Adding server discovery task for squeezebox")
|
||||||
hass.data[DOMAIN][DISCOVERY_TASK] = hass.async_create_task(
|
hass.data[DOMAIN][DISCOVERY_TASK] = hass.async_create_background_task(
|
||||||
async_discover(_discovered_server)
|
async_discover(_discovered_server),
|
||||||
|
name="squeezebox server discovery",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -24,7 +24,6 @@ SCAN_INTERVAL = timedelta(minutes=1)
|
|||||||
|
|
||||||
ATTR_LAST_UPDATED = "time_last_updated"
|
ATTR_LAST_UPDATED = "time_last_updated"
|
||||||
|
|
||||||
SIGNAL_UPDATE_ENTITY = "tellduslive_update"
|
|
||||||
TELLDUS_DISCOVERY_NEW = "telldus_new_{}_{}"
|
TELLDUS_DISCOVERY_NEW = "telldus_new_{}_{}"
|
||||||
|
|
||||||
CLOUD_NAME = "Cloud API"
|
CLOUD_NAME = "Cloud API"
|
||||||
|
@ -46,14 +46,14 @@ class TelldusLiveCover(TelldusLiveEntity, CoverEntity):
|
|||||||
def close_cover(self, **kwargs: Any) -> None:
|
def close_cover(self, **kwargs: Any) -> None:
|
||||||
"""Close the cover."""
|
"""Close the cover."""
|
||||||
self.device.down()
|
self.device.down()
|
||||||
self._update_callback()
|
self.schedule_update_ha_state()
|
||||||
|
|
||||||
def open_cover(self, **kwargs: Any) -> None:
|
def open_cover(self, **kwargs: Any) -> None:
|
||||||
"""Open the cover."""
|
"""Open the cover."""
|
||||||
self.device.up()
|
self.device.up()
|
||||||
self._update_callback()
|
self.schedule_update_ha_state()
|
||||||
|
|
||||||
def stop_cover(self, **kwargs: Any) -> None:
|
def stop_cover(self, **kwargs: Any) -> None:
|
||||||
"""Stop the cover."""
|
"""Stop the cover."""
|
||||||
self.device.stop()
|
self.device.stop()
|
||||||
self._update_callback()
|
self.schedule_update_ha_state()
|
||||||
|
@ -11,7 +11,6 @@ from homeassistant.const import (
|
|||||||
ATTR_MODEL,
|
ATTR_MODEL,
|
||||||
ATTR_VIA_DEVICE,
|
ATTR_VIA_DEVICE,
|
||||||
)
|
)
|
||||||
from homeassistant.core import callback
|
|
||||||
from homeassistant.helpers.device_registry import DeviceInfo
|
from homeassistant.helpers.device_registry import DeviceInfo
|
||||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.helpers.entity import Entity
|
||||||
@ -33,25 +32,16 @@ class TelldusLiveEntity(Entity):
|
|||||||
"""Initialize the entity."""
|
"""Initialize the entity."""
|
||||||
self._id = device_id
|
self._id = device_id
|
||||||
self._client = client
|
self._client = client
|
||||||
self._async_unsub_dispatcher_connect = None
|
|
||||||
|
|
||||||
async def async_added_to_hass(self):
|
async def async_added_to_hass(self):
|
||||||
"""Call when entity is added to hass."""
|
"""Call when entity is added to hass."""
|
||||||
_LOGGER.debug("Created device %s", self)
|
_LOGGER.debug("Created device %s", self)
|
||||||
self._async_unsub_dispatcher_connect = async_dispatcher_connect(
|
self.async_on_remove(
|
||||||
self.hass, SIGNAL_UPDATE_ENTITY, self._update_callback
|
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
|
@property
|
||||||
def device_id(self):
|
def device_id(self):
|
||||||
"""Return the id of the device."""
|
"""Return the id of the device."""
|
||||||
|
@ -50,7 +50,7 @@ class TelldusLiveLight(TelldusLiveEntity, LightEntity):
|
|||||||
def changed(self):
|
def changed(self):
|
||||||
"""Define a property of the device that might have changed."""
|
"""Define a property of the device that might have changed."""
|
||||||
self._last_brightness = self.brightness
|
self._last_brightness = self.brightness
|
||||||
self._update_callback()
|
self.schedule_update_ha_state()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def brightness(self):
|
def brightness(self):
|
||||||
|
@ -45,9 +45,9 @@ class TelldusLiveSwitch(TelldusLiveEntity, SwitchEntity):
|
|||||||
def turn_on(self, **kwargs: Any) -> None:
|
def turn_on(self, **kwargs: Any) -> None:
|
||||||
"""Turn the switch on."""
|
"""Turn the switch on."""
|
||||||
self.device.turn_on()
|
self.device.turn_on()
|
||||||
self._update_callback()
|
self.schedule_update_ha_state()
|
||||||
|
|
||||||
def turn_off(self, **kwargs: Any) -> None:
|
def turn_off(self, **kwargs: Any) -> None:
|
||||||
"""Turn the switch off."""
|
"""Turn the switch off."""
|
||||||
self.device.turn_off()
|
self.device.turn_off()
|
||||||
self._update_callback()
|
self.schedule_update_ha_state()
|
||||||
|
@ -7,6 +7,6 @@
|
|||||||
"integration_type": "device",
|
"integration_type": "device",
|
||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"quality_scale": "platinum",
|
"quality_scale": "platinum",
|
||||||
"requirements": ["wled==0.17.0"],
|
"requirements": ["wled==0.17.1"],
|
||||||
"zeroconf": ["_wled._tcp.local."]
|
"zeroconf": ["_wled._tcp.local."]
|
||||||
}
|
}
|
||||||
|
@ -41,7 +41,6 @@ from zwave_js_server.const.command_class.thermostat import (
|
|||||||
THERMOSTAT_SETPOINT_PROPERTY,
|
THERMOSTAT_SETPOINT_PROPERTY,
|
||||||
)
|
)
|
||||||
from zwave_js_server.exceptions import UnknownValueData
|
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.node import Node as ZwaveNode
|
||||||
from zwave_js_server.model.value import (
|
from zwave_js_server.model.value import (
|
||||||
ConfigurationValue,
|
ConfigurationValue,
|
||||||
@ -1180,14 +1179,22 @@ def async_discover_single_value(
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
# check device_class_generic
|
# check device_class_generic
|
||||||
if value.node.device_class and not check_device_class(
|
if schema.device_class_generic and (
|
||||||
value.node.device_class.generic, schema.device_class_generic
|
not value.node.device_class
|
||||||
|
or not any(
|
||||||
|
value.node.device_class.generic.label == val
|
||||||
|
for val in schema.device_class_generic
|
||||||
|
)
|
||||||
):
|
):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# check device_class_specific
|
# check device_class_specific
|
||||||
if value.node.device_class and not check_device_class(
|
if schema.device_class_specific and (
|
||||||
value.node.device_class.specific, schema.device_class_specific
|
not value.node.device_class
|
||||||
|
or not any(
|
||||||
|
value.node.device_class.specific.label == val
|
||||||
|
for val in schema.device_class_specific
|
||||||
|
)
|
||||||
):
|
):
|
||||||
continue
|
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:
|
if schema.stateful is not None and value.metadata.stateful != schema.stateful:
|
||||||
return False
|
return False
|
||||||
return True
|
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"
|
APPLICATION_NAME: Final = "HomeAssistant"
|
||||||
MAJOR_VERSION: Final = 2024
|
MAJOR_VERSION: Final = 2024
|
||||||
MINOR_VERSION: Final = 5
|
MINOR_VERSION: Final = 5
|
||||||
PATCH_VERSION: Final = "3"
|
PATCH_VERSION: Final = "4"
|
||||||
__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, 12, 0)
|
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0)
|
||||||
|
@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "homeassistant"
|
name = "homeassistant"
|
||||||
version = "2024.5.3"
|
version = "2024.5.4"
|
||||||
license = {text = "Apache-2.0"}
|
license = {text = "Apache-2.0"}
|
||||||
description = "Open-source home automation platform running on Python 3."
|
description = "Open-source home automation platform running on Python 3."
|
||||||
readme = "README.rst"
|
readme = "README.rst"
|
||||||
|
@ -1649,7 +1649,7 @@ pyCEC==0.5.2
|
|||||||
pyControl4==1.1.0
|
pyControl4==1.1.0
|
||||||
|
|
||||||
# homeassistant.components.duotecno
|
# homeassistant.components.duotecno
|
||||||
pyDuotecno==2024.3.2
|
pyDuotecno==2024.5.0
|
||||||
|
|
||||||
# homeassistant.components.electrasmart
|
# homeassistant.components.electrasmart
|
||||||
pyElectra==1.2.0
|
pyElectra==1.2.0
|
||||||
@ -2439,7 +2439,7 @@ renault-api==0.2.2
|
|||||||
renson-endura-delta==1.7.1
|
renson-endura-delta==1.7.1
|
||||||
|
|
||||||
# homeassistant.components.reolink
|
# homeassistant.components.reolink
|
||||||
reolink-aio==0.8.9
|
reolink-aio==0.8.10
|
||||||
|
|
||||||
# homeassistant.components.idteck_prox
|
# homeassistant.components.idteck_prox
|
||||||
rfk101py==0.0.1
|
rfk101py==0.0.1
|
||||||
@ -2572,7 +2572,7 @@ smhi-pkg==1.0.16
|
|||||||
snapcast==2.3.6
|
snapcast==2.3.6
|
||||||
|
|
||||||
# homeassistant.components.sonos
|
# homeassistant.components.sonos
|
||||||
soco==0.30.3
|
soco==0.30.4
|
||||||
|
|
||||||
# homeassistant.components.solaredge_local
|
# homeassistant.components.solaredge_local
|
||||||
solaredge-local==0.2.3
|
solaredge-local==0.2.3
|
||||||
@ -2866,7 +2866,7 @@ wiffi==1.1.2
|
|||||||
wirelesstagpy==0.8.1
|
wirelesstagpy==0.8.1
|
||||||
|
|
||||||
# homeassistant.components.wled
|
# homeassistant.components.wled
|
||||||
wled==0.17.0
|
wled==0.17.1
|
||||||
|
|
||||||
# homeassistant.components.wolflink
|
# homeassistant.components.wolflink
|
||||||
wolf-comm==0.0.7
|
wolf-comm==0.0.7
|
||||||
|
@ -1305,7 +1305,7 @@ pyCEC==0.5.2
|
|||||||
pyControl4==1.1.0
|
pyControl4==1.1.0
|
||||||
|
|
||||||
# homeassistant.components.duotecno
|
# homeassistant.components.duotecno
|
||||||
pyDuotecno==2024.3.2
|
pyDuotecno==2024.5.0
|
||||||
|
|
||||||
# homeassistant.components.electrasmart
|
# homeassistant.components.electrasmart
|
||||||
pyElectra==1.2.0
|
pyElectra==1.2.0
|
||||||
@ -1897,7 +1897,7 @@ renault-api==0.2.2
|
|||||||
renson-endura-delta==1.7.1
|
renson-endura-delta==1.7.1
|
||||||
|
|
||||||
# homeassistant.components.reolink
|
# homeassistant.components.reolink
|
||||||
reolink-aio==0.8.9
|
reolink-aio==0.8.10
|
||||||
|
|
||||||
# homeassistant.components.rflink
|
# homeassistant.components.rflink
|
||||||
rflink==0.0.66
|
rflink==0.0.66
|
||||||
@ -1991,7 +1991,7 @@ smhi-pkg==1.0.16
|
|||||||
snapcast==2.3.6
|
snapcast==2.3.6
|
||||||
|
|
||||||
# homeassistant.components.sonos
|
# homeassistant.components.sonos
|
||||||
soco==0.30.3
|
soco==0.30.4
|
||||||
|
|
||||||
# homeassistant.components.solax
|
# homeassistant.components.solax
|
||||||
solax==3.1.0
|
solax==3.1.0
|
||||||
@ -2222,7 +2222,7 @@ whois==0.9.27
|
|||||||
wiffi==1.1.2
|
wiffi==1.1.2
|
||||||
|
|
||||||
# homeassistant.components.wled
|
# homeassistant.components.wled
|
||||||
wled==0.17.0
|
wled==0.17.1
|
||||||
|
|
||||||
# homeassistant.components.wolflink
|
# homeassistant.components.wolflink
|
||||||
wolf-comm==0.0.7
|
wolf-comm==0.0.7
|
||||||
|
@ -6,12 +6,12 @@ from unittest.mock import AsyncMock
|
|||||||
import pytest
|
import pytest
|
||||||
from python_homeassistant_analytics import HomeassistantAnalyticsConnectionError
|
from python_homeassistant_analytics import HomeassistantAnalyticsConnectionError
|
||||||
|
|
||||||
from homeassistant import config_entries
|
|
||||||
from homeassistant.components.analytics_insights.const import (
|
from homeassistant.components.analytics_insights.const import (
|
||||||
CONF_TRACKED_CUSTOM_INTEGRATIONS,
|
CONF_TRACKED_CUSTOM_INTEGRATIONS,
|
||||||
CONF_TRACKED_INTEGRATIONS,
|
CONF_TRACKED_INTEGRATIONS,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
)
|
)
|
||||||
|
from homeassistant.config_entries import SOURCE_USER
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.data_entry_flow import FlowResultType
|
from homeassistant.data_entry_flow import FlowResultType
|
||||||
|
|
||||||
@ -61,7 +61,7 @@ async def test_form(
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Test we get the form."""
|
"""Test we get the form."""
|
||||||
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": SOURCE_USER}
|
||||||
)
|
)
|
||||||
assert result["type"] is FlowResultType.FORM
|
assert result["type"] is FlowResultType.FORM
|
||||||
|
|
||||||
@ -96,7 +96,7 @@ async def test_submitting_empty_form(
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Test we can't submit an empty form."""
|
"""Test we can't submit an empty form."""
|
||||||
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": SOURCE_USER}
|
||||||
)
|
)
|
||||||
assert result["type"] is FlowResultType.FORM
|
assert result["type"] is FlowResultType.FORM
|
||||||
|
|
||||||
@ -128,20 +128,28 @@ async def test_submitting_empty_form(
|
|||||||
assert len(mock_setup_entry.mock_calls) == 1
|
assert len(mock_setup_entry.mock_calls) == 1
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("exception", "reason"),
|
||||||
|
[
|
||||||
|
(HomeassistantAnalyticsConnectionError, "cannot_connect"),
|
||||||
|
(Exception, "unknown"),
|
||||||
|
],
|
||||||
|
)
|
||||||
async def test_form_cannot_connect(
|
async def test_form_cannot_connect(
|
||||||
hass: HomeAssistant, mock_analytics_client: AsyncMock
|
hass: HomeAssistant,
|
||||||
|
mock_analytics_client: AsyncMock,
|
||||||
|
exception: Exception,
|
||||||
|
reason: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test we handle cannot connect error."""
|
"""Test we handle cannot connect error."""
|
||||||
|
|
||||||
mock_analytics_client.get_integrations.side_effect = (
|
mock_analytics_client.get_integrations.side_effect = exception
|
||||||
HomeassistantAnalyticsConnectionError
|
|
||||||
)
|
|
||||||
|
|
||||||
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": SOURCE_USER}
|
||||||
)
|
)
|
||||||
assert result["type"] is FlowResultType.ABORT
|
assert result["type"] is FlowResultType.ABORT
|
||||||
assert result["reason"] == "cannot_connect"
|
assert result["reason"] == reason
|
||||||
|
|
||||||
|
|
||||||
async def test_form_already_configured(
|
async def test_form_already_configured(
|
||||||
@ -159,7 +167,7 @@ async def test_form_already_configured(
|
|||||||
entry.add_to_hass(hass)
|
entry.add_to_hass(hass)
|
||||||
|
|
||||||
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": SOURCE_USER}
|
||||||
)
|
)
|
||||||
assert result["type"] is FlowResultType.ABORT
|
assert result["type"] is FlowResultType.ABORT
|
||||||
assert result["reason"] == "single_instance_allowed"
|
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")
|
group_state = hass.states.get("light.opbergruimte")
|
||||||
assert group_state.state == STATE_ON
|
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)
|
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"
|
key = "test_key"
|
||||||
value = "true"
|
value = "true"
|
||||||
await hass.services.async_call(
|
await hass.services.async_call(
|
||||||
|
@ -95,29 +95,59 @@ async def test_default_prompt(
|
|||||||
suggested_area="Test Area 2",
|
suggested_area="Test Area 2",
|
||||||
)
|
)
|
||||||
with patch("google.generativeai.GenerativeModel") as mock_model:
|
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(
|
result = await conversation.async_converse(
|
||||||
hass, "hello", None, Context(), agent_id=mock_config_entry.entry_id
|
hass, "hello", None, Context(), agent_id=mock_config_entry.entry_id
|
||||||
)
|
)
|
||||||
|
|
||||||
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE
|
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
|
assert [tuple(mock_call) for mock_call in mock_model.mock_calls] == snapshot
|
||||||
|
|
||||||
|
|
||||||
async def test_error_handling(
|
async def test_error_handling(
|
||||||
hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_init_component
|
hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_init_component
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test that the default prompt works."""
|
"""Test that client errors are caught."""
|
||||||
with patch("google.generativeai.GenerativeModel") as mock_model:
|
with patch("google.generativeai.GenerativeModel") as mock_model:
|
||||||
mock_chat = AsyncMock()
|
mock_chat = AsyncMock()
|
||||||
mock_model.return_value.start_chat.return_value = mock_chat
|
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(
|
result = await conversation.async_converse(
|
||||||
hass, "hello", None, Context(), agent_id=mock_config_entry.entry_id
|
hass, "hello", None, Context(), agent_id=mock_config_entry.entry_id
|
||||||
)
|
)
|
||||||
|
|
||||||
assert result.response.response_type == intent.IntentResponseType.ERROR, result
|
assert result.response.response_type == intent.IntentResponseType.ERROR, result
|
||||||
assert result.response.error_code == "unknown", 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(
|
async def test_template_error(
|
||||||
|
@ -22,7 +22,7 @@ from tests.common import MockConfigEntry, load_fixture, load_json_value_fixture
|
|||||||
|
|
||||||
|
|
||||||
@pytest.fixture(name="jwt")
|
@pytest.fixture(name="jwt")
|
||||||
def load_jwt_fixture():
|
def load_jwt_fixture() -> str:
|
||||||
"""Load Fixture data."""
|
"""Load Fixture data."""
|
||||||
return load_fixture("jwt", DOMAIN)
|
return load_fixture("jwt", DOMAIN)
|
||||||
|
|
||||||
@ -33,8 +33,14 @@ def mock_expires_at() -> float:
|
|||||||
return time.time() + 3600
|
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
|
@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 the default mocked config entry."""
|
||||||
return MockConfigEntry(
|
return MockConfigEntry(
|
||||||
version=1,
|
version=1,
|
||||||
@ -44,7 +50,7 @@ def mock_config_entry(jwt, expires_at: int) -> MockConfigEntry:
|
|||||||
"auth_implementation": DOMAIN,
|
"auth_implementation": DOMAIN,
|
||||||
"token": {
|
"token": {
|
||||||
"access_token": jwt,
|
"access_token": jwt,
|
||||||
"scope": "iam:read amc:api",
|
"scope": scope,
|
||||||
"expires_in": 86399,
|
"expires_in": 86399,
|
||||||
"refresh_token": "3012bc9f-7a65-4240-b817-9154ffdcc30f",
|
"refresh_token": "3012bc9f-7a65-4240-b817-9154ffdcc30f",
|
||||||
"provider": "husqvarna",
|
"provider": "husqvarna",
|
||||||
|
@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
from unittest.mock import AsyncMock, patch
|
from unittest.mock import AsyncMock, patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
from homeassistant import config_entries
|
from homeassistant import config_entries
|
||||||
from homeassistant.components.husqvarna_automower.const import (
|
from homeassistant.components.husqvarna_automower.const import (
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
@ -21,12 +23,21 @@ from tests.test_util.aiohttp import AiohttpClientMocker
|
|||||||
from tests.typing import ClientSessionGenerator
|
from tests.typing import ClientSessionGenerator
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("new_scope", "amount"),
|
||||||
|
[
|
||||||
|
("iam:read amc:api", 1),
|
||||||
|
("iam:read", 0),
|
||||||
|
],
|
||||||
|
)
|
||||||
async def test_full_flow(
|
async def test_full_flow(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
hass_client_no_auth,
|
hass_client_no_auth,
|
||||||
aioclient_mock: AiohttpClientMocker,
|
aioclient_mock: AiohttpClientMocker,
|
||||||
current_request_with_host,
|
current_request_with_host,
|
||||||
jwt,
|
jwt: str,
|
||||||
|
new_scope: str,
|
||||||
|
amount: int,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Check full flow."""
|
"""Check full flow."""
|
||||||
result = await hass.config_entries.flow.async_init(
|
result = await hass.config_entries.flow.async_init(
|
||||||
@ -56,7 +67,7 @@ async def test_full_flow(
|
|||||||
OAUTH2_TOKEN,
|
OAUTH2_TOKEN,
|
||||||
json={
|
json={
|
||||||
"access_token": jwt,
|
"access_token": jwt,
|
||||||
"scope": "iam:read amc:api",
|
"scope": new_scope,
|
||||||
"expires_in": 86399,
|
"expires_in": 86399,
|
||||||
"refresh_token": "mock-refresh-token",
|
"refresh_token": "mock-refresh-token",
|
||||||
"provider": "husqvarna",
|
"provider": "husqvarna",
|
||||||
@ -72,8 +83,8 @@ async def test_full_flow(
|
|||||||
) as mock_setup:
|
) as mock_setup:
|
||||||
await hass.config_entries.flow.async_configure(result["flow_id"])
|
await hass.config_entries.flow.async_configure(result["flow_id"])
|
||||||
|
|
||||||
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
|
assert len(hass.config_entries.async_entries(DOMAIN)) == amount
|
||||||
assert len(mock_setup.mock_calls) == 1
|
assert len(mock_setup.mock_calls) == amount
|
||||||
|
|
||||||
|
|
||||||
async def test_config_non_unique_profile(
|
async def test_config_non_unique_profile(
|
||||||
@ -129,6 +140,14 @@ async def test_config_non_unique_profile(
|
|||||||
assert result["reason"] == "already_configured"
|
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(
|
async def test_reauth(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
hass_client_no_auth: ClientSessionGenerator,
|
hass_client_no_auth: ClientSessionGenerator,
|
||||||
@ -136,7 +155,10 @@ async def test_reauth(
|
|||||||
mock_config_entry: MockConfigEntry,
|
mock_config_entry: MockConfigEntry,
|
||||||
current_request_with_host: None,
|
current_request_with_host: None,
|
||||||
mock_automower_client: AsyncMock,
|
mock_automower_client: AsyncMock,
|
||||||
jwt,
|
jwt: str,
|
||||||
|
step_id: str,
|
||||||
|
new_scope: str,
|
||||||
|
reason: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test the reauthentication case updates the existing config entry."""
|
"""Test the reauthentication case updates the existing config entry."""
|
||||||
|
|
||||||
@ -148,7 +170,7 @@ async def test_reauth(
|
|||||||
flows = hass.config_entries.flow.async_progress()
|
flows = hass.config_entries.flow.async_progress()
|
||||||
assert len(flows) == 1
|
assert len(flows) == 1
|
||||||
result = flows[0]
|
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"], {})
|
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
|
||||||
state = config_entry_oauth2_flow._encode_jwt(
|
state = config_entry_oauth2_flow._encode_jwt(
|
||||||
@ -172,7 +194,7 @@ async def test_reauth(
|
|||||||
OAUTH2_TOKEN,
|
OAUTH2_TOKEN,
|
||||||
json={
|
json={
|
||||||
"access_token": "mock-updated-token",
|
"access_token": "mock-updated-token",
|
||||||
"scope": "iam:read amc:api",
|
"scope": new_scope,
|
||||||
"expires_in": 86399,
|
"expires_in": 86399,
|
||||||
"refresh_token": "mock-refresh-token",
|
"refresh_token": "mock-refresh-token",
|
||||||
"provider": "husqvarna",
|
"provider": "husqvarna",
|
||||||
@ -191,7 +213,7 @@ async def test_reauth(
|
|||||||
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
|
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
|
||||||
|
|
||||||
assert result.get("type") is FlowResultType.ABORT
|
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 mock_config_entry.unique_id == USER_ID
|
||||||
assert "token" in mock_config_entry.data
|
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"
|
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(
|
async def test_reauth_wrong_account(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
hass_client_no_auth: ClientSessionGenerator,
|
hass_client_no_auth: ClientSessionGenerator,
|
||||||
@ -208,6 +236,9 @@ async def test_reauth_wrong_account(
|
|||||||
current_request_with_host: None,
|
current_request_with_host: None,
|
||||||
mock_automower_client: AsyncMock,
|
mock_automower_client: AsyncMock,
|
||||||
jwt,
|
jwt,
|
||||||
|
user_id: str,
|
||||||
|
reason: str,
|
||||||
|
scope: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test the reauthentication aborts, if user tries to reauthenticate with another account."""
|
"""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,
|
"expires_in": 86399,
|
||||||
"refresh_token": "mock-refresh-token",
|
"refresh_token": "mock-refresh-token",
|
||||||
"provider": "husqvarna",
|
"provider": "husqvarna",
|
||||||
"user_id": "wrong-user-id",
|
"user_id": user_id,
|
||||||
"token_type": "Bearer",
|
"token_type": "Bearer",
|
||||||
"expires_at": 1697753347,
|
"expires_at": 1697753347,
|
||||||
},
|
},
|
||||||
@ -262,7 +293,7 @@ async def test_reauth_wrong_account(
|
|||||||
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
|
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
|
||||||
|
|
||||||
assert result.get("type") is FlowResultType.ABORT
|
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 mock_config_entry.unique_id == USER_ID
|
||||||
assert "token" in mock_config_entry.data
|
assert "token" in mock_config_entry.data
|
||||||
|
@ -5,7 +5,11 @@ import http
|
|||||||
import time
|
import time
|
||||||
from unittest.mock import AsyncMock
|
from unittest.mock import AsyncMock
|
||||||
|
|
||||||
from aioautomower.exceptions import ApiException, HusqvarnaWSServerHandshakeError
|
from aioautomower.exceptions import (
|
||||||
|
ApiException,
|
||||||
|
AuthException,
|
||||||
|
HusqvarnaWSServerHandshakeError,
|
||||||
|
)
|
||||||
from freezegun.api import FrozenDateTimeFactory
|
from freezegun.api import FrozenDateTimeFactory
|
||||||
import pytest
|
import pytest
|
||||||
from syrupy.assertion import SnapshotAssertion
|
from syrupy.assertion import SnapshotAssertion
|
||||||
@ -39,6 +43,26 @@ async def test_load_unload_entry(
|
|||||||
assert entry.state is ConfigEntryState.NOT_LOADED
|
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(
|
@pytest.mark.parametrize(
|
||||||
("expires_at", "status", "expected_state"),
|
("expires_at", "status", "expected_state"),
|
||||||
[
|
[
|
||||||
@ -75,19 +99,25 @@ async def test_expired_token_refresh_failure(
|
|||||||
assert mock_config_entry.state is expected_state
|
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(
|
async def test_update_failed(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
mock_automower_client: AsyncMock,
|
mock_automower_client: AsyncMock,
|
||||||
mock_config_entry: MockConfigEntry,
|
mock_config_entry: MockConfigEntry,
|
||||||
|
exception: Exception,
|
||||||
|
entry_state: ConfigEntryState,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test load and unload entry."""
|
"""Test update failed."""
|
||||||
getattr(mock_automower_client, "get_status").side_effect = ApiException(
|
mock_automower_client.get_status.side_effect = exception("Test error")
|
||||||
"Test error"
|
|
||||||
)
|
|
||||||
await setup_integration(hass, mock_config_entry)
|
await setup_integration(hass, mock_config_entry)
|
||||||
entry = hass.config_entries.async_entries(DOMAIN)[0]
|
entry = hass.config_entries.async_entries(DOMAIN)[0]
|
||||||
|
assert entry.state is entry_state
|
||||||
assert entry.state is ConfigEntryState.SETUP_RETRY
|
|
||||||
|
|
||||||
|
|
||||||
async def test_websocket_not_available(
|
async def test_websocket_not_available(
|
||||||
|
@ -4382,6 +4382,34 @@ async def test_server_sock_connect_and_disconnect(
|
|||||||
assert len(calls) == 0
|
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.INITIAL_SUBSCRIBE_COOLDOWN", 0.0)
|
||||||
@patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.0)
|
@patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.0)
|
||||||
@patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0)
|
@patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0)
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
"""Test pi_hole component."""
|
"""Test pi_hole component."""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from unittest.mock import AsyncMock
|
from unittest.mock import ANY, AsyncMock
|
||||||
|
|
||||||
from hole.exceptions import HoleError
|
from hole.exceptions import HoleError
|
||||||
import pytest
|
import pytest
|
||||||
@ -12,12 +12,20 @@ from homeassistant.components.pi_hole.const import (
|
|||||||
SERVICE_DISABLE,
|
SERVICE_DISABLE,
|
||||||
SERVICE_DISABLE_ATTR_DURATION,
|
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 homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
from . import (
|
from . import (
|
||||||
|
API_KEY,
|
||||||
CONFIG_DATA,
|
CONFIG_DATA,
|
||||||
CONFIG_DATA_DEFAULTS,
|
CONFIG_DATA_DEFAULTS,
|
||||||
|
CONFIG_ENTRY_WITHOUT_API_KEY,
|
||||||
SWITCH_ENTITY_ID,
|
SWITCH_ENTITY_ID,
|
||||||
_create_mocked_hole,
|
_create_mocked_hole,
|
||||||
_patch_init_hole,
|
_patch_init_hole,
|
||||||
@ -26,6 +34,29 @@ from . import (
|
|||||||
from tests.common import MockConfigEntry
|
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:
|
async def test_setup_with_defaults(hass: HomeAssistant) -> None:
|
||||||
"""Tests component setup with default config."""
|
"""Tests component setup with default config."""
|
||||||
mocked_hole = _create_mocked_hole()
|
mocked_hole = _create_mocked_hole()
|
||||||
|
@ -5,7 +5,7 @@ from typing import Any
|
|||||||
from unittest.mock import AsyncMock, MagicMock, Mock, patch
|
from unittest.mock import AsyncMock, MagicMock, Mock, patch
|
||||||
|
|
||||||
import pytest
|
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.components.reolink import FIRMWARE_UPDATE_INTERVAL, const
|
||||||
from homeassistant.config import async_process_ha_core_config
|
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")),
|
AsyncMock(side_effect=ReolinkError("Test error")),
|
||||||
ConfigEntryState.SETUP_RETRY,
|
ConfigEntryState.SETUP_RETRY,
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
"get_states",
|
||||||
|
AsyncMock(side_effect=CredentialsInvalidError("Test error")),
|
||||||
|
ConfigEntryState.SETUP_ERROR,
|
||||||
|
),
|
||||||
(
|
(
|
||||||
"supported",
|
"supported",
|
||||||
Mock(return_value=False),
|
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"))
|
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
|
# 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))
|
node = Node(client, copy.deepcopy(central_scene_node_state))
|
||||||
client.driver.controller.nodes[node.node_id] = node
|
client.driver.controller.nodes[node.node_id] = node
|
||||||
return 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",
|
"propertyKey": "Switch",
|
||||||
}
|
}
|
||||||
assert args["value"] is False
|
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