Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

2023.4.3 #91316

Merged
merged 22 commits into from
Apr 13, 2023
Merged

2023.4.3 #91316

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
fa29aea
Fix configuring Flo instances (#90990)
amattas Apr 10, 2023
34394d9
Fall back to polling if webhook cannot be registered on Nuki (#91013)
pree Apr 11, 2023
2d41fe8
Track availability of source sensor in utility meter (#91035)
dgomes Apr 10, 2023
3f6486d
Bump aiopyarr to 23.4.0 (#91110)
tkdrob Apr 9, 2023
4cd00da
Bump env_canada to 0.5.32 (#91126)
michaeldavie Apr 9, 2023
dc777f7
Relax calendar event validation to allow existing zero duration event…
allenporter Apr 10, 2023
3efffe7
Bump ulid-transform to 0.6.3 (#91133)
bdraco Apr 10, 2023
2db8d70
Fix false positive in SQL sensor full table scan check (#91134)
bdraco Apr 10, 2023
30d615f
Reolink config flow fix custom port when USE_HTTPS not selected (#91137)
starkillerOG Apr 10, 2023
c601266
Fix all day event coercion logic (#91169)
allenporter Apr 10, 2023
48df638
Reduce startup time for System Bridge integration (#91171)
timmo001 Apr 12, 2023
a87c78c
Cleanup ZHA from Zigpy deprecated property removal (#91180)
dmulcahey Apr 10, 2023
a806e07
Bump `pytile` to 2023.04.0 (#91191)
bachya Apr 11, 2023
68920a1
Flush conversation name cache when an entity is renamed (#91214)
emontnemery Apr 11, 2023
8eb75be
Update frontend to 20230411.0 (#91219)
bramkragten Apr 11, 2023
d6574b4
Fix switch_as_x name (#91232)
emontnemery Apr 11, 2023
fd53eda
Update Inovelli Blue Series switch support in ZHA (#91254)
codyhackw Apr 12, 2023
64a9bfc
Bump ZHA dependencies (#91291)
puddly Apr 12, 2023
a420007
Restore use of local timezone for MariaDB/MySQL in SQL integration (#…
bdraco Apr 13, 2023
e9f1148
Bumped version to 2023.4.3
balloob Apr 13, 2023
c073cee
Google Assistant SDK: Fix broadcast command for Portuguese (#91293)
tronikos Apr 13, 2023
bbf2d0e
Remove codecov from Python test requirements (#91295)
frenck Apr 12, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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