Skip to content

Commit

Permalink
2023.4.3 (#91316)
Browse files Browse the repository at this point in the history
  • Loading branch information
balloob committed Apr 13, 2023
2 parents 7928b31 + bbf2d0e commit 8ec6afb
Show file tree
Hide file tree
Showing 51 changed files with 1,030 additions and 187 deletions.
38 changes: 28 additions & 10 deletions homeassistant/components/calendar/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,13 @@
# Don't support rrules more often than daily
VALID_FREQS = {"DAILY", "WEEKLY", "MONTHLY", "YEARLY"}

# Ensure events created in Home Assistant have a positive duration
MIN_NEW_EVENT_DURATION = datetime.timedelta(seconds=1)

# Events must have a non-negative duration e.g. Google Calendar can create zero
# duration events in the UI.
MIN_EVENT_DURATION = datetime.timedelta(seconds=0)


def _has_timezone(*keys: Any) -> Callable[[dict[str, Any]], dict[str, Any]]:
"""Assert that all datetime values have a timezone."""
Expand Down Expand Up @@ -116,17 +123,18 @@ def validate(obj: dict[str, Any]) -> dict[str, Any]:
return validate


def _has_duration(
start_key: str, end_key: str
def _has_min_duration(
start_key: str, end_key: str, min_duration: datetime.timedelta
) -> Callable[[dict[str, Any]], dict[str, Any]]:
"""Verify that the time span between start and end is positive."""
"""Verify that the time span between start and end has a minimum duration."""

def validate(obj: dict[str, Any]) -> dict[str, Any]:
"""Test that all keys in the dict are in order."""
if (start := obj.get(start_key)) and (end := obj.get(end_key)):
duration = end - start
if duration.total_seconds() <= 0:
raise vol.Invalid(f"Expected positive event duration ({start}, {end})")
if duration < min_duration:
raise vol.Invalid(
f"Expected minimum event duration of {min_duration} ({start}, {end})"
)
return obj

return validate
Expand Down Expand Up @@ -204,8 +212,8 @@ def _validate_rrule(value: Any) -> str:
),
_has_consistent_timezone(EVENT_START_DATETIME, EVENT_END_DATETIME),
_as_local_timezone(EVENT_START_DATETIME, EVENT_END_DATETIME),
_has_duration(EVENT_START_DATE, EVENT_END_DATE),
_has_duration(EVENT_START_DATETIME, EVENT_END_DATETIME),
_has_min_duration(EVENT_START_DATE, EVENT_END_DATE, MIN_NEW_EVENT_DURATION),
_has_min_duration(EVENT_START_DATETIME, EVENT_END_DATETIME, MIN_NEW_EVENT_DURATION),
)

WEBSOCKET_EVENT_SCHEMA = vol.Schema(
Expand All @@ -221,7 +229,7 @@ def _validate_rrule(value: Any) -> str:
_has_same_type(EVENT_START, EVENT_END),
_has_consistent_timezone(EVENT_START, EVENT_END),
_as_local_timezone(EVENT_START, EVENT_END),
_has_duration(EVENT_START, EVENT_END),
_has_min_duration(EVENT_START, EVENT_END, MIN_NEW_EVENT_DURATION),
)
)

Expand All @@ -238,7 +246,7 @@ def _validate_rrule(value: Any) -> str:
_has_timezone("start", "end"),
_has_consistent_timezone("start", "end"),
_as_local_timezone("start", "end"),
_has_duration("start", "end"),
_has_min_duration("start", "end", MIN_EVENT_DURATION),
),
extra=vol.ALLOW_EXTRA,
)
Expand Down Expand Up @@ -346,6 +354,16 @@ def skip_none(obj: Iterable[tuple[str, Any]]) -> dict[str, str]:
f"Failed to validate CalendarEvent: {err}"
) from err

# It is common to set a start an end date to be the same thing for
# an all day event, but that is not a valid duration. Fix to have a
# duration of one day.
if (
not isinstance(self.start, datetime.datetime)
and not isinstance(self.end, datetime.datetime)
and self.start == self.end
):
self.end = self.start + datetime.timedelta(days=1)


def _event_dict_factory(obj: Iterable[tuple[str, Any]]) -> dict[str, str]:
"""Convert CalendarEvent dataclass items to dictionary of attributes."""
Expand Down
7 changes: 5 additions & 2 deletions homeassistant/components/conversation/default_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@

_LOGGER = logging.getLogger(__name__)
_DEFAULT_ERROR_TEXT = "Sorry, I couldn't understand that"
_ENTITY_REGISTRY_UPDATE_FIELDS = ["aliases", "name", "original_name"]

REGEX_TYPE = type(re.compile(""))

Expand Down Expand Up @@ -450,8 +451,10 @@ def _async_handle_area_registry_changed(self, event: core.Event) -> None:

