This commit is contained in:
Paulus Schoutsen 2023-01-12 15:00:21 -05:00 committed by GitHub
commit 3bb9be2382
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 167 additions and 93 deletions

View File

@ -11,22 +11,45 @@ WORKDIR /usr/src
COPY requirements.txt homeassistant/ COPY requirements.txt homeassistant/
COPY homeassistant/package_constraints.txt homeassistant/homeassistant/ COPY homeassistant/package_constraints.txt homeassistant/homeassistant/
RUN \ RUN \
pip3 install --no-cache-dir --no-index --only-binary=:all: --find-links "${WHEELS_LINKS}" \ pip3 install \
-r homeassistant/requirements.txt --use-deprecated=legacy-resolver --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/ COPY requirements_all.txt home_assistant_frontend-* homeassistant/
RUN \ RUN \
if ls homeassistant/home_assistant_frontend*.whl 1> /dev/null 2>&1; then \ 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 \ 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 ## Setup Home Assistant Core
COPY . homeassistant/ COPY . homeassistant/
RUN \ RUN \
pip3 install --no-cache-dir --no-index --only-binary=:all: --find-links "${WHEELS_LINKS}" \ pip3 install \
-e ./homeassistant --use-deprecated=legacy-resolver \ --no-cache-dir \
&& python3 -m compileall homeassistant/homeassistant --no-index \
--only-binary=:all: \
--find-links "${WHEELS_LINKS}" \
--use-deprecated=legacy-resolver \
-e ./homeassistant \
&& python3 -m compileall \
homeassistant/homeassistant
# Home Assistant S6-Overlay # Home Assistant S6-Overlay
COPY rootfs / COPY rootfs /

View File

@ -1,6 +1,4 @@
"""application_credentials platform for Google Sheets.""" """application_credentials platform for Google Sheets."""
import oauth2client
from homeassistant.components.application_credentials import AuthorizationServer from homeassistant.components.application_credentials import AuthorizationServer
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
@ -8,17 +6,15 @@ from homeassistant.core import HomeAssistant
async def async_get_authorization_server(hass: HomeAssistant) -> AuthorizationServer: async def async_get_authorization_server(hass: HomeAssistant) -> AuthorizationServer:
"""Return authorization server.""" """Return authorization server."""
return AuthorizationServer( return AuthorizationServer(
oauth2client.GOOGLE_AUTH_URI, "https://accounts.google.com/o/oauth2/v2/auth",
oauth2client.GOOGLE_TOKEN_URI, "https://oauth2.googleapis.com/token",
) )
async def async_get_description_placeholders(hass: HomeAssistant) -> dict[str, str]: async def async_get_description_placeholders(hass: HomeAssistant) -> dict[str, str]:
"""Return description placeholders for the credentials dialog.""" """Return description placeholders for the credentials dialog."""
return { return {
"oauth_consent_url": ( "oauth_consent_url": "https://console.cloud.google.com/apis/credentials/consent",
"https://console.cloud.google.com/apis/credentials/consent"
),
"more_info_url": "https://www.home-assistant.io/integrations/google_sheets/", "more_info_url": "https://www.home-assistant.io/integrations/google_sheets/",
"oauth_creds_url": "https://console.cloud.google.com/apis/credentials", "oauth_creds_url": "https://console.cloud.google.com/apis/credentials",
} }

View File

