diff --git a/Dockerfile b/Dockerfile
index 03bd9131ea0..b80e86fb33c 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -11,22 +11,45 @@ WORKDIR /usr/src
COPY requirements.txt homeassistant/
COPY homeassistant/package_constraints.txt homeassistant/homeassistant/
RUN \
- pip3 install --no-cache-dir --no-index --only-binary=:all: --find-links "${WHEELS_LINKS}" \
- -r homeassistant/requirements.txt --use-deprecated=legacy-resolver
+ pip3 install \
+ --no-cache-dir \
+ --no-index \
+ --only-binary=:all: \
+ --find-links "${WHEELS_LINKS}" \
+ --use-deprecated=legacy-resolver \
+ -r homeassistant/requirements.txt
+
COPY requirements_all.txt home_assistant_frontend-* homeassistant/
RUN \
if ls homeassistant/home_assistant_frontend*.whl 1> /dev/null 2>&1; then \
- pip3 install --no-cache-dir --no-index homeassistant/home_assistant_frontend-*.whl; \
+ pip3 install \
+ --no-cache-dir \
+ --no-index \
+ homeassistant/home_assistant_frontend-*.whl; \
fi \
- && pip3 install --no-cache-dir --no-index --only-binary=:all: --find-links "${WHEELS_LINKS}" \
- -r homeassistant/requirements_all.txt --use-deprecated=legacy-resolver
+ && \
+ LD_PRELOAD="/usr/local/lib/libjemalloc.so.2" \
+ MALLOC_CONF="background_thread:true,metadata_thp:auto,dirty_decay_ms:20000,muzzy_decay_ms:20000" \
+ pip3 install \
+ --no-cache-dir \
+ --no-index \
+ --only-binary=:all: \
+ --find-links "${WHEELS_LINKS}" \
+ --use-deprecated=legacy-resolver \
+ -r homeassistant/requirements_all.txt
## Setup Home Assistant Core
COPY . homeassistant/
RUN \
- pip3 install --no-cache-dir --no-index --only-binary=:all: --find-links "${WHEELS_LINKS}" \
- -e ./homeassistant --use-deprecated=legacy-resolver \
- && python3 -m compileall homeassistant/homeassistant
+ pip3 install \
+ --no-cache-dir \
+ --no-index \
+ --only-binary=:all: \
+ --find-links "${WHEELS_LINKS}" \
+ --use-deprecated=legacy-resolver \
+ -e ./homeassistant \
+ && python3 -m compileall \
+ homeassistant/homeassistant
# Home Assistant S6-Overlay
COPY rootfs /
diff --git a/homeassistant/components/google_sheets/application_credentials.py b/homeassistant/components/google_sheets/application_credentials.py
index 415ab5947bf..f10f6891125 100644
--- a/homeassistant/components/google_sheets/application_credentials.py
+++ b/homeassistant/components/google_sheets/application_credentials.py
@@ -1,6 +1,4 @@
"""application_credentials platform for Google Sheets."""
-import oauth2client
-
from homeassistant.components.application_credentials import AuthorizationServer
from homeassistant.core import HomeAssistant
@@ -8,17 +6,15 @@ from homeassistant.core import HomeAssistant
async def async_get_authorization_server(hass: HomeAssistant) -> AuthorizationServer:
"""Return authorization server."""
return AuthorizationServer(
- oauth2client.GOOGLE_AUTH_URI,
- oauth2client.GOOGLE_TOKEN_URI,
+ "https://accounts.google.com/o/oauth2/v2/auth",
+ "https://oauth2.googleapis.com/token",
)
async def async_get_description_placeholders(hass: HomeAssistant) -> dict[str, str]:
"""Return description placeholders for the credentials dialog."""
return {
- "oauth_consent_url": (
- "https://console.cloud.google.com/apis/credentials/consent"
- ),
+ "oauth_consent_url": "https://console.cloud.google.com/apis/credentials/consent",
"more_info_url": "https://www.home-assistant.io/integrations/google_sheets/",
"oauth_creds_url": "https://console.cloud.google.com/apis/credentials",
}
diff --git a/homeassistant/components/huawei_lte/config_flow.py b/homeassistant/components/huawei_lte/config_flow.py
index d1ab0547801..2319cc9a3ed 100644
--- a/homeassistant/components/huawei_lte/config_flow.py
+++ b/homeassistant/components/huawei_lte/config_flow.py
@@ -250,6 +250,24 @@ class ConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
await self.async_set_unique_id(unique_id)
self._abort_if_unique_id_configured(updates={CONF_URL: url})
+ def _is_supported_device() -> bool:
+ """
+ See if we are looking at a possibly supported device.
+
+ Matching solely on SSDP data does not yield reliable enough results.
+ """
+ try:
+ with Connection(url=url, timeout=CONNECTION_TIMEOUT) as conn:
+ basic_info = Client(conn).device.basic_information()
+ except ResponseErrorException: # API compatible error
+ return True
+ except Exception: # API incompatible error # pylint: disable=broad-except
+ return False
+ return isinstance(basic_info, dict) # Crude content check
+
+ if not await self.hass.async_add_executor_job(_is_supported_device):
+ return self.async_abort(reason="unsupported_device")
+
self.context.update(
{
"title_placeholders": {
diff --git a/homeassistant/components/huawei_lte/manifest.json b/homeassistant/components/huawei_lte/manifest.json
index f0997e2e165..1bd81536aa5 100644
--- a/homeassistant/components/huawei_lte/manifest.json
+++ b/homeassistant/components/huawei_lte/manifest.json
@@ -4,7 +4,7 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/huawei_lte",
"requirements": [
- "huawei-lte-api==1.6.7",
+ "huawei-lte-api==1.6.11",
"stringcase==1.2.0",
"url-normalize==1.4.3"
],
diff --git a/homeassistant/components/huawei_lte/strings.json b/homeassistant/components/huawei_lte/strings.json
index dbc30510d13..3875433888d 100644
--- a/homeassistant/components/huawei_lte/strings.json
+++ b/homeassistant/components/huawei_lte/strings.json
@@ -1,7 +1,8 @@
{
"config": {
"abort": {
- "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
+ "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
+ "unsupported_device": "Unsupported device"
},
"error": {
"connection_timeout": "Connection timeout",
diff --git a/homeassistant/components/huawei_lte/translations/en.json b/homeassistant/components/huawei_lte/translations/en.json
index 134a5372f71..42d28a26871 100644
--- a/homeassistant/components/huawei_lte/translations/en.json
+++ b/homeassistant/components/huawei_lte/translations/en.json
@@ -1,8 +1,8 @@
{
"config": {
"abort": {
- "not_huawei_lte": "Not a Huawei LTE device",
- "reauth_successful": "Re-authentication was successful"
+ "reauth_successful": "Re-authentication was successful",
+ "unsupported_device": "Unsupported device"
},
"error": {
"connection_timeout": "Connection timeout",
diff --git a/homeassistant/components/litterrobot/manifest.json b/homeassistant/components/litterrobot/manifest.json
index f81e663f302..a6c392f4f62 100644
--- a/homeassistant/components/litterrobot/manifest.json
+++ b/homeassistant/components/litterrobot/manifest.json
@@ -3,7 +3,7 @@
"name": "Litter-Robot",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/litterrobot",
- "requirements": ["pylitterbot==2022.12.0"],
+ "requirements": ["pylitterbot==2023.1.1"],
"codeowners": ["@natekspencer", "@tkdrob"],
"dhcp": [{ "hostname": "litter-robot4" }],
"iot_class": "cloud_push",
diff --git a/homeassistant/components/litterrobot/update.py b/homeassistant/components/litterrobot/update.py
index d6475ea486b..845b42efaee 100644
--- a/homeassistant/components/litterrobot/update.py
+++ b/homeassistant/components/litterrobot/update.py
@@ -1,8 +1,7 @@
"""Support for Litter-Robot updates."""
from __future__ import annotations
-from collections.abc import Callable
-from datetime import datetime, timedelta
+from datetime import timedelta
from typing import Any
from pylitterbot import LitterRobot4
@@ -17,12 +16,12 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddEntitiesCallback
-from homeassistant.helpers.event import async_call_later
-from homeassistant.helpers.start import async_at_start
from .const import DOMAIN
from .entity import LitterRobotEntity, LitterRobotHub
+SCAN_INTERVAL = timedelta(days=1)
+
FIRMWARE_UPDATE_ENTITY = UpdateEntityDescription(
key="firmware",
name="Firmware",
@@ -43,7 +42,7 @@ async def async_setup_entry(
for robot in robots
if isinstance(robot, LitterRobot4)
]
- async_add_entities(entities)
+ async_add_entities(entities, True)
class RobotUpdateEntity(LitterRobotEntity[LitterRobot4], UpdateEntity):
@@ -53,16 +52,6 @@ class RobotUpdateEntity(LitterRobotEntity[LitterRobot4], UpdateEntity):
UpdateEntityFeature.INSTALL | UpdateEntityFeature.PROGRESS
)
- def __init__(
- self,
- robot: LitterRobot4,
- hub: LitterRobotHub,
- description: UpdateEntityDescription,
- ) -> None:
- """Initialize a Litter-Robot update entity."""
- super().__init__(robot, hub, description)
- self._poll_unsub: Callable[[], None] | None = None
-
@property
def installed_version(self) -> str:
"""Version installed and in use."""
@@ -73,39 +62,27 @@ class RobotUpdateEntity(LitterRobotEntity[LitterRobot4], UpdateEntity):
"""Update installation progress."""
return self.robot.firmware_update_triggered
- async def _async_update(self, _: HomeAssistant | datetime | None = None) -> None:
+ @property
+ def should_poll(self) -> bool:
+ """Set polling to True."""
+ return True
+
+ async def async_update(self) -> None:
"""Update the entity."""
- self._poll_unsub = None
-
- if await self.robot.has_firmware_update():
- latest_version = await self.robot.get_latest_firmware()
- else:
- latest_version = self.installed_version
-
- if self._attr_latest_version != self.installed_version:
+ # If the robot has a firmware update already in progress, checking for the
+ # latest firmware informs that an update has already been triggered, no
+ # firmware information is returned and we won't know the latest version.
+ if not self.robot.firmware_update_triggered:
+ latest_version = await self.robot.get_latest_firmware(True)
+ if not await self.robot.has_firmware_update():
+ latest_version = self.robot.firmware
self._attr_latest_version = latest_version
- self.async_write_ha_state()
-
- self._poll_unsub = async_call_later(
- self.hass, timedelta(days=1), self._async_update
- )
-
- async def async_added_to_hass(self) -> None:
- """Set up a listener for the entity."""
- await super().async_added_to_hass()
- self.async_on_remove(async_at_start(self.hass, self._async_update))
async def async_install(
self, version: str | None, backup: bool, **kwargs: Any
) -> None:
"""Install an update."""
- if await self.robot.has_firmware_update():
+ if await self.robot.has_firmware_update(True):
if not await self.robot.update_firmware():
message = f"Unable to start firmware update on {self.robot.name}"
raise HomeAssistantError(message)
-
- async def async_will_remove_from_hass(self) -> None:
- """Call when entity will be removed."""
- if self._poll_unsub:
- self._poll_unsub()
- self._poll_unsub = None
diff --git a/homeassistant/components/nanoleaf/manifest.json b/homeassistant/components/nanoleaf/manifest.json
index 1ce3210a206..8cc690f4fc0 100644
--- a/homeassistant/components/nanoleaf/manifest.json
+++ b/homeassistant/components/nanoleaf/manifest.json
@@ -3,7 +3,7 @@
"name": "Nanoleaf",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/nanoleaf",
- "requirements": ["aionanoleaf==0.2.0"],
+ "requirements": ["aionanoleaf==0.2.1"],
"zeroconf": ["_nanoleafms._tcp.local.", "_nanoleafapi._tcp.local."],
"homekit": {
"models": ["NL29", "NL42", "NL47", "NL48", "NL52", "NL59"]
diff --git a/homeassistant/components/tibber/manifest.json b/homeassistant/components/tibber/manifest.json
index 2082d6ddf30..115f3ed7d2e 100644
--- a/homeassistant/components/tibber/manifest.json
+++ b/homeassistant/components/tibber/manifest.json
@@ -3,7 +3,7 @@
"domain": "tibber",
"name": "Tibber",
"documentation": "https://www.home-assistant.io/integrations/tibber",
- "requirements": ["pyTibber==0.26.7"],
+ "requirements": ["pyTibber==0.26.8"],
"codeowners": ["@danielhiversen"],
"quality_scale": "silver",
"config_flow": true,
diff --git a/homeassistant/components/webostv/manifest.json b/homeassistant/components/webostv/manifest.json
index 2547663be28..b4e761b067a 100644
--- a/homeassistant/components/webostv/manifest.json
+++ b/homeassistant/components/webostv/manifest.json
@@ -3,7 +3,7 @@
"name": "LG webOS Smart TV",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/webostv",
- "requirements": ["aiowebostv==0.2.1"],
+ "requirements": ["aiowebostv==0.3.0"],
"codeowners": ["@bendavid", "@thecode"],
"ssdp": [{ "st": "urn:lge-com:service:webos-second-screen:1" }],
"quality_scale": "platinum",
diff --git a/homeassistant/const.py b/homeassistant/const.py
index d7bd1fece49..2e8992e14b1 100644
--- a/homeassistant/const.py
+++ b/homeassistant/const.py
@@ -8,7 +8,7 @@ from .backports.enum import StrEnum
APPLICATION_NAME: Final = "HomeAssistant"
MAJOR_VERSION: Final = 2023
MINOR_VERSION: Final = 1
-PATCH_VERSION: Final = "3"
+PATCH_VERSION: Final = "4"
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0)
diff --git a/pyproject.toml b/pyproject.toml
index e61e5b6e824..ec14caa7515 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "homeassistant"
-version = "2023.1.3"
+version = "2023.1.4"
license = {text = "Apache-2.0"}
description = "Open-source home automation platform running on Python 3."
readme = "README.rst"
diff --git a/requirements_all.txt b/requirements_all.txt
index 111afd8cf3c..8e6e063ed8c 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -220,7 +220,7 @@ aiomodernforms==0.1.8
aiomusiccast==0.14.4
# homeassistant.components.nanoleaf
-aionanoleaf==0.2.0
+aionanoleaf==0.2.1
# homeassistant.components.keyboard_remote
aionotify==0.2.0
@@ -297,7 +297,7 @@ aiovlc==0.1.0
aiowatttime==0.1.1
# homeassistant.components.webostv
-aiowebostv==0.2.1
+aiowebostv==0.3.0
# homeassistant.components.yandex_transport
aioymaps==1.2.2
@@ -906,7 +906,7 @@ horimote==0.4.1
httplib2==0.20.4
# homeassistant.components.huawei_lte
-huawei-lte-api==1.6.7
+huawei-lte-api==1.6.11
# homeassistant.components.hydrawise
hydrawiser==0.2
@@ -1439,7 +1439,7 @@ pyRFXtrx==0.30.0
pySwitchmate==0.5.1
# homeassistant.components.tibber
-pyTibber==0.26.7
+pyTibber==0.26.8
# homeassistant.components.dlink
pyW215==0.7.0
@@ -1719,7 +1719,7 @@ pylibrespot-java==0.1.1
pylitejet==0.3.0
# homeassistant.components.litterrobot
-pylitterbot==2022.12.0
+pylitterbot==2023.1.1
# homeassistant.components.lutron_caseta
pylutron-caseta==0.17.1
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index 0845fd0913b..c6e06374bcb 100644
--- a/requirements_test_all.txt
+++ b/requirements_test_all.txt
@@ -198,7 +198,7 @@ aiomodernforms==0.1.8
aiomusiccast==0.14.4
# homeassistant.components.nanoleaf
-aionanoleaf==0.2.0
+aionanoleaf==0.2.1
# homeassistant.components.notion
aionotion==3.0.2
@@ -272,7 +272,7 @@ aiovlc==0.1.0
aiowatttime==0.1.1
# homeassistant.components.webostv
-aiowebostv==0.2.1
+aiowebostv==0.3.0
# homeassistant.components.yandex_transport
aioymaps==1.2.2
@@ -683,7 +683,7 @@ homepluscontrol==0.0.5
httplib2==0.20.4
# homeassistant.components.huawei_lte
-huawei-lte-api==1.6.7
+huawei-lte-api==1.6.11
# homeassistant.components.hyperion
hyperion-py==0.7.5
@@ -1039,7 +1039,7 @@ pyMetno==0.9.0
pyRFXtrx==0.30.0
# homeassistant.components.tibber
-pyTibber==0.26.7
+pyTibber==0.26.8
# homeassistant.components.nextbus
py_nextbusnext==0.1.5
@@ -1220,7 +1220,7 @@ pylibrespot-java==0.1.1
pylitejet==0.3.0
# homeassistant.components.litterrobot
-pylitterbot==2022.12.0
+pylitterbot==2023.1.1
# homeassistant.components.lutron_caseta
pylutron-caseta==0.17.1
diff --git a/tests/components/google_sheets/test_config_flow.py b/tests/components/google_sheets/test_config_flow.py
index e74602dc8a1..7f434e19953 100644
--- a/tests/components/google_sheets/test_config_flow.py
+++ b/tests/components/google_sheets/test_config_flow.py
@@ -4,7 +4,6 @@ from collections.abc import Generator
from unittest.mock import Mock, patch
from gspread import GSpreadException
-import oauth2client
import pytest
from homeassistant import config_entries
@@ -21,6 +20,8 @@ from tests.common import MockConfigEntry
CLIENT_ID = "1234"
CLIENT_SECRET = "5678"
+GOOGLE_AUTH_URI = "https://accounts.google.com/o/oauth2/v2/auth"
+GOOGLE_TOKEN_URI = "https://oauth2.googleapis.com/token"
SHEET_ID = "google-sheet-id"
TITLE = "Google Sheets"
@@ -66,7 +67,7 @@ async def test_full_flow(
)
assert result["url"] == (
- f"{oauth2client.GOOGLE_AUTH_URI}?response_type=code&client_id={CLIENT_ID}"
+ f"{GOOGLE_AUTH_URI}?response_type=code&client_id={CLIENT_ID}"
"&redirect_uri=https://example.com/auth/external/callback"
f"&state={state}&scope=https://www.googleapis.com/auth/drive.file"
"&access_type=offline&prompt=consent"
@@ -83,7 +84,7 @@ async def test_full_flow(
mock_client.return_value.create = mock_create
aioclient_mock.post(
- oauth2client.GOOGLE_TOKEN_URI,
+ GOOGLE_TOKEN_URI,
json={
"refresh_token": "mock-refresh-token",
"access_token": "mock-access-token",
@@ -133,7 +134,7 @@ async def test_create_sheet_error(
)
assert result["url"] == (
- f"{oauth2client.GOOGLE_AUTH_URI}?response_type=code&client_id={CLIENT_ID}"
+ f"{GOOGLE_AUTH_URI}?response_type=code&client_id={CLIENT_ID}"
"&redirect_uri=https://example.com/auth/external/callback"
f"&state={state}&scope=https://www.googleapis.com/auth/drive.file"
"&access_type=offline&prompt=consent"
@@ -150,7 +151,7 @@ async def test_create_sheet_error(
mock_client.return_value.create = mock_create
aioclient_mock.post(
- oauth2client.GOOGLE_TOKEN_URI,
+ GOOGLE_TOKEN_URI,
json={
"refresh_token": "mock-refresh-token",
"access_token": "mock-access-token",
@@ -202,7 +203,7 @@ async def test_reauth(
},
)
assert result["url"] == (
- f"{oauth2client.GOOGLE_AUTH_URI}?response_type=code&client_id={CLIENT_ID}"
+ f"{GOOGLE_AUTH_URI}?response_type=code&client_id={CLIENT_ID}"
"&redirect_uri=https://example.com/auth/external/callback"
f"&state={state}&scope=https://www.googleapis.com/auth/drive.file"
"&access_type=offline&prompt=consent"
@@ -218,7 +219,7 @@ async def test_reauth(
mock_client.return_value.open_by_key = mock_open
aioclient_mock.post(
- oauth2client.GOOGLE_TOKEN_URI,
+ GOOGLE_TOKEN_URI,
json={
"refresh_token": "mock-refresh-token",
"access_token": "updated-access-token",
@@ -283,7 +284,7 @@ async def test_reauth_abort(
},
)
assert result["url"] == (
- f"{oauth2client.GOOGLE_AUTH_URI}?response_type=code&client_id={CLIENT_ID}"
+ f"{GOOGLE_AUTH_URI}?response_type=code&client_id={CLIENT_ID}"
"&redirect_uri=https://example.com/auth/external/callback"
f"&state={state}&scope=https://www.googleapis.com/auth/drive.file"
"&access_type=offline&prompt=consent"
@@ -300,7 +301,7 @@ async def test_reauth_abort(
mock_client.return_value.open_by_key = mock_open
aioclient_mock.post(
- oauth2client.GOOGLE_TOKEN_URI,
+ GOOGLE_TOKEN_URI,
json={
"refresh_token": "mock-refresh-token",
"access_token": "updated-access-token",
@@ -346,7 +347,7 @@ async def test_already_configured(
)
assert result["url"] == (
- f"{oauth2client.GOOGLE_AUTH_URI}?response_type=code&client_id={CLIENT_ID}"
+ f"{GOOGLE_AUTH_URI}?response_type=code&client_id={CLIENT_ID}"
"&redirect_uri=https://example.com/auth/external/callback"
f"&state={state}&scope=https://www.googleapis.com/auth/drive.file"
"&access_type=offline&prompt=consent"
@@ -363,7 +364,7 @@ async def test_already_configured(
mock_client.return_value.create = mock_create
aioclient_mock.post(
- oauth2client.GOOGLE_TOKEN_URI,
+ GOOGLE_TOKEN_URI,
json={
"refresh_token": "mock-refresh-token",
"access_token": "mock-access-token",
diff --git a/tests/components/huawei_lte/test_config_flow.py b/tests/components/huawei_lte/test_config_flow.py
index d29c0554bca..9e723204b33 100644
--- a/tests/components/huawei_lte/test_config_flow.py
+++ b/tests/components/huawei_lte/test_config_flow.py
@@ -211,9 +211,14 @@ async def test_success(hass, login_requests_mock):
@pytest.mark.parametrize(
- ("upnp_data", "expected_result"),
+ ("requests_mock_request_kwargs", "upnp_data", "expected_result"),
(
(
+ {
+ "method": ANY,
+ "url": f"{FIXTURE_USER_INPUT[CONF_URL]}api/device/basic_information",
+ "text": "Mock device",
+ },
{
ssdp.ATTR_UPNP_FRIENDLY_NAME: "Mobile Wi-Fi",
ssdp.ATTR_UPNP_SERIAL: "00000000",
@@ -225,6 +230,11 @@ async def test_success(hass, login_requests_mock):
},
),
(
+ {
+ "method": ANY,
+ "url": f"{FIXTURE_USER_INPUT[CONF_URL]}api/device/basic_information",
+ "text": "100002
",
+ },
{
ssdp.ATTR_UPNP_FRIENDLY_NAME: "Mobile Wi-Fi",
# No ssdp.ATTR_UPNP_SERIAL
@@ -235,19 +245,36 @@ async def test_success(hass, login_requests_mock):
"errors": {},
},
),
+ (
+ {
+ "method": ANY,
+ "url": f"{FIXTURE_USER_INPUT[CONF_URL]}api/device/basic_information",
+ "exc": Exception("Something unexpected"),
+ },
+ {
+ # Does not matter
+ },
+ {
+ "type": data_entry_flow.FlowResultType.ABORT,
+ "reason": "unsupported_device",
+ },
+ ),
),
)
-async def test_ssdp(hass, upnp_data, expected_result):
+async def test_ssdp(
+ hass, login_requests_mock, requests_mock_request_kwargs, upnp_data, expected_result
+):
"""Test SSDP discovery initiates config properly."""
- url = "http://192.168.100.1/"
+ url = FIXTURE_USER_INPUT[CONF_URL][:-1] # strip trailing slash for appending port
context = {"source": config_entries.SOURCE_SSDP}
+ login_requests_mock.request(**requests_mock_request_kwargs)
result = await hass.config_entries.flow.async_init(
DOMAIN,
context=context,
data=ssdp.SsdpServiceInfo(
ssdp_usn="mock_usn",
ssdp_st="upnp:rootdevice",
- ssdp_location="http://192.168.100.1:60957/rootDesc.xml",
+ ssdp_location=f"{url}:60957/rootDesc.xml",
upnp={
ssdp.ATTR_UPNP_DEVICE_TYPE: "urn:schemas-upnp-org:device:InternetGatewayDevice:1",
ssdp.ATTR_UPNP_MANUFACTURER: "Huawei",
@@ -264,7 +291,7 @@ async def test_ssdp(hass, upnp_data, expected_result):
for k, v in expected_result.items():
assert result[k] == v
if result.get("data_schema"):
- result["data_schema"]({})[CONF_URL] == url
+ assert result["data_schema"]({})[CONF_URL] == url + "/"
@pytest.mark.parametrize(
diff --git a/tests/components/litterrobot/test_update.py b/tests/components/litterrobot/test_update.py
index f4311992c8e..4940ec64824 100644
--- a/tests/components/litterrobot/test_update.py
+++ b/tests/components/litterrobot/test_update.py
@@ -11,7 +11,13 @@ from homeassistant.components.update import (
SERVICE_INSTALL,
UpdateDeviceClass,
)
-from homeassistant.const import ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, STATE_OFF, STATE_ON
+from homeassistant.const import (
+ ATTR_DEVICE_CLASS,
+ ATTR_ENTITY_ID,
+ STATE_OFF,
+ STATE_ON,
+ STATE_UNKNOWN,
+)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
@@ -28,6 +34,7 @@ async def test_robot_with_no_update(
"""Tests the update entity was set up."""
robot: LitterRobot4 = mock_account_with_litterrobot_4.robots[0]
robot.has_firmware_update = AsyncMock(return_value=False)
+ robot.get_latest_firmware = AsyncMock(return_value=None)
entry = await setup_integration(
hass, mock_account_with_litterrobot_4, PLATFORM_DOMAIN
@@ -79,3 +86,27 @@ async def test_robot_with_update(
)
await hass.async_block_till_done()
assert robot.update_firmware.call_count == 1
+
+
+async def test_robot_with_update_already_in_progress(
+ hass: HomeAssistant, mock_account_with_litterrobot_4: MagicMock
+):
+ """Tests the update entity was set up."""
+ robot: LitterRobot4 = mock_account_with_litterrobot_4.robots[0]
+ robot._update_data( # pylint:disable=protected-access
+ {"isFirmwareUpdateTriggered": True}, partial=True
+ )
+
+ entry = await setup_integration(
+ hass, mock_account_with_litterrobot_4, PLATFORM_DOMAIN
+ )
+
+ state = hass.states.get(ENTITY_ID)
+ assert state
+ assert state.state == STATE_UNKNOWN
+ assert state.attributes[ATTR_DEVICE_CLASS] == UpdateDeviceClass.FIRMWARE
+ assert state.attributes[ATTR_INSTALLED_VERSION] == OLD_FIRMWARE
+ assert state.attributes[ATTR_LATEST_VERSION] is None
+
+ assert await hass.config_entries.async_unload(entry.entry_id)
+ await hass.async_block_till_done()