@core.callback
def _async_handle_entity_registry_changed(self, event: core.Event) -> None:
"""Clear names list cache when an entity changes aliases."""
if event.data["action"] == "update" and "aliases" not in event.data["changes"]:
"""Clear names list cache when an entity registry entry has changed."""
if event.data["action"] == "update" and not any(
field in event.data["changes"] for field in _ENTITY_REGISTRY_UPDATE_FIELDS
):
return
self._slot_lists = None

Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/environment_canada/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/environment_canada",
"iot_class": "cloud_polling",
"loggers": ["env_canada"],
"requirements": ["env_canada==0.5.31"]
"requirements": ["env_canada==0.5.32"]
}
19 changes: 8 additions & 11 deletions homeassistant/components/flo/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@

from .const import DOMAIN, LOGGER

DATA_SCHEMA = vol.Schema({vol.Required("username"): str, vol.Required("password"): str})
DATA_SCHEMA = vol.Schema(
{vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str}
)


async def validate_input(hass: core.HomeAssistant, data):
Expand All @@ -20,18 +22,11 @@ async def validate_input(hass: core.HomeAssistant, data):

session = async_get_clientsession(hass)
try:
api = await async_get_api(
data[CONF_USERNAME], data[CONF_PASSWORD], session=session
)
await async_get_api(data[CONF_USERNAME], data[CONF_PASSWORD], session=session)
except RequestError as request_error:
LOGGER.error("Error connecting to the Flo API: %s", request_error)
raise CannotConnect from request_error

user_info = await api.user.get_info()
a_location_id = user_info["locations"][0]["id"]
location_info = await api.location.get_info(a_location_id)
return {"title": location_info["nickname"]}


class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for flo."""
Expand All @@ -45,8 +40,10 @@ async def async_step_user(self, user_input=None):
await self.async_set_unique_id(user_input[CONF_USERNAME])
self._abort_if_unique_id_configured()
try:
info = await validate_input(self.hass, user_input)
return self.async_create_entry(title=info["title"], data=user_input)
await validate_input(self.hass, user_input)
return self.async_create_entry(
title=user_input[CONF_USERNAME], data=user_input
)
except CannotConnect:
errors["base"] = "cannot_connect"

Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/frontend/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,5 @@
"documentation": "https://www.home-assistant.io/integrations/frontend",
"integration_type": "system",
"quality_scale": "internal",
"requirements": ["home-assistant-frontend==20230406.1"]
"requirements": ["home-assistant-frontend==20230411.0"]
}
2 changes: 1 addition & 1 deletion homeassistant/components/google_assistant_sdk/notify.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
"it": ("Trasmetti {0}", "Trasmetti in {1} {0}"),
"ja": ("{0}とブロードキャストして", "{0}と{1}にブロードキャストして"),
"ko": ("{0} 라고 방송해 줘", "{0} 라고 {1}에 방송해 줘"),
"pt": ("Transmite {0}", "Transmite para {1} {0}"),
"pt": ("Transmitir {0}", "Transmitir {0} para {1}"),
}


Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/lidarr/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@
"integration_type": "service",
"iot_class": "local_polling",
"loggers": ["aiopyarr"],
"requirements": ["aiopyarr==22.11.0"]
"requirements": ["aiopyarr==23.4.0"]
}
2 changes: 1 addition & 1 deletion homeassistant/components/local_calendar/calendar.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ def _get_calendar_event(event: Event) -> CalendarEvent:
else:
start = event.start
end = event.end
if (end - start) <= timedelta(days=0):
if (end - start) < timedelta(days=0):
end = start + timedelta(days=1)

return CalendarEvent(
Expand Down
151 changes: 81 additions & 70 deletions homeassistant/components/nuki/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@
Platform,
)
from homeassistant.core import Event, HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import (
device_registry as dr,
entity_registry as er,
Expand All @@ -47,7 +46,7 @@
DOMAIN,
ERROR_STATES,
)
from .helpers import parse_id
from .helpers import NukiWebhookException, parse_id

_NukiDeviceT = TypeVar("_NukiDeviceT", bound=NukiDevice)

Expand All @@ -61,71 +60,10 @@ def _get_bridge_devices(bridge: NukiBridge) -> tuple[list[NukiLock], list[NukiOp
return bridge.locks, bridge.openers


def _register_webhook(bridge: NukiBridge, entry_id: str, url: str) -> bool:
# Register HA URL as webhook if not already
callbacks = bridge.callback_list()
for item in callbacks["callbacks"]:
if entry_id in item["url"]:
if item["url"] == url:
return True
bridge.callback_remove(item["id"])

if bridge.callback_add(url)["success"]:
return True

return False


def _remove_webhook(bridge: NukiBridge, entry_id: str) -> None:
# Remove webhook if set
callbacks = bridge.callback_list()
for item in callbacks["callbacks"]:
if entry_id in item["url"]:
bridge.callback_remove(item["id"])


async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up the Nuki entry."""

hass.data.setdefault(DOMAIN, {})

# Migration of entry unique_id
if isinstance(entry.unique_id, int):
new_id = parse_id(entry.unique_id)
params = {"unique_id": new_id}
if entry.title == entry.unique_id:
params["title"] = new_id
hass.config_entries.async_update_entry(entry, **params)

