Skip to content

Commit

Permalink
2024.5.4 (#117631)
Browse files Browse the repository at this point in the history
  • Loading branch information
frenck committed May 17, 2024
2 parents 9b65005 + 5c8f7fe commit 3dc3de9
Show file tree
Hide file tree
Showing 49 changed files with 11,064 additions and 126 deletions.
3 changes: 3 additions & 0 deletions homeassistant/components/analytics_insights/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,9 @@ async def async_step_user(
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(
Expand Down
3 changes: 2 additions & 1 deletion homeassistant/components/analytics_insights/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
1 change: 1 addition & 0 deletions homeassistant/components/aurora/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
34 changes: 26 additions & 8 deletions homeassistant/components/deconz/light.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -148,11 +165,12 @@ def async_add_group(_: EventType, group_id: str) -> None:
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)])

Expand Down Expand Up @@ -326,7 +344,7 @@ def async_update_callback(self) -> None:
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]):
Expand Down
5 changes: 5 additions & 0 deletions homeassistant/components/duotecno/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ async def _on_update(self) -> None:
"""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")
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/duotecno/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
}
23 changes: 13 additions & 10 deletions homeassistant/components/fully_kiosk/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,18 +69,21 @@ async def async_start_app(call: ServiceCall) -> 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 = [
Expand Down Expand Up @@ -111,7 +114,7 @@ async def async_set_config(call: ServiceCall) -> 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),
}
)
),
Expand Down
4 changes: 3 additions & 1 deletion homeassistant/components/google_assistant_sdk/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,9 @@ async def async_process(
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)
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/google_assistant_sdk/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -182,11 +182,11 @@ async def async_process(
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}",
Expand All @@ -210,7 +210,6 @@ async def async_process(
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}",
Expand All @@ -220,9 +219,15 @@ async def async_process(
)

_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
Expand Down
5 changes: 5 additions & 0 deletions homeassistant/components/husqvarna_automower/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
27 changes: 27 additions & 0 deletions homeassistant/components/husqvarna_automower/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@
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(
Expand All @@ -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)
Expand All @@ -56,6 +64,9 @@ async def async_step_reauth(
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(
Expand All @@ -65,3 +76,19 @@ async def async_step_reauth_confirm(
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()
9 changes: 8 additions & 1 deletion homeassistant/components/husqvarna_automower/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,17 @@
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
Expand Down Expand Up @@ -46,6 +51,8 @@ async def _async_update_data(self) -> dict[str, MowerAttributes]:
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:
Expand Down
7 changes: 6 additions & 1 deletion homeassistant/components/husqvarna_automower/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -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%]"
}
Expand All @@ -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%]"
Expand Down
2 changes: 2 additions & 0 deletions homeassistant/components/insteon/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
CONF_CAT,
CONF_DIM_STEPS,
CONF_HOUSECODE,
CONF_HUB_VERSION,
CONF_SUBCAT,
CONF_UNITCODE,
HOUSECODES,
Expand Down Expand Up @@ -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
Expand Down
8 changes: 7 additions & 1 deletion homeassistant/components/kodi/media_player.py
Original file line number Diff line number Diff line change
Expand Up @@ -480,7 +480,13 @@ async def async_update(self) -> None:
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()
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/lutron_caseta/cover.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ async def async_open_cover_tilt(self, **kwargs: Any) -> None:

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 = {
Expand Down

0 comments on commit 3dc3de9

Please sign in to comment.