mirror of
https://github.com/home-assistant/core.git
synced 2025-07-27 07:07:28 +00:00
2023.1.4 (#85764)
This commit is contained in:
commit
3bb9be2382
39
Dockerfile
39
Dockerfile
@ -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 /
|
||||||
|
@ -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",
|
||||||
}
|
}
|
||||||
|
@ -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": {
|
||||||
|
@ -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"
|
||||||
],
|
],
|
||||||
|
@ -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",
|
||||||
|
@ -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",
|
||||||
|
@ -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",
|
||||||
|
@ -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
|
|
||||||
|
@ -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"]
|
||||||
|
@ -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,
|
||||||
|
@ -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",
|
||||||
|
@ -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)
|
||||||
|
@ -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"
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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",
|
||||||
|
@ -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(
|
||||||
|
@ -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()
|
||||||
|
Loading…
x
Reference in New Issue
Block a user