try:
bridge = await hass.async_add_executor_job(
NukiBridge,
entry.data[CONF_HOST],
entry.data[CONF_TOKEN],
entry.data[CONF_PORT],
True,
DEFAULT_TIMEOUT,
)

locks, openers = await hass.async_add_executor_job(_get_bridge_devices, bridge)
except InvalidCredentialsException as err:
raise exceptions.ConfigEntryAuthFailed from err
except RequestException as err:
raise exceptions.ConfigEntryNotReady from err

# Device registration for the bridge
info = bridge.info()
bridge_id = parse_id(info["ids"]["hardwareId"])
dev_reg = dr.async_get(hass)
dev_reg.async_get_or_create(
config_entry_id=entry.entry_id,
identifiers={(DOMAIN, bridge_id)},
manufacturer="Nuki Home Solutions GmbH",
name=f"Nuki Bridge {bridge_id}",
model="Hardware Bridge",
sw_version=info["versions"]["firmwareVersion"],
)

async def _create_webhook(
hass: HomeAssistant, entry: ConfigEntry, bridge: NukiBridge
) -> None:
# Create HomeAssistant webhook
async def handle_webhook(
hass: HomeAssistant, webhook_id: str, request: web.Request
) -> web.Response:
Expand Down Expand Up @@ -163,7 +101,7 @@ async def handle_webhook(
)
except NoURLAvailableError:
webhook.async_unregister(hass, entry.entry_id)
raise ConfigEntryNotReady(
raise NukiWebhookException(
f"Error registering URL for webhook {entry.entry_id}: "
"HomeAssistant URL is not available"
) from None
Expand Down Expand Up @@ -193,13 +131,86 @@ async def handle_webhook(
)
except InvalidCredentialsException as err:
webhook.async_unregister(hass, entry.entry_id)
raise ConfigEntryNotReady(f"Invalid credentials for Bridge: {err}") from err
raise NukiWebhookException(
f"Invalid credentials for Bridge: {err}"
) from err
except RequestException as err:
webhook.async_unregister(hass, entry.entry_id)
raise ConfigEntryNotReady(
raise NukiWebhookException(
f"Error communicating with Bridge: {err}"
) from err


def _register_webhook(bridge: NukiBridge, entry_id: str, url: str) -> bool:
# Register HA URL as webhook if not already
callbacks = bridge.callback_list()
for item in callbacks["callbacks"]:
if entry_id in item["url"]:
if item["url"] == url:
return True
bridge.callback_remove(item["id"])

if bridge.callback_add(url)["success"]:
return True

return False


def _remove_webhook(bridge: NukiBridge, entry_id: str) -> None:
# Remove webhook if set
callbacks = bridge.callback_list()
for item in callbacks["callbacks"]:
if entry_id in item["url"]:
bridge.callback_remove(item["id"])


async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up the Nuki entry."""

hass.data.setdefault(DOMAIN, {})

# Migration of entry unique_id
if isinstance(entry.unique_id, int):
new_id = parse_id(entry.unique_id)
params = {"unique_id": new_id}
if entry.title == entry.unique_id:
params["title"] = new_id
hass.config_entries.async_update_entry(entry, **params)

try:
bridge = await hass.async_add_executor_job(
NukiBridge,
entry.data[CONF_HOST],
entry.data[CONF_TOKEN],
entry.data[CONF_PORT],
True,
DEFAULT_TIMEOUT,
)

locks, openers = await hass.async_add_executor_job(_get_bridge_devices, bridge)
except InvalidCredentialsException as err:
raise exceptions.ConfigEntryAuthFailed from err
except RequestException as err:
raise exceptions.ConfigEntryNotReady from err

# Device registration for the bridge
info = bridge.info()
bridge_id = parse_id(info["ids"]["hardwareId"])
dev_reg = dr.async_get(hass)
dev_reg.async_get_or_create(
config_entry_id=entry.entry_id,
identifiers={(DOMAIN, bridge_id)},
manufacturer="Nuki Home Solutions GmbH",
name=f"Nuki Bridge {bridge_id}",
model="Hardware Bridge",
sw_version=info["versions"]["firmwareVersion"],
)

try:
await _create_webhook(hass, entry, bridge)
except NukiWebhookException as err:
_LOGGER.warning("Error registering HomeAssistant webhook: %s", err)

async def _stop_nuki(_: Event):
"""Stop and remove the Nuki webhook."""
webhook.async_unregister(hass, entry.entry_id)
Expand Down
4 changes: 4 additions & 0 deletions homeassistant/components/nuki/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,7 @@ class CannotConnect(exceptions.HomeAssistantError):

class InvalidAuth(exceptions.HomeAssistantError):
"""Error to indicate there is invalid auth."""


class NukiWebhookException(exceptions.HomeAssistantError):
"""Error to indicate there was an issue with the webhook."""
2 changes: 1 addition & 1 deletion homeassistant/components/radarr/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@
"integration_type": "service",
"iot_class": "local_polling",
"loggers": ["aiopyarr"],
"requirements": ["aiopyarr==22.11.0"]
"requirements": ["aiopyarr==23.4.0"]
}
Loading

0 comments on commit 8ec6afb

Please sign in to comment.