@ -250,6 +250,24 @@ class ConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
await self.async_set_unique_id(unique_id) await self.async_set_unique_id(unique_id)
self._abort_if_unique_id_configured(updates={CONF_URL: url}) 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( self.context.update(
{ {
"title_placeholders": { "title_placeholders": {

View File

@ -4,7 +4,7 @@
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/huawei_lte", "documentation": "https://www.home-assistant.io/integrations/huawei_lte",
"requirements": [ "requirements": [
"huawei-lte-api==1.6.7", "huawei-lte-api==1.6.11",
"stringcase==1.2.0", "stringcase==1.2.0",
"url-normalize==1.4.3" "url-normalize==1.4.3"
], ],

View File

@ -1,7 +1,8 @@
{ {
"config": { "config": {
"abort": { "abort": {
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
"unsupported_device": "Unsupported device"
}, },
"error": { "error": {
"connection_timeout": "Connection timeout", "connection_timeout": "Connection timeout",

View File

@ -1,8 +1,8 @@
{ {
"config": { "config": {
"abort": { "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": { "error": {
"connection_timeout": "Connection timeout", "connection_timeout": "Connection timeout",

View File

@ -3,7 +3,7 @@
"name": "Litter-Robot", "name": "Litter-Robot",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/litterrobot", "documentation": "https://www.home-assistant.io/integrations/litterrobot",
"requirements": ["pylitterbot==2022.12.0"], "requirements": ["pylitterbot==2023.1.1"],
"codeowners": ["@natekspencer", "@tkdrob"], "codeowners": ["@natekspencer", "@tkdrob"],
"dhcp": [{ "hostname": "litter-robot4" }], "dhcp": [{ "hostname": "litter-robot4" }],
"iot_class": "cloud_push", "iot_class": "cloud_push",

View File

@ -1,8 +1,7 @@
"""Support for Litter-Robot updates.""" """Support for Litter-Robot updates."""
from __future__ import annotations from __future__ import annotations
from collections.abc import Callable from datetime import timedelta
from datetime import datetime, timedelta
from typing import Any from typing import Any
from pylitterbot import LitterRobot4 from pylitterbot import LitterRobot4
@ -17,12 +16,12 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddEntitiesCallback 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 .const import DOMAIN
from .entity import LitterRobotEntity, LitterRobotHub from .entity import LitterRobotEntity, LitterRobotHub
SCAN_INTERVAL = timedelta(days=1)
FIRMWARE_UPDATE_ENTITY = UpdateEntityDescription( FIRMWARE_UPDATE_ENTITY = UpdateEntityDescription(
key="firmware", key="firmware",
name="Firmware", name="Firmware",
@ -43,7 +42,7 @@ async def async_setup_entry(
for robot in robots for robot in robots
if isinstance(robot, LitterRobot4) if isinstance(robot, LitterRobot4)
] ]
async_add_entities(entities) async_add_entities(entities, True)
class RobotUpdateEntity(LitterRobotEntity[LitterRobot4], UpdateEntity): class RobotUpdateEntity(LitterRobotEntity[LitterRobot4], UpdateEntity):
@ -53,16 +52,6 @@ class RobotUpdateEntity(LitterRobotEntity[LitterRobot4], UpdateEntity):
UpdateEntityFeature.INSTALL | UpdateEntityFeature.PROGRESS 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 @property
def installed_version(self) -> str: def installed_version(self) -> str:
"""Version installed and in use.""" """Version installed and in use."""
@ -73,39 +62,27 @@ class RobotUpdateEntity(LitterRobotEntity[LitterRobot4], UpdateEntity):
"""Update installation progress.""" """Update installation progress."""
return self.robot.firmware_update_triggered 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.""" """Update the entity."""
self._poll_unsub = None # If the robot has a firmware update already in progress, checking for the
# latest firmware informs that an update has already been triggered, no
if await self.robot.has_firmware_update(): # firmware information is returned and we won't know the latest version.
latest_version = await self.robot.get_latest_firmware() if not self.robot.firmware_update_triggered:
else: latest_version = await self.robot.get_latest_firmware(True)
latest_version = self.installed_version if not await self.robot.has_firmware_update():
latest_version = self.robot.firmware
if self._attr_latest_version != self.installed_version:
self._attr_latest_version = latest_version 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( async def async_install(
self, version: str | None, backup: bool, **kwargs: Any self, version: str | None, backup: bool, **kwargs: Any
) -> None: ) -> None:
"""Install an update.""" """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(): if not await self.robot.update_firmware():
message = f"Unable to start firmware update on {self.robot.name}" message = f"Unable to start firmware update on {self.robot.name}"
raise HomeAssistantError(message) 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

View File

@ -3,7 +3,7 @@
"name": "Nanoleaf", "name": "Nanoleaf",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/nanoleaf", "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."], "zeroconf": ["_nanoleafms._tcp.local.", "_nanoleafapi._tcp.local."],
"homekit": { "homekit": {
"models": ["NL29", "NL42", "NL47", "NL48", "NL52", "NL59"] "models": ["NL29", "NL42", "NL47", "NL48", "NL52", "NL59"]

View File

@ -3,7 +3,7 @@
"domain": "tibber", "domain": "tibber",
"name": "Tibber", "name": "Tibber",
"documentation": "https://www.home-assistant.io/integrations/tibber", "documentation": "https://www.home-assistant.io/integrations/tibber",
"requirements": ["pyTibber==0.26.7"], "requirements": ["pyTibber==0.26.8"],
"codeowners": ["@danielhiversen"], "codeowners": ["@danielhiversen"],
"quality_scale": "silver", "quality_scale": "silver",
"config_flow": true, "config_flow": true,

View File

@ -3,7 +3,7 @@
"name": "LG webOS Smart TV", "name": "LG webOS Smart TV",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/webostv", "documentation": "https://www.home-assistant.io/integrations/webostv",
"requirements": ["aiowebostv==0.2.1"], "requirements": ["aiowebostv==0.3.0"],
"codeowners": ["@bendavid", "@thecode"], "codeowners": ["@bendavid", "@thecode"],
"ssdp": [{ "st": "urn:lge-com:service:webos-second-screen:1" }], "ssdp": [{ "st": "urn:lge-com:service:webos-second-screen:1" }],
"quality_scale": "platinum", "quality_scale": "platinum",

View File

@ -8,7 +8,7 @@ from .backports.enum import StrEnum
APPLICATION_NAME: Final = "HomeAssistant" APPLICATION_NAME: Final = "HomeAssistant"
MAJOR_VERSION: Final = 2023 MAJOR_VERSION: Final = 2023
MINOR_VERSION: Final = 1 MINOR_VERSION: Final = 1
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, 9, 0) REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0)

View File

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "homeassistant" name = "homeassistant"
version = "2023.1.3" version = "2023.1.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"

View File

@ -220,7 +220,7 @@ aiomodernforms==0.1.8
aiomusiccast==0.14.4 aiomusiccast==0.14.4
# homeassistant.components.nanoleaf # homeassistant.components.nanoleaf
aionanoleaf==0.2.0 aionanoleaf==0.2.1
# homeassistant.components.keyboard_remote # homeassistant.components.keyboard_remote
aionotify==0.2.0 aionotify==0.2.0
@ -297,7 +297,7 @@ aiovlc==0.1.0
aiowatttime==0.1.1 aiowatttime==0.1.1
# homeassistant.components.webostv # homeassistant.components.webostv
aiowebostv==0.2.1 aiowebostv==0.3.0
# homeassistant.components.yandex_transport # homeassistant.components.yandex_transport
aioymaps==1.2.2 aioymaps==1.2.2
@ -906,7 +906,7 @@ horimote==0.4.1
httplib2==0.20.4 httplib2==0.20.4
# homeassistant.components.huawei_lte # homeassistant.components.huawei_lte
huawei-lte-api==1.6.7 huawei-lte-api==1.6.11
# homeassistant.components.hydrawise # homeassistant.components.hydrawise
hydrawiser==0.2 hydrawiser==0.2
@ -1439,7 +1439,7 @@ pyRFXtrx==0.30.0
pySwitchmate==0.5.1 pySwitchmate==0.5.1
# homeassistant.components.tibber # homeassistant.components.tibber
pyTibber==0.26.7 pyTibber==0.26.8
# homeassistant.components.dlink # homeassistant.components.dlink
pyW215==0.7.0 pyW215==0.7.0
@ -1719,7 +1719,7 @@ pylibrespot-java==0.1.1
pylitejet==0.3.0 pylitejet==0.3.0
# homeassistant.components.litterrobot # homeassistant.components.litterrobot
pylitterbot==2022.12.0 pylitterbot==2023.1.1
# homeassistant.components.lutron_caseta # homeassistant.components.lutron_caseta
pylutron-caseta==0.17.1 pylutron-caseta==0.17.1

View File

@ -198,7 +198,7 @@ aiomodernforms==0.1.8
aiomusiccast==0.14.4 aiomusiccast==0.14.4
# homeassistant.components.nanoleaf # homeassistant.components.nanoleaf
aionanoleaf==0.2.0 aionanoleaf==0.2.1
# homeassistant.components.notion # homeassistant.components.notion
aionotion==3.0.2 aionotion==3.0.2
@ -272,7 +272,7 @@ aiovlc==0.1.0
aiowatttime==0.1.1 aiowatttime==0.1.1
# homeassistant.components.webostv # homeassistant.components.webostv
aiowebostv==0.2.1 aiowebostv==0.3.0
# homeassistant.components.yandex_transport # homeassistant.components.yandex_transport
aioymaps==1.2.2 aioymaps==1.2.2
@ -683,7 +683,7 @@ homepluscontrol==0.0.5
httplib2==0.20.4 httplib2==0.20.4
# homeassistant.components.huawei_lte # homeassistant.components.huawei_lte
huawei-lte-api==1.6.7 huawei-lte-api==1.6.11
# homeassistant.components.hyperion # homeassistant.components.hyperion
hyperion-py==0.7.5 hyperion-py==0.7.5
@ -1039,7 +1039,7 @@ pyMetno==0.9.0
pyRFXtrx==0.30.0 pyRFXtrx==0.30.0
# homeassistant.components.tibber # homeassistant.components.tibber
pyTibber==0.26.7 pyTibber==0.26.8
# homeassistant.components.nextbus # homeassistant.components.nextbus
py_nextbusnext==0.1.5 py_nextbusnext==0.1.5
@ -1220,7 +1220,7 @@ pylibrespot-java==0.1.1
pylitejet==0.3.0 pylitejet==0.3.0
# homeassistant.components.litterrobot # homeassistant.components.litterrobot
pylitterbot==2022.12.0 pylitterbot==2023.1.1
# homeassistant.components.lutron_caseta # homeassistant.components.lutron_caseta
pylutron-caseta==0.17.1 pylutron-caseta==0.17.1

View File

@ -4,7 +4,6 @@ from collections.abc import Generator
from unittest.mock import Mock, patch from unittest.mock import Mock, patch
from gspread import GSpreadException from gspread import GSpreadException
import oauth2client
import pytest import pytest
from homeassistant import config_entries from homeassistant import config_entries
@ -21,6 +20,8 @@ from tests.common import MockConfigEntry
CLIENT_ID = "1234" CLIENT_ID = "1234"
CLIENT_SECRET = "5678" 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" SHEET_ID = "google-sheet-id"
TITLE = "Google Sheets" TITLE = "Google Sheets"
@ -66,7 +67,7 @@ async def test_full_flow(
) )
assert result["url"] == ( 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" "&redirect_uri=https://example.com/auth/external/callback"
f"&state={state}&scope=https://www.googleapis.com/auth/drive.file" f"&state={state}&scope=https://www.googleapis.com/auth/drive.file"
"&access_type=offline&prompt=consent" "&access_type=offline&prompt=consent"
@ -83,7 +84,7 @@ async def test_full_flow(
mock_client.return_value.create = mock_create mock_client.return_value.create = mock_create
aioclient_mock.post( aioclient_mock.post(
oauth2client.GOOGLE_TOKEN_URI, GOOGLE_TOKEN_URI,
json={ json={
"refresh_token": "mock-refresh-token", "refresh_token": "mock-refresh-token",
"access_token": "mock-access-token", "access_token": "mock-access-token",
@ -133,7 +134,7 @@ async def test_create_sheet_error(
) )
assert result["url"] == ( 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" "&redirect_uri=https://example.com/auth/external/callback"
f"&state={state}&scope=https://www.googleapis.com/auth/drive.file" f"&state={state}&scope=https://www.googleapis.com/auth/drive.file"
"&access_type=offline&prompt=consent" "&access_type=offline&prompt=consent"
@ -150,7 +151,7 @@ async def test_create_sheet_error(
mock_client.return_value.create = mock_create mock_client.return_value.create = mock_create
aioclient_mock.post( aioclient_mock.post(
oauth2client.GOOGLE_TOKEN_URI, GOOGLE_TOKEN_URI,
json={ json={
"refresh_token": "mock-refresh-token", "refresh_token": "mock-refresh-token",
"access_token": "mock-access-token", "access_token": "mock-access-token",
@ -202,7 +203,7 @@ async def test_reauth(
}, },
) )
assert result["url"] == ( 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" "&redirect_uri=https://example.com/auth/external/callback"
f"&state={state}&scope=https://www.googleapis.com/auth/drive.file" f"&state={state}&scope=https://www.googleapis.com/auth/drive.file"
"&access_type=offline&prompt=consent" "&access_type=offline&prompt=consent"
@ -218,7 +219,7 @@ async def test_reauth(
mock_client.return_value.open_by_key = mock_open mock_client.return_value.open_by_key = mock_open
aioclient_mock.post( aioclient_mock.post(
oauth2client.GOOGLE_TOKEN_URI, GOOGLE_TOKEN_URI,
json={ json={
"refresh_token": "mock-refresh-token", "refresh_token": "mock-refresh-token",
"access_token": "updated-access-token", "access_token": "updated-access-token",
@ -283,7 +284,7 @@ async def test_reauth_abort(
}, },
) )
assert result["url"] == ( 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" "&redirect_uri=https://example.com/auth/external/callback"
f"&state={state}&scope=https://www.googleapis.com/auth/drive.file" f"&state={state}&scope=https://www.googleapis.com/auth/drive.file"
"&access_type=offline&prompt=consent" "&access_type=offline&prompt=consent"
@ -300,7 +301,7 @@ async def test_reauth_abort(
mock_client.return_value.open_by_key = mock_open mock_client.return_value.open_by_key = mock_open
aioclient_mock.post( aioclient_mock.post(
oauth2client.GOOGLE_TOKEN_URI, GOOGLE_TOKEN_URI,
json={ json={
"refresh_token": "mock-refresh-token", "refresh_token": "mock-refresh-token",
"access_token": "updated-access-token", "access_token": "updated-access-token",
@ -346,7 +347,7 @@ async def test_already_configured(
) )
assert result["url"] == ( 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" "&redirect_uri=https://example.com/auth/external/callback"
f"&state={state}&scope=https://www.googleapis.com/auth/drive.file" f"&state={state}&scope=https://www.googleapis.com/auth/drive.file"
"&access_type=offline&prompt=consent" "&access_type=offline&prompt=consent"
@ -363,7 +364,7 @@ async def test_already_configured(
mock_client.return_value.create = mock_create mock_client.return_value.create = mock_create
aioclient_mock.post( aioclient_mock.post(
oauth2client.GOOGLE_TOKEN_URI, GOOGLE_TOKEN_URI,
json={ json={
"refresh_token": "mock-refresh-token", "refresh_token": "mock-refresh-token",
"access_token": "mock-access-token", "access_token": "mock-access-token",

View File

@ -211,9 +211,14 @@ async def test_success(hass, login_requests_mock):
@pytest.mark.parametrize( @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": "<response><devicename>Mock device</devicename></response>",
},
{ {
ssdp.ATTR_UPNP_FRIENDLY_NAME: "Mobile Wi-Fi", ssdp.ATTR_UPNP_FRIENDLY_NAME: "Mobile Wi-Fi",
ssdp.ATTR_UPNP_SERIAL: "00000000", 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": "<error><code>100002</code><message/></error>",
},
{ {
ssdp.ATTR_UPNP_FRIENDLY_NAME: "Mobile Wi-Fi", ssdp.ATTR_UPNP_FRIENDLY_NAME: "Mobile Wi-Fi",
# No ssdp.ATTR_UPNP_SERIAL # No ssdp.ATTR_UPNP_SERIAL
@ -235,19 +245,36 @@ async def test_success(hass, login_requests_mock):
"errors": {}, "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.""" """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} context = {"source": config_entries.SOURCE_SSDP}
login_requests_mock.request(**requests_mock_request_kwargs)
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, DOMAIN,
context=context, context=context,
data=ssdp.SsdpServiceInfo( data=ssdp.SsdpServiceInfo(
ssdp_usn="mock_usn", ssdp_usn="mock_usn",
ssdp_st="upnp:rootdevice", ssdp_st="upnp:rootdevice",
ssdp_location="http://192.168.100.1:60957/rootDesc.xml", ssdp_location=f"{url}:60957/rootDesc.xml",
upnp={ upnp={
ssdp.ATTR_UPNP_DEVICE_TYPE: "urn:schemas-upnp-org:device:InternetGatewayDevice:1", ssdp.ATTR_UPNP_DEVICE_TYPE: "urn:schemas-upnp-org:device:InternetGatewayDevice:1",
ssdp.ATTR_UPNP_MANUFACTURER: "Huawei", 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(): for k, v in expected_result.items():
assert result[k] == v assert result[k] == v
if result.get("data_schema"): if result.get("data_schema"):
result["data_schema"]({})[CONF_URL] == url assert result["data_schema"]({})[CONF_URL] == url + "/"
@pytest.mark.parametrize( @pytest.mark.parametrize(

View File

@ -11,7 +11,13 @@ from homeassistant.components.update import (
SERVICE_INSTALL, SERVICE_INSTALL,
UpdateDeviceClass, 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.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
@ -28,6 +34,7 @@ async def test_robot_with_no_update(
"""Tests the update entity was set up.""" """Tests the update entity was set up."""
robot: LitterRobot4 = mock_account_with_litterrobot_4.robots[0] robot: LitterRobot4 = mock_account_with_litterrobot_4.robots[0]
robot.has_firmware_update = AsyncMock(return_value=False) robot.has_firmware_update = AsyncMock(return_value=False)
robot.get_latest_firmware = AsyncMock(return_value=None)
entry = await setup_integration( entry = await setup_integration(
hass, mock_account_with_litterrobot_4, PLATFORM_DOMAIN hass, mock_account_with_litterrobot_4, PLATFORM_DOMAIN
@ -79,3 +86,27 @@ async def test_robot_with_update(
) )
await hass.async_block_till_done() await hass.async_block_till_done()
assert robot.update_firmware.call_count == 1 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()