forked from home-assistant/core
Compare commits
23 Commits
2024.11.0b
...
2024.11.0b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
211ce43127 | ||
|
|
f5555df990 | ||
|
|
82c2422990 | ||
|
|
734ebc1adb | ||
|
|
eb3371beef | ||
|
|
e1ef1063fe | ||
|
|
c355a53485 | ||
|
|
c85eb6bf8e | ||
|
|
cc30d34e87 | ||
|
|
14875a1101 | ||
|
|
030aebb97f | ||
|
|
6e2f36b6d4 | ||
|
|
25a05eb156 | ||
|
|
b71c4377f6 | ||
|
|
d671341864 | ||
|
|
383f712d43 | ||
|
|
8a20cd77a0 | ||
|
|
14023644ef | ||
|
|
496fc42b94 | ||
|
|
da0688ce8e | ||
|
|
89d3707cb7 | ||
|
|
3f5e395e2f | ||
|
|
00ea1cab9f |
@@ -7,7 +7,11 @@ from typing import Any
|
||||
|
||||
from bimmer_connected.api.authentication import MyBMWAuthentication
|
||||
from bimmer_connected.api.regions import get_region_from_name
|
||||
from bimmer_connected.models import MyBMWAPIError, MyBMWAuthError
|
||||
from bimmer_connected.models import (
|
||||
MyBMWAPIError,
|
||||
MyBMWAuthError,
|
||||
MyBMWCaptchaMissingError,
|
||||
)
|
||||
from httpx import RequestError
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -54,6 +58,8 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str,
|
||||
|
||||
try:
|
||||
await auth.login()
|
||||
except MyBMWCaptchaMissingError as ex:
|
||||
raise MissingCaptcha from ex
|
||||
except MyBMWAuthError as ex:
|
||||
raise InvalidAuth from ex
|
||||
except (MyBMWAPIError, RequestError) as ex:
|
||||
@@ -98,6 +104,8 @@ class BMWConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
CONF_REFRESH_TOKEN: info.get(CONF_REFRESH_TOKEN),
|
||||
CONF_GCID: info.get(CONF_GCID),
|
||||
}
|
||||
except MissingCaptcha:
|
||||
errors["base"] = "missing_captcha"
|
||||
except CannotConnect:
|
||||
errors["base"] = "cannot_connect"
|
||||
except InvalidAuth:
|
||||
@@ -192,3 +200,7 @@ class CannotConnect(HomeAssistantError):
|
||||
|
||||
class InvalidAuth(HomeAssistantError):
|
||||
"""Error to indicate there is invalid auth."""
|
||||
|
||||
|
||||
class MissingCaptcha(HomeAssistantError):
|
||||
"""Error to indicate the captcha token is missing."""
|
||||
|
||||
@@ -7,7 +7,12 @@ import logging
|
||||
|
||||
from bimmer_connected.account import MyBMWAccount
|
||||
from bimmer_connected.api.regions import get_region_from_name
|
||||
from bimmer_connected.models import GPSPosition, MyBMWAPIError, MyBMWAuthError
|
||||
from bimmer_connected.models import (
|
||||
GPSPosition,
|
||||
MyBMWAPIError,
|
||||
MyBMWAuthError,
|
||||
MyBMWCaptchaMissingError,
|
||||
)
|
||||
from httpx import RequestError
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
@@ -61,6 +66,12 @@ class BMWDataUpdateCoordinator(DataUpdateCoordinator[None]):
|
||||
|
||||
try:
|
||||
await self.account.get_vehicles()
|
||||
except MyBMWCaptchaMissingError as err:
|
||||
# If a captcha is required (user/password login flow), always trigger the reauth flow
|
||||
raise ConfigEntryAuthFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="missing_captcha",
|
||||
) from err
|
||||
except MyBMWAuthError as err:
|
||||
# Allow one retry interval before raising AuthFailed to avoid flaky API issues
|
||||
if self.last_update_success:
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["bimmer_connected"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["bimmer-connected[china]==0.16.3"]
|
||||
"requirements": ["bimmer-connected[china]==0.16.4"]
|
||||
}
|
||||
|
||||
@@ -11,7 +11,8 @@
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]"
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||
"missing_captcha": "Captcha validation missing"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
|
||||
@@ -200,6 +201,9 @@
|
||||
"exceptions": {
|
||||
"invalid_poi": {
|
||||
"message": "Invalid data for point of interest: {poi_exception}"
|
||||
},
|
||||
"missing_captcha": {
|
||||
"message": "Login requires captcha validation"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -484,9 +484,13 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
self._create_stream_lock: asyncio.Lock | None = None
|
||||
self._webrtc_provider: CameraWebRTCProvider | None = None
|
||||
self._legacy_webrtc_provider: CameraWebRTCLegacyProvider | None = None
|
||||
self._webrtc_sync_offer = (
|
||||
self._supports_native_sync_webrtc = (
|
||||
type(self).async_handle_web_rtc_offer != Camera.async_handle_web_rtc_offer
|
||||
)
|
||||
self._supports_native_async_webrtc = (
|
||||
type(self).async_handle_async_webrtc_offer
|
||||
!= Camera.async_handle_async_webrtc_offer
|
||||
)
|
||||
|
||||
@cached_property
|
||||
def entity_picture(self) -> str:
|
||||
@@ -623,7 +627,7 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
|
||||
Integrations can override with a native WebRTC implementation.
|
||||
"""
|
||||
if self._webrtc_sync_offer:
|
||||
if self._supports_native_sync_webrtc:
|
||||
try:
|
||||
answer = await self.async_handle_web_rtc_offer(offer_sdp)
|
||||
except ValueError as ex:
|
||||
@@ -788,18 +792,25 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
providers or inputs to the state attributes change.
|
||||
"""
|
||||
old_provider = self._webrtc_provider
|
||||
new_provider = await self._async_get_supported_webrtc_provider(
|
||||
async_get_supported_provider
|
||||
)
|
||||
|
||||
old_legacy_provider = self._legacy_webrtc_provider
|
||||
new_provider = None
|
||||
new_legacy_provider = None
|
||||
if new_provider is None:
|
||||
# Only add the legacy provider if the new provider is not available
|
||||
new_legacy_provider = await self._async_get_supported_webrtc_provider(
|
||||
async_get_supported_legacy_provider
|
||||
|
||||
# Skip all providers if the camera has a native WebRTC implementation
|
||||
if not (
|
||||
self._supports_native_sync_webrtc or self._supports_native_async_webrtc
|
||||
):
|
||||
# Camera doesn't have a native WebRTC implementation
|
||||
new_provider = await self._async_get_supported_webrtc_provider(
|
||||
async_get_supported_provider
|
||||
)
|
||||
|
||||
if new_provider is None:
|
||||
# Only add the legacy provider if the new provider is not available
|
||||
new_legacy_provider = await self._async_get_supported_webrtc_provider(
|
||||
async_get_supported_legacy_provider
|
||||
)
|
||||
|
||||
if old_provider != new_provider or old_legacy_provider != new_legacy_provider:
|
||||
self._webrtc_provider = new_provider
|
||||
self._legacy_webrtc_provider = new_legacy_provider
|
||||
@@ -827,7 +838,7 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
"""Return the WebRTC client configuration and extend it with the registered ice servers."""
|
||||
config = self._async_get_webrtc_client_configuration()
|
||||
|
||||
if not self._webrtc_sync_offer:
|
||||
if not self._supports_native_sync_webrtc:
|
||||
# Until 2024.11, the frontend was not resolving any ice servers
|
||||
# The async approach was added 2024.11 and new integrations need to use it
|
||||
ice_servers = [
|
||||
@@ -867,12 +878,7 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
"""Return the camera capabilities."""
|
||||
frontend_stream_types = set()
|
||||
if CameraEntityFeature.STREAM in self.supported_features_compat:
|
||||
if (
|
||||
type(self).async_handle_web_rtc_offer
|
||||
!= Camera.async_handle_web_rtc_offer
|
||||
or type(self).async_handle_async_webrtc_offer
|
||||
!= Camera.async_handle_async_webrtc_offer
|
||||
):
|
||||
if self._supports_native_sync_webrtc or self._supports_native_async_webrtc:
|
||||
# The camera has a native WebRTC implementation
|
||||
frontend_stream_types.add(StreamType.WEB_RTC)
|
||||
else:
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["pyfibaro"],
|
||||
"requirements": ["pyfibaro==0.7.8"]
|
||||
"requirements": ["pyfibaro==0.8.0"]
|
||||
}
|
||||
|
||||
@@ -20,5 +20,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["home-assistant-frontend==20241104.0"]
|
||||
"requirements": ["home-assistant-frontend==20241105.0"]
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import shutil
|
||||
|
||||
from aiohttp.client_exceptions import ClientConnectionError, ServerConnectionError
|
||||
from go2rtc_client import Go2RtcRestClient
|
||||
from go2rtc_client.exceptions import Go2RtcClientError
|
||||
from go2rtc_client.exceptions import Go2RtcClientError, Go2RtcVersionError
|
||||
from go2rtc_client.ws import (
|
||||
Go2RtcWsClient,
|
||||
ReceiveMessages,
|
||||
@@ -38,7 +38,7 @@ from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.util.hass_dict import HassKey
|
||||
from homeassistant.util.package import is_docker_env
|
||||
|
||||
from .const import CONF_DEBUG_UI, DEBUG_UI_URL_MESSAGE, DEFAULT_URL, DOMAIN
|
||||
from .const import CONF_DEBUG_UI, DEBUG_UI_URL_MESSAGE, DOMAIN, HA_MANAGED_URL
|
||||
from .server import Server
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -114,14 +114,18 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
server = Server(
|
||||
hass, binary, enable_ui=config.get(DOMAIN, {}).get(CONF_DEBUG_UI, False)
|
||||
)
|
||||
await server.start()
|
||||
try:
|
||||
await server.start()
|
||||
except Exception: # noqa: BLE001
|
||||
_LOGGER.warning("Could not start go2rtc server", exc_info=True)
|
||||
return False
|
||||
|
||||
async def on_stop(event: Event) -> None:
|
||||
await server.stop()
|
||||
|
||||
hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, on_stop)
|
||||
|
||||
url = DEFAULT_URL
|
||||
url = HA_MANAGED_URL
|
||||
|
||||
hass.data[_DATA_GO2RTC] = url
|
||||
discovery_flow.async_create_flow(
|
||||
@@ -143,7 +147,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
# Validate the server URL
|
||||
try:
|
||||
client = Go2RtcRestClient(async_get_clientsession(hass), url)
|
||||
await client.streams.list()
|
||||
await client.validate_server_version()
|
||||
except Go2RtcClientError as err:
|
||||
if isinstance(err.__cause__, _RETRYABLE_ERRORS):
|
||||
raise ConfigEntryNotReady(
|
||||
@@ -151,6 +155,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
) from err
|
||||
_LOGGER.warning("Could not connect to go2rtc instance on %s (%s)", url, err)
|
||||
return False
|
||||
except Go2RtcVersionError as err:
|
||||
raise ConfigEntryNotReady(
|
||||
f"The go2rtc server version is not supported, {err}"
|
||||
) from err
|
||||
except Exception as err: # noqa: BLE001
|
||||
_LOGGER.warning("Could not connect to go2rtc instance on %s (%s)", url, err)
|
||||
return False
|
||||
|
||||
@@ -4,4 +4,5 @@ DOMAIN = "go2rtc"
|
||||
|
||||
CONF_DEBUG_UI = "debug_ui"
|
||||
DEBUG_UI_URL_MESSAGE = "Url and debug_ui cannot be set at the same time."
|
||||
DEFAULT_URL = "http://localhost:1984/"
|
||||
HA_MANAGED_API_PORT = 11984
|
||||
HA_MANAGED_URL = f"http://localhost:{HA_MANAGED_API_PORT}/"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Go2rtc server."""
|
||||
|
||||
import asyncio
|
||||
from collections import deque
|
||||
from contextlib import suppress
|
||||
import logging
|
||||
from tempfile import NamedTemporaryFile
|
||||
@@ -11,13 +12,14 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import DEFAULT_URL
|
||||
from .const import HA_MANAGED_API_PORT, HA_MANAGED_URL
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
_TERMINATE_TIMEOUT = 5
|
||||
_SETUP_TIMEOUT = 30
|
||||
_SUCCESSFUL_BOOT_MESSAGE = "INF [api] listen addr="
|
||||
_LOCALHOST_IP = "127.0.0.1"
|
||||
_LOG_BUFFER_SIZE = 512
|
||||
_RESPAWN_COOLDOWN = 1
|
||||
|
||||
# Default configuration for HA
|
||||
@@ -26,16 +28,27 @@ _RESPAWN_COOLDOWN = 1
|
||||
# - Clear default ice servers
|
||||
_GO2RTC_CONFIG_FORMAT = r"""
|
||||
api:
|
||||
listen: "{api_ip}:1984"
|
||||
listen: "{api_ip}:{api_port}"
|
||||
|
||||
rtsp:
|
||||
# ffmpeg needs rtsp for opus audio transcoding
|
||||
listen: "127.0.0.1:8554"
|
||||
listen: "127.0.0.1:18554"
|
||||
|
||||
webrtc:
|
||||
listen: ":18555/tcp"
|
||||
ice_servers: []
|
||||
"""
|
||||
|
||||
_LOG_LEVEL_MAP = {
|
||||
"TRC": logging.DEBUG,
|
||||
"DBG": logging.DEBUG,
|
||||
"INF": logging.DEBUG,
|
||||
"WRN": logging.WARNING,
|
||||
"ERR": logging.WARNING,
|
||||
"FTL": logging.ERROR,
|
||||
"PNC": logging.ERROR,
|
||||
}
|
||||
|
||||
|
||||
class Go2RTCServerStartError(HomeAssistantError):
|
||||
"""Raised when server does not start."""
|
||||
@@ -52,7 +65,11 @@ def _create_temp_file(api_ip: str) -> str:
|
||||
# Set delete=False to prevent the file from being deleted when the file is closed
|
||||
# Linux is clearing tmp folder on reboot, so no need to delete it manually
|
||||
with NamedTemporaryFile(prefix="go2rtc_", suffix=".yaml", delete=False) as file:
|
||||
file.write(_GO2RTC_CONFIG_FORMAT.format(api_ip=api_ip).encode())
|
||||
file.write(
|
||||
_GO2RTC_CONFIG_FORMAT.format(
|
||||
api_ip=api_ip, api_port=HA_MANAGED_API_PORT
|
||||
).encode()
|
||||
)
|
||||
return file.name
|
||||
|
||||
|
||||
@@ -65,6 +82,7 @@ class Server:
|
||||
"""Initialize the server."""
|
||||
self._hass = hass
|
||||
self._binary = binary
|
||||
self._log_buffer: deque[str] = deque(maxlen=_LOG_BUFFER_SIZE)
|
||||
self._process: asyncio.subprocess.Process | None = None
|
||||
self._startup_complete = asyncio.Event()
|
||||
self._api_ip = _LOCALHOST_IP
|
||||
@@ -109,19 +127,34 @@ class Server:
|
||||
except TimeoutError as err:
|
||||
msg = "Go2rtc server didn't start correctly"
|
||||
_LOGGER.exception(msg)
|
||||
self._log_server_output(logging.WARNING)
|
||||
await self._stop()
|
||||
raise Go2RTCServerStartError from err
|
||||
|
||||
# Check the server version
|
||||
client = Go2RtcRestClient(async_get_clientsession(self._hass), HA_MANAGED_URL)
|
||||
await client.validate_server_version()
|
||||
|
||||
async def _log_output(self, process: asyncio.subprocess.Process) -> None:
|
||||
"""Log the output of the process."""
|
||||
assert process.stdout is not None
|
||||
|
||||
async for line in process.stdout:
|
||||
msg = line[:-1].decode().strip()
|
||||
_LOGGER.debug(msg)
|
||||
self._log_buffer.append(msg)
|
||||
loglevel = logging.WARNING
|
||||
if len(split_msg := msg.split(" ", 2)) == 3:
|
||||
loglevel = _LOG_LEVEL_MAP.get(split_msg[1], loglevel)
|
||||
_LOGGER.log(loglevel, msg)
|
||||
if not self._startup_complete.is_set() and _SUCCESSFUL_BOOT_MESSAGE in msg:
|
||||
self._startup_complete.set()
|
||||
|
||||
def _log_server_output(self, loglevel: int) -> None:
|
||||
"""Log captured process output, then clear the log buffer."""
|
||||
for line in list(self._log_buffer): # Copy the deque to avoid mutation error
|
||||
_LOGGER.log(loglevel, line)
|
||||
self._log_buffer.clear()
|
||||
|
||||
async def _watchdog(self) -> None:
|
||||
"""Keep respawning go2rtc servers.
|
||||
|
||||
@@ -149,6 +182,8 @@ class Server:
|
||||
await asyncio.sleep(_RESPAWN_COOLDOWN)
|
||||
try:
|
||||
await self._stop()
|
||||
_LOGGER.warning("Go2rtc unexpectedly stopped, server log:")
|
||||
self._log_server_output(logging.WARNING)
|
||||
_LOGGER.debug("Spawning new go2rtc server")
|
||||
with suppress(Go2RTCServerStartError):
|
||||
await self._start()
|
||||
@@ -169,12 +204,12 @@ class Server:
|
||||
|
||||
async def _monitor_api(self) -> None:
|
||||
"""Raise if the go2rtc process terminates."""
|
||||
client = Go2RtcRestClient(async_get_clientsession(self._hass), DEFAULT_URL)
|
||||
client = Go2RtcRestClient(async_get_clientsession(self._hass), HA_MANAGED_URL)
|
||||
|
||||
_LOGGER.debug("Monitoring go2rtc API")
|
||||
try:
|
||||
while True:
|
||||
await client.streams.list()
|
||||
await client.validate_server_version()
|
||||
await asyncio.sleep(10)
|
||||
except Exception as err:
|
||||
_LOGGER.debug("go2rtc API did not reply", exc_info=True)
|
||||
|
||||
@@ -103,6 +103,7 @@ PLACEHOLDER_KEY_ADDON_URL = "addon_url"
|
||||
PLACEHOLDER_KEY_REFERENCE = "reference"
|
||||
PLACEHOLDER_KEY_COMPONENTS = "components"
|
||||
|
||||
ISSUE_KEY_ADDON_BOOT_FAIL = "issue_addon_boot_fail"
|
||||
ISSUE_KEY_SYSTEM_DOCKER_CONFIG = "issue_system_docker_config"
|
||||
ISSUE_KEY_ADDON_DETACHED_ADDON_MISSING = "issue_addon_detached_addon_missing"
|
||||
ISSUE_KEY_ADDON_DETACHED_ADDON_REMOVED = "issue_addon_detached_addon_removed"
|
||||
|
||||
@@ -36,6 +36,7 @@ from .const import (
|
||||
EVENT_SUPERVISOR_EVENT,
|
||||
EVENT_SUPERVISOR_UPDATE,
|
||||
EVENT_SUPPORTED_CHANGED,
|
||||
ISSUE_KEY_ADDON_BOOT_FAIL,
|
||||
ISSUE_KEY_ADDON_DETACHED_ADDON_MISSING,
|
||||
ISSUE_KEY_ADDON_DETACHED_ADDON_REMOVED,
|
||||
ISSUE_KEY_SYSTEM_DOCKER_CONFIG,
|
||||
@@ -94,6 +95,7 @@ UNHEALTHY_REASONS = {
|
||||
|
||||
# Keys (type + context) of issues that when found should be made into a repair
|
||||
ISSUE_KEYS_FOR_REPAIRS = {
|
||||
ISSUE_KEY_ADDON_BOOT_FAIL,
|
||||
"issue_mount_mount_failed",
|
||||
"issue_system_multiple_data_disks",
|
||||
"issue_system_reboot_required",
|
||||
|
||||
@@ -14,6 +14,7 @@ from homeassistant.data_entry_flow import FlowResult
|
||||
|
||||
from . import get_addons_info, get_issues_info
|
||||
from .const import (
|
||||
ISSUE_KEY_ADDON_BOOT_FAIL,
|
||||
ISSUE_KEY_ADDON_DETACHED_ADDON_REMOVED,
|
||||
ISSUE_KEY_SYSTEM_DOCKER_CONFIG,
|
||||
PLACEHOLDER_KEY_ADDON,
|
||||
@@ -181,8 +182,8 @@ class DockerConfigIssueRepairFlow(SupervisorIssueRepairFlow):
|
||||
return placeholders
|
||||
|
||||
|
||||
class DetachedAddonIssueRepairFlow(SupervisorIssueRepairFlow):
|
||||
"""Handler for detached addon issue fixing flows."""
|
||||
class AddonIssueRepairFlow(SupervisorIssueRepairFlow):
|
||||
"""Handler for addon issue fixing flows."""
|
||||
|
||||
@property
|
||||
def description_placeholders(self) -> dict[str, str] | None:
|
||||
@@ -210,7 +211,10 @@ async def async_create_fix_flow(
|
||||
issue = supervisor_issues and supervisor_issues.get_issue(issue_id)
|
||||
if issue and issue.key == ISSUE_KEY_SYSTEM_DOCKER_CONFIG:
|
||||
return DockerConfigIssueRepairFlow(issue_id)
|
||||
if issue and issue.key == ISSUE_KEY_ADDON_DETACHED_ADDON_REMOVED:
|
||||
return DetachedAddonIssueRepairFlow(issue_id)
|
||||
if issue and issue.key in {
|
||||
ISSUE_KEY_ADDON_DETACHED_ADDON_REMOVED,
|
||||
ISSUE_KEY_ADDON_BOOT_FAIL,
|
||||
}:
|
||||
return AddonIssueRepairFlow(issue_id)
|
||||
|
||||
return SupervisorIssueRepairFlow(issue_id)
|
||||
|
||||
@@ -17,6 +17,23 @@
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"issue_addon_boot_fail": {
|
||||
"title": "Add-on failed to start at boot",
|
||||
"fix_flow": {
|
||||
"step": {
|
||||
"fix_menu": {
|
||||
"description": "Add-on {addon} is set to start at boot but failed to start. Usually this occurs when the configuration is incorrect or the same port is used in multiple add-ons. Check the configuration as well as logs for {addon} and Supervisor.\n\nUse Start to try again or Disable to turn off the start at boot option.",
|
||||
"menu_options": {
|
||||
"addon_execute_start": "Start",
|
||||
"addon_disable_boot": "Disable"
|
||||
}
|
||||
}
|
||||
},
|
||||
"abort": {
|
||||
"apply_suggestion_fail": "Could not apply the fix. Check the Supervisor logs for more details."
|
||||
}
|
||||
}
|
||||
},
|
||||
"issue_addon_detached_addon_missing": {
|
||||
"title": "Missing repository for an installed add-on",
|
||||
"description": "Repository for add-on {addon} is missing. This means it will not get updates, and backups may not be restored correctly as the supervisor may not be able to build/download the resources required.\n\nPlease check the [add-on's documentation]({addon_url}) for installation instructions and add the repository to the store."
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/holiday",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["holidays==0.59", "babel==2.15.0"]
|
||||
"requirements": ["holidays==0.60", "babel==2.15.0"]
|
||||
}
|
||||
|
||||
@@ -37,11 +37,8 @@
|
||||
"set_light_color": {
|
||||
"message": "Error while trying to set color of {entity_id}: {description}"
|
||||
},
|
||||
"set_light_effect": {
|
||||
"message": "Error while trying to set effect of {entity_id}: {description}"
|
||||
},
|
||||
"set_setting": {
|
||||
"message": "Error while trying to set \"{value}\" to \"{key}\" setting for {entity_id}: {description}"
|
||||
"message": "Error while trying to assign the value \"{value}\" to the setting \"{key}\" for {entity_id}: {description}"
|
||||
},
|
||||
"turn_on": {
|
||||
"message": "Error while trying to turn on {entity_id} ({key}): {description}"
|
||||
|
||||
@@ -120,12 +120,22 @@ class ImprovBLEConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
assert self._discovery_info is not None
|
||||
|
||||
service_data = self._discovery_info.service_data
|
||||
improv_service_data = ImprovServiceData.from_bytes(
|
||||
service_data[SERVICE_DATA_UUID]
|
||||
)
|
||||
try:
|
||||
improv_service_data = ImprovServiceData.from_bytes(
|
||||
service_data[SERVICE_DATA_UUID]
|
||||
)
|
||||
except improv_ble_errors.InvalidCommand as err:
|
||||
_LOGGER.warning(
|
||||
"Aborting improv flow, device %s sent invalid improv data: '%s'",
|
||||
self._discovery_info.address,
|
||||
service_data[SERVICE_DATA_UUID].hex(),
|
||||
)
|
||||
raise AbortFlow("invalid_improv_data") from err
|
||||
|
||||
if improv_service_data.state in (State.PROVISIONING, State.PROVISIONED):
|
||||
_LOGGER.debug(
|
||||
"Aborting improv flow, device is already provisioned: %s",
|
||||
"Aborting improv flow, device %s is already provisioned: %s",
|
||||
self._discovery_info.address,
|
||||
improv_service_data.state,
|
||||
)
|
||||
raise AbortFlow("already_provisioned")
|
||||
|
||||
@@ -92,4 +92,7 @@ class IronOSUpdate(IronOSBaseEntity, UpdateEntity):
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return if entity is available."""
|
||||
return super().available and self.firmware_update.last_update_success
|
||||
return (
|
||||
self.installed_version is not None
|
||||
and self.firmware_update.last_update_success
|
||||
)
|
||||
|
||||
@@ -255,73 +255,9 @@ WATER_INFO_SENSOR_DESC: dict[ThinQProperty, SensorEntityDescription] = {
|
||||
translation_key=ThinQProperty.WATER_TYPE,
|
||||
),
|
||||
}
|
||||
TIMER_SENSOR_DESC: dict[ThinQProperty, SensorEntityDescription] = {
|
||||
TimerProperty.RELATIVE_TO_START: SensorEntityDescription(
|
||||
key=TimerProperty.RELATIVE_TO_START,
|
||||
translation_key=TimerProperty.RELATIVE_TO_START,
|
||||
),
|
||||
TimerProperty.RELATIVE_TO_START_WM: SensorEntityDescription(
|
||||
key=TimerProperty.RELATIVE_TO_START,
|
||||
translation_key=TimerProperty.RELATIVE_TO_START_WM,
|
||||
),
|
||||
TimerProperty.RELATIVE_TO_STOP: SensorEntityDescription(
|
||||
key=TimerProperty.RELATIVE_TO_STOP,
|
||||
translation_key=TimerProperty.RELATIVE_TO_STOP,
|
||||
),
|
||||
TimerProperty.RELATIVE_TO_STOP_WM: SensorEntityDescription(
|
||||
key=TimerProperty.RELATIVE_TO_STOP,
|
||||
translation_key=TimerProperty.RELATIVE_TO_STOP_WM,
|
||||
),
|
||||
TimerProperty.SLEEP_TIMER_RELATIVE_TO_STOP: SensorEntityDescription(
|
||||
key=TimerProperty.SLEEP_TIMER_RELATIVE_TO_STOP,
|
||||
translation_key=TimerProperty.SLEEP_TIMER_RELATIVE_TO_STOP,
|
||||
),
|
||||
TimerProperty.ABSOLUTE_TO_START: SensorEntityDescription(
|
||||
key=TimerProperty.ABSOLUTE_TO_START,
|
||||
translation_key=TimerProperty.ABSOLUTE_TO_START,
|
||||
),
|
||||
TimerProperty.ABSOLUTE_TO_STOP: SensorEntityDescription(
|
||||
key=TimerProperty.ABSOLUTE_TO_STOP,
|
||||
translation_key=TimerProperty.ABSOLUTE_TO_STOP,
|
||||
),
|
||||
TimerProperty.REMAIN: SensorEntityDescription(
|
||||
key=TimerProperty.REMAIN,
|
||||
translation_key=TimerProperty.REMAIN,
|
||||
),
|
||||
TimerProperty.TARGET: SensorEntityDescription(
|
||||
key=TimerProperty.TARGET,
|
||||
translation_key=TimerProperty.TARGET,
|
||||
),
|
||||
TimerProperty.RUNNING: SensorEntityDescription(
|
||||
key=TimerProperty.RUNNING,
|
||||
translation_key=TimerProperty.RUNNING,
|
||||
),
|
||||
TimerProperty.TOTAL: SensorEntityDescription(
|
||||
key=TimerProperty.TOTAL,
|
||||
translation_key=TimerProperty.TOTAL,
|
||||
),
|
||||
TimerProperty.LIGHT_START: SensorEntityDescription(
|
||||
key=TimerProperty.LIGHT_START,
|
||||
translation_key=TimerProperty.LIGHT_START,
|
||||
),
|
||||
ThinQProperty.ELAPSED_DAY_STATE: SensorEntityDescription(
|
||||
key=ThinQProperty.ELAPSED_DAY_STATE,
|
||||
native_unit_of_measurement=UnitOfTime.DAYS,
|
||||
translation_key=ThinQProperty.ELAPSED_DAY_STATE,
|
||||
),
|
||||
ThinQProperty.ELAPSED_DAY_TOTAL: SensorEntityDescription(
|
||||
key=ThinQProperty.ELAPSED_DAY_TOTAL,
|
||||
native_unit_of_measurement=UnitOfTime.DAYS,
|
||||
translation_key=ThinQProperty.ELAPSED_DAY_TOTAL,
|
||||
),
|
||||
}
|
||||
|
||||
WASHER_SENSORS: tuple[SensorEntityDescription, ...] = (
|
||||
RUN_STATE_SENSOR_DESC[ThinQProperty.CURRENT_STATE],
|
||||
TIMER_SENSOR_DESC[TimerProperty.RELATIVE_TO_START_WM],
|
||||
TIMER_SENSOR_DESC[TimerProperty.RELATIVE_TO_STOP_WM],
|
||||
TIMER_SENSOR_DESC[TimerProperty.REMAIN],
|
||||
TIMER_SENSOR_DESC[TimerProperty.TOTAL],
|
||||
)
|
||||
DEVICE_TYPE_SENSOR_MAP: dict[DeviceType, tuple[SensorEntityDescription, ...]] = {
|
||||
DeviceType.AIR_CONDITIONER: (
|
||||
@@ -332,9 +268,6 @@ DEVICE_TYPE_SENSOR_MAP: dict[DeviceType, tuple[SensorEntityDescription, ...]] =
|
||||
AIR_QUALITY_SENSOR_DESC[ThinQProperty.ODOR_LEVEL],
|
||||
AIR_QUALITY_SENSOR_DESC[ThinQProperty.TOTAL_POLLUTION_LEVEL],
|
||||
FILTER_INFO_SENSOR_DESC[ThinQProperty.FILTER_LIFETIME],
|
||||
TIMER_SENSOR_DESC[TimerProperty.RELATIVE_TO_START],
|
||||
TIMER_SENSOR_DESC[TimerProperty.RELATIVE_TO_STOP],
|
||||
TIMER_SENSOR_DESC[TimerProperty.SLEEP_TIMER_RELATIVE_TO_STOP],
|
||||
),
|
||||
DeviceType.AIR_PURIFIER_FAN: (
|
||||
AIR_QUALITY_SENSOR_DESC[ThinQProperty.PM1],
|
||||
@@ -345,7 +278,6 @@ DEVICE_TYPE_SENSOR_MAP: dict[DeviceType, tuple[SensorEntityDescription, ...]] =
|
||||
AIR_QUALITY_SENSOR_DESC[ThinQProperty.MONITORING_ENABLED],
|
||||
AIR_QUALITY_SENSOR_DESC[ThinQProperty.ODOR_LEVEL],
|
||||
AIR_QUALITY_SENSOR_DESC[ThinQProperty.TOTAL_POLLUTION_LEVEL],
|
||||
TIMER_SENSOR_DESC[TimerProperty.SLEEP_TIMER_RELATIVE_TO_STOP],
|
||||
),
|
||||
DeviceType.AIR_PURIFIER: (
|
||||
AIR_QUALITY_SENSOR_DESC[ThinQProperty.PM1],
|
||||
@@ -361,7 +293,6 @@ DEVICE_TYPE_SENSOR_MAP: dict[DeviceType, tuple[SensorEntityDescription, ...]] =
|
||||
DeviceType.COOKTOP: (
|
||||
RUN_STATE_SENSOR_DESC[ThinQProperty.CURRENT_STATE],
|
||||
POWER_SENSOR_DESC[ThinQProperty.POWER_LEVEL],
|
||||
TIMER_SENSOR_DESC[TimerProperty.REMAIN],
|
||||
),
|
||||
DeviceType.DEHUMIDIFIER: (
|
||||
JOB_MODE_SENSOR_DESC[ThinQProperty.CURRENT_JOB_MODE],
|
||||
@@ -372,9 +303,6 @@ DEVICE_TYPE_SENSOR_MAP: dict[DeviceType, tuple[SensorEntityDescription, ...]] =
|
||||
PREFERENCE_SENSOR_DESC[ThinQProperty.RINSE_LEVEL],
|
||||
PREFERENCE_SENSOR_DESC[ThinQProperty.SOFTENING_LEVEL],
|
||||
RUN_STATE_SENSOR_DESC[ThinQProperty.CURRENT_STATE],
|
||||
TIMER_SENSOR_DESC[TimerProperty.RELATIVE_TO_START_WM],
|
||||
TIMER_SENSOR_DESC[TimerProperty.REMAIN],
|
||||
TIMER_SENSOR_DESC[TimerProperty.TOTAL],
|
||||
),
|
||||
DeviceType.DRYER: WASHER_SENSORS,
|
||||
DeviceType.HOME_BREW: (
|
||||
@@ -385,10 +313,7 @@ DEVICE_TYPE_SENSOR_MAP: dict[DeviceType, tuple[SensorEntityDescription, ...]] =
|
||||
RECIPE_SENSOR_DESC[ThinQProperty.FLAVOR_INFO],
|
||||
RECIPE_SENSOR_DESC[ThinQProperty.BEER_REMAIN],
|
||||
RUN_STATE_SENSOR_DESC[ThinQProperty.CURRENT_STATE],
|
||||
TIMER_SENSOR_DESC[ThinQProperty.ELAPSED_DAY_STATE],
|
||||
TIMER_SENSOR_DESC[ThinQProperty.ELAPSED_DAY_TOTAL],
|
||||
),
|
||||
DeviceType.HOOD: (TIMER_SENSOR_DESC[TimerProperty.REMAIN],),
|
||||
DeviceType.HUMIDIFIER: (
|
||||
AIR_QUALITY_SENSOR_DESC[ThinQProperty.PM1],
|
||||
AIR_QUALITY_SENSOR_DESC[ThinQProperty.PM2],
|
||||
@@ -397,9 +322,6 @@ DEVICE_TYPE_SENSOR_MAP: dict[DeviceType, tuple[SensorEntityDescription, ...]] =
|
||||
AIR_QUALITY_SENSOR_DESC[ThinQProperty.TEMPERATURE],
|
||||
AIR_QUALITY_SENSOR_DESC[ThinQProperty.MONITORING_ENABLED],
|
||||
AIR_QUALITY_SENSOR_DESC[ThinQProperty.TOTAL_POLLUTION_LEVEL],
|
||||
TIMER_SENSOR_DESC[TimerProperty.ABSOLUTE_TO_START],
|
||||
TIMER_SENSOR_DESC[TimerProperty.ABSOLUTE_TO_STOP],
|
||||
TIMER_SENSOR_DESC[TimerProperty.SLEEP_TIMER_RELATIVE_TO_STOP],
|
||||
),
|
||||
DeviceType.KIMCHI_REFRIGERATOR: (
|
||||
REFRIGERATION_SENSOR_DESC[ThinQProperty.FRESH_AIR_FILTER],
|
||||
@@ -408,15 +330,10 @@ DEVICE_TYPE_SENSOR_MAP: dict[DeviceType, tuple[SensorEntityDescription, ...]] =
|
||||
translation_key=ThinQProperty.TARGET_TEMPERATURE,
|
||||
),
|
||||
),
|
||||
DeviceType.MICROWAVE_OVEN: (
|
||||
RUN_STATE_SENSOR_DESC[ThinQProperty.CURRENT_STATE],
|
||||
TIMER_SENSOR_DESC[TimerProperty.REMAIN],
|
||||
),
|
||||
DeviceType.MICROWAVE_OVEN: (RUN_STATE_SENSOR_DESC[ThinQProperty.CURRENT_STATE],),
|
||||
DeviceType.OVEN: (
|
||||
RUN_STATE_SENSOR_DESC[ThinQProperty.CURRENT_STATE],
|
||||
TEMPERATURE_SENSOR_DESC[ThinQProperty.TARGET_TEMPERATURE],
|
||||
TIMER_SENSOR_DESC[TimerProperty.REMAIN],
|
||||
TIMER_SENSOR_DESC[TimerProperty.TARGET],
|
||||
),
|
||||
DeviceType.PLANT_CULTIVATOR: (
|
||||
LIGHT_SENSOR_DESC[ThinQProperty.BRIGHTNESS],
|
||||
@@ -427,7 +344,6 @@ DEVICE_TYPE_SENSOR_MAP: dict[DeviceType, tuple[SensorEntityDescription, ...]] =
|
||||
TEMPERATURE_SENSOR_DESC[ThinQProperty.DAY_TARGET_TEMPERATURE],
|
||||
TEMPERATURE_SENSOR_DESC[ThinQProperty.NIGHT_TARGET_TEMPERATURE],
|
||||
TEMPERATURE_SENSOR_DESC[ThinQProperty.TEMPERATURE_STATE],
|
||||
TIMER_SENSOR_DESC[TimerProperty.LIGHT_START],
|
||||
),
|
||||
DeviceType.REFRIGERATOR: (
|
||||
REFRIGERATION_SENSOR_DESC[ThinQProperty.FRESH_AIR_FILTER],
|
||||
@@ -436,7 +352,6 @@ DEVICE_TYPE_SENSOR_MAP: dict[DeviceType, tuple[SensorEntityDescription, ...]] =
|
||||
DeviceType.ROBOT_CLEANER: (
|
||||
RUN_STATE_SENSOR_DESC[ThinQProperty.CURRENT_STATE],
|
||||
JOB_MODE_SENSOR_DESC[ThinQProperty.CURRENT_JOB_MODE],
|
||||
TIMER_SENSOR_DESC[TimerProperty.RUNNING],
|
||||
),
|
||||
DeviceType.STICK_CLEANER: (
|
||||
BATTERY_SENSOR_DESC[ThinQProperty.BATTERY_PERCENT],
|
||||
|
||||
@@ -8,6 +8,6 @@
|
||||
"iot_class": "calculated",
|
||||
"loggers": ["yt_dlp"],
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["yt-dlp==2024.11.04"],
|
||||
"requirements": ["yt-dlp[default]==2024.11.04"],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
||||
@@ -24,8 +24,12 @@ MAX_HISTORY_SECONDS = 60 * 60 # 1 hour
|
||||
MODEL_NAMES = [ # https://ollama.com/library
|
||||
"alfred",
|
||||
"all-minilm",
|
||||
"aya-expanse",
|
||||
"aya",
|
||||
"bakllava",
|
||||
"bespoke-minicheck",
|
||||
"bge-large",
|
||||
"bge-m3",
|
||||
"codebooga",
|
||||
"codegeex4",
|
||||
"codegemma",
|
||||
@@ -33,18 +37,19 @@ MODEL_NAMES = [ # https://ollama.com/library
|
||||
"codeqwen",
|
||||
"codestral",
|
||||
"codeup",
|
||||
"command-r",
|
||||
"command-r-plus",
|
||||
"command-r",
|
||||
"dbrx",
|
||||
"deepseek-coder",
|
||||
"deepseek-coder-v2",
|
||||
"deepseek-coder",
|
||||
"deepseek-llm",
|
||||
"deepseek-v2.5",
|
||||
"deepseek-v2",
|
||||
"dolphincoder",
|
||||
"dolphin-llama3",
|
||||
"dolphin-mistral",
|
||||
"dolphin-mixtral",
|
||||
"dolphin-phi",
|
||||
"dolphincoder",
|
||||
"duckdb-nsql",
|
||||
"everythinglm",
|
||||
"falcon",
|
||||
@@ -55,74 +60,97 @@ MODEL_NAMES = [ # https://ollama.com/library
|
||||
"glm4",
|
||||
"goliath",
|
||||
"granite-code",
|
||||
"granite3-dense",
|
||||
"granite3-guardian" "granite3-moe",
|
||||
"hermes3",
|
||||
"internlm2",
|
||||
"llama2",
|
||||
"llama-guard3",
|
||||
"llama-pro",
|
||||
"llama2-chinese",
|
||||
"llama2-uncensored",
|
||||
"llama3",
|
||||
"llama2",
|
||||
"llama3-chatqa",
|
||||
"llama3-gradient",
|
||||
"llama3-groq-tool-use",
|
||||
"llama-pro",
|
||||
"llava",
|
||||
"llama3.1",
|
||||
"llama3.2",
|
||||
"llama3",
|
||||
"llava-llama3",
|
||||
"llava-phi3",
|
||||
"llava",
|
||||
"magicoder",
|
||||
"mathstral",
|
||||
"meditron",
|
||||
"medllama2",
|
||||
"megadolphin",
|
||||
"mistral",
|
||||
"mistrallite",
|
||||
"minicpm-v",
|
||||
"mistral-large",
|
||||
"mistral-nemo",
|
||||
"mistral-openorca",
|
||||
"mistral-small",
|
||||
"mistral",
|
||||
"mistrallite",
|
||||
"mixtral",
|
||||
"moondream",
|
||||
"mxbai-embed-large",
|
||||
"nemotron-mini",
|
||||
"nemotron",
|
||||
"neural-chat",
|
||||
"nexusraven",
|
||||
"nomic-embed-text",
|
||||
"notus",
|
||||
"notux",
|
||||
"nous-hermes",
|
||||
"nous-hermes2",
|
||||
"nous-hermes2-mixtral",
|
||||
"nous-hermes2",
|
||||
"nuextract",
|
||||
"open-orca-platypus2",
|
||||
"openchat",
|
||||
"openhermes",
|
||||
"open-orca-platypus2",
|
||||
"orca2",
|
||||
"orca-mini",
|
||||
"orca2",
|
||||
"paraphrase-multilingual",
|
||||
"phi",
|
||||
"phi3.5",
|
||||
"phi3",
|
||||
"phind-codellama",
|
||||
"qwen",
|
||||
"qwen2-math",
|
||||
"qwen2.5-coder",
|
||||
"qwen2.5",
|
||||
"qwen2",
|
||||
"reader-lm",
|
||||
"reflection",
|
||||
"samantha-mistral",
|
||||
"shieldgemma",
|
||||
"smollm",
|
||||
"smollm2",
|
||||
"snowflake-arctic-embed",
|
||||
"solar-pro",
|
||||
"solar",
|
||||
"sqlcoder",
|
||||
"stable-beluga",
|
||||
"stable-code",
|
||||
"stablelm2",
|
||||
"stablelm-zephyr",
|
||||
"stablelm2",
|
||||
"starcoder",
|
||||
"starcoder2",
|
||||
"starling-lm",
|
||||
"tinydolphin",
|
||||
"tinyllama",
|
||||
"vicuna",
|
||||
"wizard-math",
|
||||
"wizard-vicuna-uncensored",
|
||||
"wizard-vicuna",
|
||||
"wizardcoder",
|
||||
"wizardlm-uncensored",
|
||||
"wizardlm",
|
||||
"wizardlm2",
|
||||
"wizardlm-uncensored",
|
||||
"wizard-math",
|
||||
"wizard-vicuna",
|
||||
"wizard-vicuna-uncensored",
|
||||
"xwinlm",
|
||||
"yarn-llama2",
|
||||
"yarn-mistral",
|
||||
"yi-coder",
|
||||
"yi",
|
||||
"zephyr",
|
||||
]
|
||||
DEFAULT_MODEL = "llama3.1:latest"
|
||||
DEFAULT_MODEL = "llama3.2:latest"
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/palazzetti",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["pypalazzetti==0.1.6"]
|
||||
"requirements": ["pypalazzetti==0.1.10"]
|
||||
}
|
||||
|
||||
@@ -151,7 +151,7 @@ BINARY_SENSORS: dict[str, tuple[TuyaBinarySensorEntityDescription, ...]] = {
|
||||
TuyaBinarySensorEntityDescription(
|
||||
key=DPCode.PRESENCE_STATE,
|
||||
device_class=BinarySensorDeviceClass.OCCUPANCY,
|
||||
on_value="presence",
|
||||
on_value={"presence", "small_move", "large_move"},
|
||||
),
|
||||
),
|
||||
# Formaldehyde Detector
|
||||
|
||||
@@ -6,7 +6,7 @@ import logging
|
||||
|
||||
from homeassistant.components.select import SelectEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_UNIQUE_ID
|
||||
from homeassistant.const import CONF_NAME, CONF_UNIQUE_ID
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device import async_device_info_to_link_from_entity
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
@@ -36,9 +36,9 @@ async def async_setup_entry(
|
||||
)
|
||||
|
||||
tariff_select = TariffSelect(
|
||||
name,
|
||||
tariffs,
|
||||
unique_id,
|
||||
name=name,
|
||||
tariffs=tariffs,
|
||||
unique_id=unique_id,
|
||||
device_info=device_info,
|
||||
)
|
||||
async_add_entities([tariff_select])
|
||||
@@ -62,13 +62,15 @@ async def async_setup_platform(
|
||||
conf_meter_unique_id: str | None = hass.data[DATA_UTILITY][meter].get(
|
||||
CONF_UNIQUE_ID
|
||||
)
|
||||
conf_meter_name = hass.data[DATA_UTILITY][meter].get(CONF_NAME, meter)
|
||||
|
||||
async_add_entities(
|
||||
[
|
||||
TariffSelect(
|
||||
meter,
|
||||
discovery_info[CONF_TARIFFS],
|
||||
conf_meter_unique_id,
|
||||
name=conf_meter_name,
|
||||
tariffs=discovery_info[CONF_TARIFFS],
|
||||
yaml_slug=meter,
|
||||
unique_id=conf_meter_unique_id,
|
||||
)
|
||||
]
|
||||
)
|
||||
@@ -82,12 +84,16 @@ class TariffSelect(SelectEntity, RestoreEntity):
|
||||
def __init__(
|
||||
self,
|
||||
name,
|
||||
tariffs,
|
||||
unique_id,
|
||||
tariffs: list[str],
|
||||
*,
|
||||
yaml_slug: str | None = None,
|
||||
unique_id: str | None = None,
|
||||
device_info: DeviceInfo | None = None,
|
||||
) -> None:
|
||||
"""Initialize a tariff selector."""
|
||||
self._attr_name = name
|
||||
if yaml_slug: # Backwards compatibility with YAML configuration entries
|
||||
self.entity_id = f"select.{yaml_slug}"
|
||||
self._attr_unique_id = unique_id
|
||||
self._attr_device_info = device_info
|
||||
self._current_tariff: str | None = None
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["holidays"],
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["holidays==0.59"]
|
||||
"requirements": ["holidays==0.60"]
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ if TYPE_CHECKING:
|
||||
APPLICATION_NAME: Final = "HomeAssistant"
|
||||
MAJOR_VERSION: Final = 2024
|
||||
MINOR_VERSION: Final = 11
|
||||
PATCH_VERSION: Final = "0b5"
|
||||
PATCH_VERSION: Final = "0b7"
|
||||
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
||||
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
|
||||
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0)
|
||||
|
||||
@@ -33,7 +33,7 @@ habluetooth==3.6.0
|
||||
hass-nabucasa==0.83.0
|
||||
hassil==1.7.4
|
||||
home-assistant-bluetooth==1.13.0
|
||||
home-assistant-frontend==20241104.0
|
||||
home-assistant-frontend==20241105.0
|
||||
home-assistant-intents==2024.10.30
|
||||
httpx==0.27.2
|
||||
ifaddr==0.2.0
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "homeassistant"
|
||||
version = "2024.11.0b5"
|
||||
version = "2024.11.0b7"
|
||||
license = {text = "Apache-2.0"}
|
||||
description = "Open-source home automation platform running on Python 3."
|
||||
readme = "README.rst"
|
||||
|
||||
@@ -572,7 +572,7 @@ beautifulsoup4==4.12.3
|
||||
# beewi-smartclim==0.0.10
|
||||
|
||||
# homeassistant.components.bmw_connected_drive
|
||||
bimmer-connected[china]==0.16.3
|
||||
bimmer-connected[china]==0.16.4
|
||||
|
||||
# homeassistant.components.bizkaibus
|
||||
bizkaibus==0.1.1
|
||||
@@ -1121,10 +1121,10 @@ hole==0.8.0
|
||||
|
||||
# homeassistant.components.holiday
|
||||
# homeassistant.components.workday
|
||||
holidays==0.59
|
||||
holidays==0.60
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20241104.0
|
||||
home-assistant-frontend==20241105.0
|
||||
|
||||
# homeassistant.components.conversation
|
||||
home-assistant-intents==2024.10.30
|
||||
@@ -1904,7 +1904,7 @@ pyevilgenius==2.0.0
|
||||
pyezviz==0.2.1.2
|
||||
|
||||
# homeassistant.components.fibaro
|
||||
pyfibaro==0.7.8
|
||||
pyfibaro==0.8.0
|
||||
|
||||
# homeassistant.components.fido
|
||||
pyfido==2.1.2
|
||||
@@ -2143,7 +2143,7 @@ pyoverkiz==1.14.1
|
||||
pyownet==0.10.0.post1
|
||||
|
||||
# homeassistant.components.palazzetti
|
||||
pypalazzetti==0.1.6
|
||||
pypalazzetti==0.1.10
|
||||
|
||||
# homeassistant.components.elv
|
||||
pypca==0.0.7
|
||||
@@ -3051,7 +3051,7 @@ youless-api==2.1.2
|
||||
youtubeaio==1.1.5
|
||||
|
||||
# homeassistant.components.media_extractor
|
||||
yt-dlp==2024.11.04
|
||||
yt-dlp[default]==2024.11.04
|
||||
|
||||
# homeassistant.components.zamg
|
||||
zamg==0.3.6
|
||||
|
||||
@@ -506,7 +506,7 @@ base36==0.1.1
|
||||
beautifulsoup4==4.12.3
|
||||
|
||||
# homeassistant.components.bmw_connected_drive
|
||||
bimmer-connected[china]==0.16.3
|
||||
bimmer-connected[china]==0.16.4
|
||||
|
||||
# homeassistant.components.eq3btsmart
|
||||
# homeassistant.components.esphome
|
||||
@@ -947,10 +947,10 @@ hole==0.8.0
|
||||
|
||||
# homeassistant.components.holiday
|
||||
# homeassistant.components.workday
|
||||
holidays==0.59
|
||||
holidays==0.60
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20241104.0
|
||||
home-assistant-frontend==20241105.0
|
||||
|
||||
# homeassistant.components.conversation
|
||||
home-assistant-intents==2024.10.30
|
||||
@@ -1533,7 +1533,7 @@ pyevilgenius==2.0.0
|
||||
pyezviz==0.2.1.2
|
||||
|
||||
# homeassistant.components.fibaro
|
||||
pyfibaro==0.7.8
|
||||
pyfibaro==0.8.0
|
||||
|
||||
# homeassistant.components.fido
|
||||
pyfido==2.1.2
|
||||
@@ -1730,7 +1730,7 @@ pyoverkiz==1.14.1
|
||||
pyownet==0.10.0.post1
|
||||
|
||||
# homeassistant.components.palazzetti
|
||||
pypalazzetti==0.1.6
|
||||
pypalazzetti==0.1.10
|
||||
|
||||
# homeassistant.components.lcn
|
||||
pypck==0.7.24
|
||||
@@ -2437,7 +2437,7 @@ youless-api==2.1.2
|
||||
youtubeaio==1.1.5
|
||||
|
||||
# homeassistant.components.media_extractor
|
||||
yt-dlp==2024.11.04
|
||||
yt-dlp[default]==2024.11.04
|
||||
|
||||
# homeassistant.components.zamg
|
||||
zamg==0.3.6
|
||||
|
||||
@@ -4,8 +4,13 @@ from copy import deepcopy
|
||||
from unittest.mock import patch
|
||||
|
||||
from bimmer_connected.api.authentication import MyBMWAuthentication
|
||||
from bimmer_connected.models import MyBMWAPIError, MyBMWAuthError
|
||||
from bimmer_connected.models import (
|
||||
MyBMWAPIError,
|
||||
MyBMWAuthError,
|
||||
MyBMWCaptchaMissingError,
|
||||
)
|
||||
from httpx import RequestError
|
||||
import pytest
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.bmw_connected_drive.config_flow import DOMAIN
|
||||
@@ -311,3 +316,31 @@ async def test_reconfigure_unique_id_abort(hass: HomeAssistant) -> None:
|
||||
assert result2["type"] is FlowResultType.ABORT
|
||||
assert result2["reason"] == "account_mismatch"
|
||||
assert config_entry.data == FIXTURE_COMPLETE_ENTRY
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("bmw_fixture")
|
||||
async def test_captcha_flow_not_set(hass: HomeAssistant) -> None:
|
||||
"""Test the external flow with captcha failing once and succeeding the second time."""
|
||||
|
||||
TEST_REGION = "north_america"
|
||||
|
||||
# Start flow and open form
|
||||
# Start flow and open form
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
# Add login data
|
||||
with patch(
|
||||
"bimmer_connected.api.authentication.MyBMWAuthentication._login_row_na",
|
||||
side_effect=MyBMWCaptchaMissingError(
|
||||
"Missing hCaptcha token for North America login"
|
||||
),
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={**FIXTURE_USER_INPUT, CONF_REGION: TEST_REGION},
|
||||
)
|
||||
assert result["errors"]["base"] == "missing_captcha"
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
"""Test BMW coordinator."""
|
||||
|
||||
from copy import deepcopy
|
||||
from datetime import timedelta
|
||||
from unittest.mock import patch
|
||||
|
||||
from bimmer_connected.models import MyBMWAPIError, MyBMWAuthError
|
||||
from bimmer_connected.models import (
|
||||
MyBMWAPIError,
|
||||
MyBMWAuthError,
|
||||
MyBMWCaptchaMissingError,
|
||||
)
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.bmw_connected_drive import DOMAIN as BMW_DOMAIN
|
||||
from homeassistant.const import CONF_REGION
|
||||
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
from homeassistant.helpers import issue_registry as ir
|
||||
@@ -122,3 +128,38 @@ async def test_init_reauth(
|
||||
f"config_entry_reauth_{BMW_DOMAIN}_{config_entry.entry_id}",
|
||||
)
|
||||
assert reauth_issue.active is True
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("bmw_fixture")
|
||||
async def test_captcha_reauth(
|
||||
hass: HomeAssistant,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
) -> None:
|
||||
"""Test the reauth form."""
|
||||
TEST_REGION = "north_america"
|
||||
|
||||
config_entry_fixure = deepcopy(FIXTURE_CONFIG_ENTRY)
|
||||
config_entry_fixure["data"][CONF_REGION] = TEST_REGION
|
||||
config_entry = MockConfigEntry(**config_entry_fixure)
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
coordinator = config_entry.runtime_data.coordinator
|
||||
|
||||
assert coordinator.last_update_success is True
|
||||
|
||||
freezer.tick(timedelta(minutes=10, seconds=1))
|
||||
with patch(
|
||||
"bimmer_connected.account.MyBMWAccount.get_vehicles",
|
||||
side_effect=MyBMWCaptchaMissingError(
|
||||
"Missing hCaptcha token for North America login"
|
||||
),
|
||||
):
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert coordinator.last_update_success is False
|
||||
assert isinstance(coordinator.last_exception, ConfigEntryAuthFailed) is True
|
||||
assert coordinator.last_exception.translation_key == "missing_captcha"
|
||||
|
||||
@@ -6,6 +6,16 @@ components. Instead call the service directly.
|
||||
|
||||
from unittest.mock import Mock
|
||||
|
||||
from webrtc_models import RTCIceCandidate
|
||||
|
||||
from homeassistant.components.camera import (
|
||||
Camera,
|
||||
CameraWebRTCProvider,
|
||||
WebRTCAnswer,
|
||||
WebRTCSendMessage,
|
||||
)
|
||||
from homeassistant.core import callback
|
||||
|
||||
EMPTY_8_6_JPEG = b"empty_8_6"
|
||||
WEBRTC_ANSWER = "a=sendonly"
|
||||
STREAM_SOURCE = "rtsp://127.0.0.1/stream"
|
||||
@@ -23,3 +33,43 @@ def mock_turbo_jpeg(
|
||||
mocked_turbo_jpeg.scale_with_quality.return_value = EMPTY_8_6_JPEG
|
||||
mocked_turbo_jpeg.encode.return_value = EMPTY_8_6_JPEG
|
||||
return mocked_turbo_jpeg
|
||||
|
||||
|
||||
class SomeTestProvider(CameraWebRTCProvider):
|
||||
"""Test provider."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize the provider."""
|
||||
self._is_supported = True
|
||||
|
||||
@property
|
||||
def domain(self) -> str:
|
||||
"""Return the integration domain of the provider."""
|
||||
return "some_test"
|
||||
|
||||
@callback
|
||||
def async_is_supported(self, stream_source: str) -> bool:
|
||||
"""Determine if the provider supports the stream source."""
|
||||
return self._is_supported
|
||||
|
||||
async def async_handle_async_webrtc_offer(
|
||||
self,
|
||||
camera: Camera,
|
||||
offer_sdp: str,
|
||||
session_id: str,
|
||||
send_message: WebRTCSendMessage,
|
||||
) -> None:
|
||||
"""Handle the WebRTC offer and return the answer via the provided callback.
|
||||
|
||||
Return value determines if the offer was handled successfully.
|
||||
"""
|
||||
send_message(WebRTCAnswer(answer="answer"))
|
||||
|
||||
async def async_on_webrtc_candidate(
|
||||
self, session_id: str, candidate: RTCIceCandidate
|
||||
) -> None:
|
||||
"""Handle the WebRTC candidate."""
|
||||
|
||||
@callback
|
||||
def async_close_session(self, session_id: str) -> None:
|
||||
"""Close the session."""
|
||||
|
||||
@@ -4,6 +4,7 @@ from collections.abc import AsyncGenerator, Generator
|
||||
from unittest.mock import AsyncMock, Mock, PropertyMock, patch
|
||||
|
||||
import pytest
|
||||
from webrtc_models import RTCIceCandidate
|
||||
|
||||
from homeassistant.components import camera
|
||||
from homeassistant.components.camera.const import StreamType
|
||||
@@ -14,7 +15,7 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from .common import STREAM_SOURCE, WEBRTC_ANSWER
|
||||
from .common import STREAM_SOURCE, WEBRTC_ANSWER, SomeTestProvider
|
||||
|
||||
from tests.common import (
|
||||
MockConfigEntry,
|
||||
@@ -155,16 +156,15 @@ def mock_stream_source_fixture() -> Generator[AsyncMock]:
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def mock_camera_webrtc_native_sync_offer(hass: HomeAssistant) -> None:
|
||||
"""Initialize a test camera with native sync WebRTC support."""
|
||||
async def mock_test_webrtc_cameras(hass: HomeAssistant) -> None:
|
||||
"""Initialize a test WebRTC cameras."""
|
||||
|
||||
# Cannot use the fixture mock_camera_web_rtc as it's mocking Camera.async_handle_web_rtc_offer
|
||||
# and native support is checked by verify the function "async_handle_web_rtc_offer" was
|
||||
# overwritten(implemented) or not
|
||||
class MockCamera(camera.Camera):
|
||||
"""Mock Camera Entity."""
|
||||
class BaseCamera(camera.Camera):
|
||||
"""Base Camera."""
|
||||
|
||||
_attr_name = "Test"
|
||||
_attr_supported_features: camera.CameraEntityFeature = (
|
||||
camera.CameraEntityFeature.STREAM
|
||||
)
|
||||
@@ -173,9 +173,30 @@ async def mock_camera_webrtc_native_sync_offer(hass: HomeAssistant) -> None:
|
||||
async def stream_source(self) -> str | None:
|
||||
return STREAM_SOURCE
|
||||
|
||||
class SyncCamera(BaseCamera):
|
||||
"""Mock Camera with native sync WebRTC support."""
|
||||
|
||||
_attr_name = "Sync"
|
||||
|
||||
async def async_handle_web_rtc_offer(self, offer_sdp: str) -> str | None:
|
||||
return WEBRTC_ANSWER
|
||||
|
||||
class AsyncCamera(BaseCamera):
|
||||
"""Mock Camera with native async WebRTC support."""
|
||||
|
||||
_attr_name = "Async"
|
||||
|
||||
async def async_handle_async_webrtc_offer(
|
||||
self, offer_sdp: str, session_id: str, send_message: WebRTCSendMessage
|
||||
) -> None:
|
||||
send_message(WebRTCAnswer(WEBRTC_ANSWER))
|
||||
|
||||
async def async_on_webrtc_candidate(
|
||||
self, session_id: str, candidate: RTCIceCandidate
|
||||
) -> None:
|
||||
"""Handle a WebRTC candidate."""
|
||||
# Do nothing
|
||||
|
||||
domain = "test"
|
||||
|
||||
entry = MockConfigEntry(domain=domain)
|
||||
@@ -208,10 +229,24 @@ async def mock_camera_webrtc_native_sync_offer(hass: HomeAssistant) -> None:
|
||||
),
|
||||
)
|
||||
setup_test_component_platform(
|
||||
hass, camera.DOMAIN, [MockCamera()], from_config_entry=True
|
||||
hass, camera.DOMAIN, [SyncCamera(), AsyncCamera()], from_config_entry=True
|
||||
)
|
||||
mock_platform(hass, f"{domain}.config_flow", Mock())
|
||||
|
||||
with mock_config_flow(domain, ConfigFlow):
|
||||
assert await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def register_test_provider(
|
||||
hass: HomeAssistant,
|
||||
) -> AsyncGenerator[SomeTestProvider]:
|
||||
"""Add WebRTC test provider."""
|
||||
await async_setup_component(hass, "camera", {})
|
||||
|
||||
provider = SomeTestProvider()
|
||||
unsub = camera.async_register_webrtc_provider(hass, provider)
|
||||
await hass.async_block_till_done()
|
||||
yield provider
|
||||
unsub()
|
||||
|
||||
@@ -979,7 +979,7 @@ async def test_camera_capabilities_hls(
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_camera_webrtc_native_sync_offer")
|
||||
@pytest.mark.usefixtures("mock_test_webrtc_cameras")
|
||||
async def test_camera_capabilities_webrtc(
|
||||
hass: HomeAssistant,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
@@ -987,5 +987,21 @@ async def test_camera_capabilities_webrtc(
|
||||
"""Test WebRTC camera capabilities."""
|
||||
|
||||
await _test_capabilities(
|
||||
hass, hass_ws_client, "camera.test", {StreamType.WEB_RTC}, {StreamType.WEB_RTC}
|
||||
hass, hass_ws_client, "camera.sync", {StreamType.WEB_RTC}, {StreamType.WEB_RTC}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("entity_id", "expect_native_async_webrtc"),
|
||||
[("camera.sync", False), ("camera.async", True)],
|
||||
)
|
||||
@pytest.mark.usefixtures("mock_test_webrtc_cameras", "register_test_provider")
|
||||
async def test_webrtc_provider_not_added_for_native_webrtc(
|
||||
hass: HomeAssistant, entity_id: str, expect_native_async_webrtc: bool
|
||||
) -> None:
|
||||
"""Test that a WebRTC provider is not added to a camera when the camera has native WebRTC support."""
|
||||
camera_obj = get_camera_from_entity_id(hass, entity_id)
|
||||
assert camera_obj
|
||||
assert camera_obj._webrtc_provider is None
|
||||
assert camera_obj._supports_native_sync_webrtc is not expect_native_async_webrtc
|
||||
assert camera_obj._supports_native_async_webrtc is expect_native_async_webrtc
|
||||
|
||||
@@ -34,7 +34,7 @@ from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import issue_registry as ir
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from .common import STREAM_SOURCE, WEBRTC_ANSWER
|
||||
from .common import STREAM_SOURCE, WEBRTC_ANSWER, SomeTestProvider
|
||||
|
||||
from tests.common import (
|
||||
MockConfigEntry,
|
||||
@@ -51,46 +51,6 @@ HLS_STREAM_SOURCE = "http://127.0.0.1/example.m3u"
|
||||
TEST_INTEGRATION_DOMAIN = "test"
|
||||
|
||||
|
||||
class SomeTestProvider(CameraWebRTCProvider):
|
||||
"""Test provider."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize the provider."""
|
||||
self._is_supported = True
|
||||
|
||||
@property
|
||||
def domain(self) -> str:
|
||||
"""Return the integration domain of the provider."""
|
||||
return "some_test"
|
||||
|
||||
@callback
|
||||
def async_is_supported(self, stream_source: str) -> bool:
|
||||
"""Determine if the provider supports the stream source."""
|
||||
return self._is_supported
|
||||
|
||||
async def async_handle_async_webrtc_offer(
|
||||
self,
|
||||
camera: Camera,
|
||||
offer_sdp: str,
|
||||
session_id: str,
|
||||
send_message: WebRTCSendMessage,
|
||||
) -> None:
|
||||
"""Handle the WebRTC offer and return the answer via the provided callback.
|
||||
|
||||
Return value determines if the offer was handled successfully.
|
||||
"""
|
||||
send_message(WebRTCAnswer(answer="answer"))
|
||||
|
||||
async def async_on_webrtc_candidate(
|
||||
self, session_id: str, candidate: RTCIceCandidate
|
||||
) -> None:
|
||||
"""Handle the WebRTC candidate."""
|
||||
|
||||
@callback
|
||||
def async_close_session(self, session_id: str) -> None:
|
||||
"""Close the session."""
|
||||
|
||||
|
||||
class Go2RTCProvider(SomeTestProvider):
|
||||
"""go2rtc provider."""
|
||||
|
||||
@@ -179,20 +139,6 @@ async def init_test_integration(
|
||||
return test_camera
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def register_test_provider(
|
||||
hass: HomeAssistant,
|
||||
) -> AsyncGenerator[SomeTestProvider]:
|
||||
"""Add WebRTC test provider."""
|
||||
await async_setup_component(hass, "camera", {})
|
||||
|
||||
provider = SomeTestProvider()
|
||||
unsub = async_register_webrtc_provider(hass, provider)
|
||||
await hass.async_block_till_done()
|
||||
yield provider
|
||||
unsub()
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_camera", "mock_stream", "mock_stream_source")
|
||||
async def test_async_register_webrtc_provider(
|
||||
hass: HomeAssistant,
|
||||
@@ -393,7 +339,7 @@ async def test_ws_get_client_config(
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_camera_webrtc_native_sync_offer")
|
||||
@pytest.mark.usefixtures("mock_test_webrtc_cameras")
|
||||
async def test_ws_get_client_config_sync_offer(
|
||||
hass: HomeAssistant, hass_ws_client: WebSocketGenerator
|
||||
) -> None:
|
||||
@@ -403,7 +349,7 @@ async def test_ws_get_client_config_sync_offer(
|
||||
|
||||
client = await hass_ws_client(hass)
|
||||
await client.send_json_auto_id(
|
||||
{"type": "camera/webrtc/get_client_config", "entity_id": "camera.test"}
|
||||
{"type": "camera/webrtc/get_client_config", "entity_id": "camera.sync"}
|
||||
)
|
||||
msg = await client.receive_json()
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ def rest_client() -> Generator[AsyncMock]:
|
||||
client = mock_client.return_value
|
||||
client.streams = streams = Mock(spec_set=_StreamClient)
|
||||
streams.list.return_value = {}
|
||||
client.validate_server_version = AsyncMock()
|
||||
client.webrtc = Mock(spec_set=_WebRTCClient)
|
||||
yield client
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ from unittest.mock import AsyncMock, Mock, patch
|
||||
|
||||
from aiohttp.client_exceptions import ClientConnectionError, ServerConnectionError
|
||||
from go2rtc_client import Stream
|
||||
from go2rtc_client.exceptions import Go2RtcClientError
|
||||
from go2rtc_client.exceptions import Go2RtcClientError, Go2RtcVersionError
|
||||
from go2rtc_client.models import Producer
|
||||
from go2rtc_client.ws import (
|
||||
ReceiveMessages,
|
||||
@@ -494,6 +494,8 @@ ERR_CONNECT = "Could not connect to go2rtc instance"
|
||||
ERR_CONNECT_RETRY = (
|
||||
"Could not connect to go2rtc instance on http://localhost:1984/; Retrying"
|
||||
)
|
||||
ERR_START_SERVER = "Could not start go2rtc server"
|
||||
ERR_UNSUPPORTED_VERSION = "The go2rtc server version is not supported"
|
||||
_INVALID_CONFIG = "Invalid config for 'go2rtc': "
|
||||
ERR_INVALID_URL = _INVALID_CONFIG + "invalid url"
|
||||
ERR_EXCLUSIVE = _INVALID_CONFIG + DEBUG_UI_URL_MESSAGE
|
||||
@@ -526,8 +528,10 @@ async def test_non_user_setup_with_error(
|
||||
("config", "go2rtc_binary", "is_docker_env", "expected_log_message"),
|
||||
[
|
||||
({DEFAULT_CONFIG_DOMAIN: {}}, None, True, ERR_BINARY_NOT_FOUND),
|
||||
({DEFAULT_CONFIG_DOMAIN: {}}, "/usr/bin/go2rtc", True, ERR_START_SERVER),
|
||||
({DOMAIN: {}}, None, False, ERR_URL_REQUIRED),
|
||||
({DOMAIN: {}}, None, True, ERR_BINARY_NOT_FOUND),
|
||||
({DOMAIN: {}}, "/usr/bin/go2rtc", True, ERR_START_SERVER),
|
||||
({DOMAIN: {CONF_URL: "invalid"}}, None, True, ERR_INVALID_URL),
|
||||
(
|
||||
{DOMAIN: {CONF_URL: "http://localhost:1984", CONF_DEBUG_UI: True}},
|
||||
@@ -559,8 +563,6 @@ async def test_setup_with_setup_error(
|
||||
@pytest.mark.parametrize(
|
||||
("config", "go2rtc_binary", "is_docker_env", "expected_log_message"),
|
||||
[
|
||||
({DEFAULT_CONFIG_DOMAIN: {}}, "/usr/bin/go2rtc", True, ERR_CONNECT),
|
||||
({DOMAIN: {}}, "/usr/bin/go2rtc", True, ERR_CONNECT),
|
||||
({DOMAIN: {CONF_URL: "http://localhost:1984/"}}, None, True, ERR_CONNECT),
|
||||
],
|
||||
)
|
||||
@@ -584,7 +586,7 @@ async def test_setup_with_setup_entry_error(
|
||||
assert expected_log_message in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.parametrize("config", [{DOMAIN: {}}, {DEFAULT_CONFIG_DOMAIN: {}}])
|
||||
@pytest.mark.parametrize("config", [{DOMAIN: {CONF_URL: "http://localhost:1984/"}}])
|
||||
@pytest.mark.parametrize(
|
||||
("cause", "expected_config_entry_state", "expected_log_message"),
|
||||
[
|
||||
@@ -598,7 +600,7 @@ async def test_setup_with_setup_entry_error(
|
||||
@pytest.mark.usefixtures(
|
||||
"mock_get_binary", "mock_go2rtc_entry", "mock_is_docker_env", "server"
|
||||
)
|
||||
async def test_setup_with_retryable_setup_entry_error(
|
||||
async def test_setup_with_retryable_setup_entry_error_custom_server(
|
||||
hass: HomeAssistant,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
rest_client: AsyncMock,
|
||||
@@ -610,7 +612,78 @@ async def test_setup_with_retryable_setup_entry_error(
|
||||
"""Test setup integration entry fails."""
|
||||
go2rtc_error = Go2RtcClientError()
|
||||
go2rtc_error.__cause__ = cause
|
||||
rest_client.streams.list.side_effect = go2rtc_error
|
||||
rest_client.validate_server_version.side_effect = go2rtc_error
|
||||
assert await async_setup_component(hass, DOMAIN, config)
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
config_entries = hass.config_entries.async_entries(DOMAIN)
|
||||
assert len(config_entries) == 1
|
||||
assert config_entries[0].state == expected_config_entry_state
|
||||
assert expected_log_message in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.parametrize("config", [{DOMAIN: {}}, {DEFAULT_CONFIG_DOMAIN: {}}])
|
||||
@pytest.mark.parametrize(
|
||||
("cause", "expected_config_entry_state", "expected_log_message"),
|
||||
[
|
||||
(ClientConnectionError(), ConfigEntryState.NOT_LOADED, ERR_START_SERVER),
|
||||
(ServerConnectionError(), ConfigEntryState.NOT_LOADED, ERR_START_SERVER),
|
||||
(None, ConfigEntryState.NOT_LOADED, ERR_START_SERVER),
|
||||
(Exception(), ConfigEntryState.NOT_LOADED, ERR_START_SERVER),
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize("has_go2rtc_entry", [True, False])
|
||||
@pytest.mark.usefixtures(
|
||||
"mock_get_binary", "mock_go2rtc_entry", "mock_is_docker_env", "server"
|
||||
)
|
||||
async def test_setup_with_retryable_setup_entry_error_default_server(
|
||||
hass: HomeAssistant,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
rest_client: AsyncMock,
|
||||
has_go2rtc_entry: bool,
|
||||
config: ConfigType,
|
||||
cause: Exception,
|
||||
expected_config_entry_state: ConfigEntryState,
|
||||
expected_log_message: str,
|
||||
) -> None:
|
||||
"""Test setup integration entry fails."""
|
||||
go2rtc_error = Go2RtcClientError()
|
||||
go2rtc_error.__cause__ = cause
|
||||
rest_client.validate_server_version.side_effect = go2rtc_error
|
||||
assert not await async_setup_component(hass, DOMAIN, config)
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
config_entries = hass.config_entries.async_entries(DOMAIN)
|
||||
assert len(config_entries) == has_go2rtc_entry
|
||||
for config_entry in config_entries:
|
||||
assert config_entry.state == expected_config_entry_state
|
||||
assert expected_log_message in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.parametrize("config", [{DOMAIN: {}}, {DEFAULT_CONFIG_DOMAIN: {}}])
|
||||
@pytest.mark.parametrize(
|
||||
("go2rtc_error", "expected_config_entry_state", "expected_log_message"),
|
||||
[
|
||||
(
|
||||
Go2RtcVersionError("1.9.4", "1.9.5", "2.0.0"),
|
||||
ConfigEntryState.SETUP_RETRY,
|
||||
ERR_UNSUPPORTED_VERSION,
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize("has_go2rtc_entry", [True, False])
|
||||
@pytest.mark.usefixtures(
|
||||
"mock_get_binary", "mock_go2rtc_entry", "mock_is_docker_env", "server"
|
||||
)
|
||||
async def test_setup_with_version_error(
|
||||
hass: HomeAssistant,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
rest_client: AsyncMock,
|
||||
config: ConfigType,
|
||||
go2rtc_error: Exception,
|
||||
expected_config_entry_state: ConfigEntryState,
|
||||
expected_log_message: str,
|
||||
) -> None:
|
||||
"""Test setup integration entry fails."""
|
||||
rest_client.validate_server_version.side_effect = [None, go2rtc_error]
|
||||
assert await async_setup_component(hass, DOMAIN, config)
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
config_entries = hass.config_entries.async_entries(DOMAIN)
|
||||
|
||||
@@ -38,6 +38,42 @@ def mock_tempfile() -> Generator[Mock]:
|
||||
yield file
|
||||
|
||||
|
||||
def _assert_server_output_logged(
|
||||
server_stdout: list[str],
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
loglevel: int,
|
||||
expect_logged: bool,
|
||||
) -> None:
|
||||
"""Check server stdout was logged."""
|
||||
for entry in server_stdout:
|
||||
assert (
|
||||
(
|
||||
"homeassistant.components.go2rtc.server",
|
||||
loglevel,
|
||||
entry,
|
||||
)
|
||||
in caplog.record_tuples
|
||||
) is expect_logged
|
||||
|
||||
|
||||
def assert_server_output_logged(
|
||||
server_stdout: list[str],
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
loglevel: int,
|
||||
) -> None:
|
||||
"""Check server stdout was logged."""
|
||||
_assert_server_output_logged(server_stdout, caplog, loglevel, True)
|
||||
|
||||
|
||||
def assert_server_output_not_logged(
|
||||
server_stdout: list[str],
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
loglevel: int,
|
||||
) -> None:
|
||||
"""Check server stdout was logged."""
|
||||
_assert_server_output_logged(server_stdout, caplog, loglevel, False)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("enable_ui", "api_ip"),
|
||||
[
|
||||
@@ -47,6 +83,7 @@ def mock_tempfile() -> Generator[Mock]:
|
||||
)
|
||||
async def test_server_run_success(
|
||||
mock_create_subprocess: AsyncMock,
|
||||
rest_client: AsyncMock,
|
||||
server_stdout: list[str],
|
||||
server: Server,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
@@ -70,32 +107,31 @@ async def test_server_run_success(
|
||||
mock_tempfile.write.assert_called_once_with(
|
||||
f"""
|
||||
api:
|
||||
listen: "{api_ip}:1984"
|
||||
listen: "{api_ip}:11984"
|
||||
|
||||
rtsp:
|
||||
# ffmpeg needs rtsp for opus audio transcoding
|
||||
listen: "127.0.0.1:8554"
|
||||
listen: "127.0.0.1:18554"
|
||||
|
||||
webrtc:
|
||||
listen: ":18555/tcp"
|
||||
ice_servers: []
|
||||
""".encode()
|
||||
)
|
||||
|
||||
# Check that server read the log lines
|
||||
for entry in server_stdout:
|
||||
assert (
|
||||
"homeassistant.components.go2rtc.server",
|
||||
logging.DEBUG,
|
||||
entry,
|
||||
) in caplog.record_tuples
|
||||
# Verify go2rtc binary stdout was logged with debug level
|
||||
assert_server_output_logged(server_stdout, caplog, logging.DEBUG)
|
||||
|
||||
await server.stop()
|
||||
mock_create_subprocess.return_value.terminate.assert_called_once()
|
||||
|
||||
# Verify go2rtc binary stdout was not logged with warning level
|
||||
assert_server_output_not_logged(server_stdout, caplog, logging.WARNING)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_tempfile")
|
||||
async def test_server_timeout_on_stop(
|
||||
mock_create_subprocess: MagicMock, server: Server
|
||||
mock_create_subprocess: MagicMock, rest_client: AsyncMock, server: Server
|
||||
) -> None:
|
||||
"""Test server run where the process takes too long to terminate."""
|
||||
# Start server thread
|
||||
@@ -138,13 +174,9 @@ async def test_server_failed_to_start(
|
||||
):
|
||||
await server.start()
|
||||
|
||||
# Verify go2rtc binary stdout was logged
|
||||
for entry in server_stdout:
|
||||
assert (
|
||||
"homeassistant.components.go2rtc.server",
|
||||
logging.DEBUG,
|
||||
entry,
|
||||
) in caplog.record_tuples
|
||||
# Verify go2rtc binary stdout was logged with debug and warning level
|
||||
assert_server_output_logged(server_stdout, caplog, logging.DEBUG)
|
||||
assert_server_output_logged(server_stdout, caplog, logging.WARNING)
|
||||
|
||||
assert (
|
||||
"homeassistant.components.go2rtc.server",
|
||||
@@ -163,12 +195,83 @@ async def test_server_failed_to_start(
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("server_stdout", "expected_loglevel"),
|
||||
[
|
||||
(
|
||||
[
|
||||
"09:00:03.466 TRC [api] register path path=/",
|
||||
"09:00:03.466 DBG build vcs.time=2024-10-28T19:47:55Z version=go1.23.2",
|
||||
"09:00:03.466 INF go2rtc platform=linux/amd64 revision=780f378 version=1.9.5",
|
||||
"09:00:03.467 INF [api] listen addr=127.0.0.1:1984",
|
||||
"09:00:03.466 WRN warning message",
|
||||
'09:00:03.466 ERR [api] listen error="listen tcp 127.0.0.1:11984: bind: address already in use"',
|
||||
"09:00:03.466 FTL fatal message",
|
||||
"09:00:03.466 PNC panic message",
|
||||
"exit with signal: interrupt", # Example of stderr write
|
||||
],
|
||||
[
|
||||
logging.DEBUG,
|
||||
logging.DEBUG,
|
||||
logging.DEBUG,
|
||||
logging.DEBUG,
|
||||
logging.WARNING,
|
||||
logging.WARNING,
|
||||
logging.ERROR,
|
||||
logging.ERROR,
|
||||
logging.WARNING,
|
||||
],
|
||||
)
|
||||
],
|
||||
)
|
||||
@patch("homeassistant.components.go2rtc.server._RESPAWN_COOLDOWN", 0)
|
||||
async def test_log_level_mapping(
|
||||
hass: HomeAssistant,
|
||||
mock_create_subprocess: MagicMock,
|
||||
server_stdout: list[str],
|
||||
rest_client: AsyncMock,
|
||||
server: Server,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
expected_loglevel: list[int],
|
||||
) -> None:
|
||||
"""Log level mapping."""
|
||||
evt = asyncio.Event()
|
||||
|
||||
async def wait_event() -> None:
|
||||
await evt.wait()
|
||||
|
||||
mock_create_subprocess.return_value.wait.side_effect = wait_event
|
||||
|
||||
await server.start()
|
||||
|
||||
await asyncio.sleep(0.1)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Verify go2rtc binary stdout was logged with default level
|
||||
for i, entry in enumerate(server_stdout):
|
||||
assert (
|
||||
"homeassistant.components.go2rtc.server",
|
||||
expected_loglevel[i],
|
||||
entry,
|
||||
) in caplog.record_tuples
|
||||
|
||||
evt.set()
|
||||
await asyncio.sleep(0.1)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert_server_output_logged(server_stdout, caplog, logging.WARNING)
|
||||
|
||||
await server.stop()
|
||||
|
||||
|
||||
@patch("homeassistant.components.go2rtc.server._RESPAWN_COOLDOWN", 0)
|
||||
async def test_server_restart_process_exit(
|
||||
hass: HomeAssistant,
|
||||
mock_create_subprocess: AsyncMock,
|
||||
server_stdout: list[str],
|
||||
rest_client: AsyncMock,
|
||||
server: Server,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test that the server is restarted when it exits."""
|
||||
evt = asyncio.Event()
|
||||
@@ -186,10 +289,16 @@ async def test_server_restart_process_exit(
|
||||
await hass.async_block_till_done()
|
||||
mock_create_subprocess.assert_not_awaited()
|
||||
|
||||
# Verify go2rtc binary stdout was not yet logged with warning level
|
||||
assert_server_output_not_logged(server_stdout, caplog, logging.WARNING)
|
||||
|
||||
evt.set()
|
||||
await asyncio.sleep(0.1)
|
||||
mock_create_subprocess.assert_awaited_once()
|
||||
|
||||
# Verify go2rtc binary stdout was logged with warning level
|
||||
assert_server_output_logged(server_stdout, caplog, logging.WARNING)
|
||||
|
||||
await server.stop()
|
||||
|
||||
|
||||
@@ -197,8 +306,10 @@ async def test_server_restart_process_exit(
|
||||
async def test_server_restart_process_error(
|
||||
hass: HomeAssistant,
|
||||
mock_create_subprocess: AsyncMock,
|
||||
server_stdout: list[str],
|
||||
rest_client: AsyncMock,
|
||||
server: Server,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test that the server is restarted on error."""
|
||||
mock_create_subprocess.return_value.wait.side_effect = [Exception, None, None, None]
|
||||
@@ -207,10 +318,16 @@ async def test_server_restart_process_error(
|
||||
mock_create_subprocess.assert_awaited_once()
|
||||
mock_create_subprocess.reset_mock()
|
||||
|
||||
# Verify go2rtc binary stdout was not yet logged with warning level
|
||||
assert_server_output_not_logged(server_stdout, caplog, logging.WARNING)
|
||||
|
||||
await asyncio.sleep(0.1)
|
||||
await hass.async_block_till_done()
|
||||
mock_create_subprocess.assert_awaited_once()
|
||||
|
||||
# Verify go2rtc binary stdout was logged with warning level
|
||||
assert_server_output_logged(server_stdout, caplog, logging.WARNING)
|
||||
|
||||
await server.stop()
|
||||
|
||||
|
||||
@@ -218,8 +335,10 @@ async def test_server_restart_process_error(
|
||||
async def test_server_restart_api_error(
|
||||
hass: HomeAssistant,
|
||||
mock_create_subprocess: AsyncMock,
|
||||
server_stdout: list[str],
|
||||
rest_client: AsyncMock,
|
||||
server: Server,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test that the server is restarted on error."""
|
||||
rest_client.streams.list.side_effect = Exception
|
||||
@@ -228,10 +347,16 @@ async def test_server_restart_api_error(
|
||||
mock_create_subprocess.assert_awaited_once()
|
||||
mock_create_subprocess.reset_mock()
|
||||
|
||||
# Verify go2rtc binary stdout was not yet logged with warning level
|
||||
assert_server_output_not_logged(server_stdout, caplog, logging.WARNING)
|
||||
|
||||
await asyncio.sleep(0.1)
|
||||
await hass.async_block_till_done()
|
||||
mock_create_subprocess.assert_awaited_once()
|
||||
|
||||
# Verify go2rtc binary stdout was logged with warning level
|
||||
assert_server_output_logged(server_stdout, caplog, logging.WARNING)
|
||||
|
||||
await server.stop()
|
||||
|
||||
|
||||
@@ -239,6 +364,7 @@ async def test_server_restart_api_error(
|
||||
async def test_server_restart_error(
|
||||
hass: HomeAssistant,
|
||||
mock_create_subprocess: AsyncMock,
|
||||
server_stdout: list[str],
|
||||
rest_client: AsyncMock,
|
||||
server: Server,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
@@ -251,10 +377,16 @@ async def test_server_restart_error(
|
||||
mock_create_subprocess.assert_awaited_once()
|
||||
mock_create_subprocess.reset_mock()
|
||||
|
||||
# Verify go2rtc binary stdout was not yet logged with warning level
|
||||
assert_server_output_not_logged(server_stdout, caplog, logging.WARNING)
|
||||
|
||||
await asyncio.sleep(0.1)
|
||||
await hass.async_block_till_done()
|
||||
mock_create_subprocess.assert_awaited_once()
|
||||
|
||||
# Verify go2rtc binary stdout was logged with warning level
|
||||
assert_server_output_logged(server_stdout, caplog, logging.WARNING)
|
||||
|
||||
assert "Unexpected error when restarting go2rtc server" in caplog.text
|
||||
|
||||
await server.stop()
|
||||
|
||||
@@ -868,3 +868,104 @@ async def test_supervisor_issue_detached_addon_removed(
|
||||
str(aioclient_mock.mock_calls[-1][1])
|
||||
== "http://127.0.0.1/resolution/suggestion/1235"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"all_setup_requests", [{"include_addons": True}], indirect=True
|
||||
)
|
||||
@pytest.mark.usefixtures("all_setup_requests")
|
||||
async def test_supervisor_issue_addon_boot_fail(
|
||||
hass: HomeAssistant,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
hass_client: ClientSessionGenerator,
|
||||
issue_registry: ir.IssueRegistry,
|
||||
) -> None:
|
||||
"""Test fix flow for supervisor issue."""
|
||||
mock_resolution_info(
|
||||
aioclient_mock,
|
||||
issues=[
|
||||
{
|
||||
"uuid": "1234",
|
||||
"type": "boot_fail",
|
||||
"context": "addon",
|
||||
"reference": "test",
|
||||
"suggestions": [
|
||||
{
|
||||
"uuid": "1235",
|
||||
"type": "execute_start",
|
||||
"context": "addon",
|
||||
"reference": "test",
|
||||
},
|
||||
{
|
||||
"uuid": "1236",
|
||||
"type": "disable_boot",
|
||||
"context": "addon",
|
||||
"reference": "test",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
)
|
||||
|
||||
assert await async_setup_component(hass, "hassio", {})
|
||||
|
||||
repair_issue = issue_registry.async_get_issue(domain="hassio", issue_id="1234")
|
||||
assert repair_issue
|
||||
|
||||
client = await hass_client()
|
||||
|
||||
resp = await client.post(
|
||||
"/api/repairs/issues/fix",
|
||||
json={"handler": "hassio", "issue_id": repair_issue.issue_id},
|
||||
)
|
||||
|
||||
assert resp.status == HTTPStatus.OK
|
||||
data = await resp.json()
|
||||
|
||||
flow_id = data["flow_id"]
|
||||
assert data == {
|
||||
"type": "menu",
|
||||
"flow_id": flow_id,
|
||||
"handler": "hassio",
|
||||
"step_id": "fix_menu",
|
||||
"data_schema": [
|
||||
{
|
||||
"type": "select",
|
||||
"options": [
|
||||
["addon_execute_start", "addon_execute_start"],
|
||||
["addon_disable_boot", "addon_disable_boot"],
|
||||
],
|
||||
"name": "next_step_id",
|
||||
}
|
||||
],
|
||||
"menu_options": ["addon_execute_start", "addon_disable_boot"],
|
||||
"description_placeholders": {
|
||||
"reference": "test",
|
||||
"addon": "test",
|
||||
},
|
||||
}
|
||||
|
||||
resp = await client.post(
|
||||
f"/api/repairs/issues/fix/{flow_id}",
|
||||
json={"next_step_id": "addon_execute_start"},
|
||||
)
|
||||
|
||||
assert resp.status == HTTPStatus.OK
|
||||
data = await resp.json()
|
||||
|
||||
flow_id = data["flow_id"]
|
||||
assert data == {
|
||||
"type": "create_entry",
|
||||
"flow_id": flow_id,
|
||||
"handler": "hassio",
|
||||
"description": None,
|
||||
"description_placeholders": None,
|
||||
}
|
||||
|
||||
assert not issue_registry.async_get_issue(domain="hassio", issue_id="1234")
|
||||
|
||||
assert aioclient_mock.mock_calls[-1][0] == "post"
|
||||
assert (
|
||||
str(aioclient_mock.mock_calls[-1][1])
|
||||
== "http://127.0.0.1/resolution/suggestion/1235"
|
||||
)
|
||||
|
||||
@@ -161,7 +161,9 @@ async def test_number_entity_error(
|
||||
with pytest.raises(HomeConnectError):
|
||||
getattr(problematic_appliance, mock_attr)()
|
||||
|
||||
with pytest.raises(ServiceValidationError, match=r"Error.*set.*setting.*"):
|
||||
with pytest.raises(
|
||||
ServiceValidationError, match=r"Error.*assign.*value.*to.*setting.*"
|
||||
):
|
||||
await hass.services.async_call(
|
||||
NUMBER_DOMAIN,
|
||||
SERVICE_SET_VALUE,
|
||||
|
||||
@@ -135,7 +135,9 @@ async def test_time_entity_error(
|
||||
with pytest.raises(HomeConnectError):
|
||||
getattr(problematic_appliance, mock_attr)()
|
||||
|
||||
with pytest.raises(ServiceValidationError, match=r"Error.*set.*setting.*"):
|
||||
with pytest.raises(
|
||||
ServiceValidationError, match=r"Error.*assign.*value.*to.*setting.*"
|
||||
):
|
||||
await hass.services.async_call(
|
||||
TIME_DOMAIN,
|
||||
SERVICE_SET_VALUE,
|
||||
|
||||
@@ -25,6 +25,25 @@ IMPROV_BLE_DISCOVERY_INFO = BluetoothServiceInfoBleak(
|
||||
)
|
||||
|
||||
|
||||
BAD_IMPROV_BLE_DISCOVERY_INFO = BluetoothServiceInfoBleak(
|
||||
name="00123456",
|
||||
address="AA:BB:CC:DD:EE:F0",
|
||||
rssi=-60,
|
||||
manufacturer_data={},
|
||||
service_uuids=[SERVICE_UUID],
|
||||
service_data={SERVICE_DATA_UUID: b"\x00\x00\x00\x00\x00\x00"},
|
||||
source="local",
|
||||
device=generate_ble_device(address="AA:BB:CC:DD:EE:F0", name="00123456"),
|
||||
advertisement=generate_advertisement_data(
|
||||
service_uuids=[SERVICE_UUID],
|
||||
service_data={SERVICE_DATA_UUID: b"\x00\x00\x00\x00\x00\x00"},
|
||||
),
|
||||
time=0,
|
||||
connectable=True,
|
||||
tx_power=-127,
|
||||
)
|
||||
|
||||
|
||||
PROVISIONED_IMPROV_BLE_DISCOVERY_INFO = BluetoothServiceInfoBleak(
|
||||
name="00123456",
|
||||
address="AA:BB:CC:DD:EE:F0",
|
||||
|
||||
@@ -15,6 +15,7 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResult, FlowResultType
|
||||
|
||||
from . import (
|
||||
BAD_IMPROV_BLE_DISCOVERY_INFO,
|
||||
IMPROV_BLE_DISCOVERY_INFO,
|
||||
NOT_IMPROV_BLE_DISCOVERY_INFO,
|
||||
PROVISIONED_IMPROV_BLE_DISCOVERY_INFO,
|
||||
@@ -649,3 +650,20 @@ async def test_provision_retry(hass: HomeAssistant, exc, error) -> None:
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "provision"
|
||||
assert result["errors"] == {"base": error}
|
||||
|
||||
|
||||
async def test_provision_fails_invalid_data(
|
||||
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
"""Test bluetooth flow with error due to invalid data."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_BLUETOOTH},
|
||||
data=BAD_IMPROV_BLE_DISCOVERY_INFO,
|
||||
)
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "invalid_improv_data"
|
||||
assert (
|
||||
"Aborting improv flow, device AA:BB:CC:DD:EE:F0 sent invalid improv data: '000000000000'"
|
||||
in caplog.text
|
||||
)
|
||||
|
||||
@@ -1 +1,13 @@
|
||||
"""Tests for the lgthinq integration."""
|
||||
"""Tests for the LG ThinQ integration."""
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def setup_integration(hass: HomeAssistant, config_entry: MockConfigEntry) -> None:
|
||||
"""Fixture for setting up the component."""
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
@@ -11,7 +11,7 @@ from homeassistant.const import CONF_ACCESS_TOKEN, CONF_COUNTRY
|
||||
|
||||
from .const import MOCK_CONNECT_CLIENT_ID, MOCK_COUNTRY, MOCK_PAT, MOCK_UUID
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
from tests.common import MockConfigEntry, load_json_object_fixture
|
||||
|
||||
|
||||
def mock_thinq_api_response(
|
||||
@@ -45,6 +45,15 @@ def mock_config_entry() -> MockConfigEntry:
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_setup_entry() -> Generator[AsyncMock]:
|
||||
"""Mock setting up a config entry."""
|
||||
with patch(
|
||||
"homeassistant.components.lg_thinq.async_setup_entry", return_value=True
|
||||
) as mock_setup_entry:
|
||||
yield mock_setup_entry
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_uuid() -> Generator[AsyncMock]:
|
||||
"""Mock a uuid."""
|
||||
@@ -59,22 +68,37 @@ def mock_uuid() -> Generator[AsyncMock]:
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_thinq_api() -> Generator[AsyncMock]:
|
||||
def mock_thinq_api(mock_thinq_mqtt_client: AsyncMock) -> Generator[AsyncMock]:
|
||||
"""Mock a thinq api."""
|
||||
with (
|
||||
patch("thinqconnect.ThinQApi", autospec=True) as mock_api,
|
||||
patch("homeassistant.components.lg_thinq.ThinQApi", autospec=True) as mock_api,
|
||||
patch(
|
||||
"homeassistant.components.lg_thinq.config_flow.ThinQApi",
|
||||
new=mock_api,
|
||||
),
|
||||
):
|
||||
thinq_api = mock_api.return_value
|
||||
thinq_api.async_get_device_list = AsyncMock(
|
||||
return_value=mock_thinq_api_response(status=200, body={})
|
||||
thinq_api.async_get_device_list.return_value = [
|
||||
load_json_object_fixture("air_conditioner/device.json", DOMAIN)
|
||||
]
|
||||
thinq_api.async_get_device_profile.return_value = load_json_object_fixture(
|
||||
"air_conditioner/profile.json", DOMAIN
|
||||
)
|
||||
thinq_api.async_get_device_status.return_value = load_json_object_fixture(
|
||||
"air_conditioner/status.json", DOMAIN
|
||||
)
|
||||
yield thinq_api
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_thinq_mqtt_client() -> Generator[AsyncMock]:
|
||||
"""Mock a thinq api."""
|
||||
with patch(
|
||||
"homeassistant.components.lg_thinq.mqtt.ThinQMQTTClient", autospec=True
|
||||
) as mock_api:
|
||||
yield mock_api
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_invalid_thinq_api(mock_thinq_api: AsyncMock) -> AsyncMock:
|
||||
"""Mock an invalid thinq api."""
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"deviceId": "MW2-2E247F93-B570-46A6-B827-920E9E10F966",
|
||||
"deviceInfo": {
|
||||
"deviceType": "DEVICE_AIR_CONDITIONER",
|
||||
"modelName": "PAC_910604_WW",
|
||||
"alias": "Test air conditioner",
|
||||
"reportable": true
|
||||
}
|
||||
}
|
||||
154
tests/components/lg_thinq/fixtures/air_conditioner/profile.json
Normal file
154
tests/components/lg_thinq/fixtures/air_conditioner/profile.json
Normal file
@@ -0,0 +1,154 @@
|
||||
{
|
||||
"notification": {
|
||||
"push": ["WATER_IS_FULL"]
|
||||
},
|
||||
"property": {
|
||||
"airConJobMode": {
|
||||
"currentJobMode": {
|
||||
"mode": ["r", "w"],
|
||||
"type": "enum",
|
||||
"value": {
|
||||
"r": ["AIR_CLEAN", "COOL", "AIR_DRY"],
|
||||
"w": ["AIR_CLEAN", "COOL", "AIR_DRY"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"airFlow": {
|
||||
"windStrength": {
|
||||
"mode": ["r", "w"],
|
||||
"type": "enum",
|
||||
"value": {
|
||||
"r": ["LOW", "HIGH", "MID"],
|
||||
"w": ["LOW", "HIGH", "MID"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"airQualitySensor": {
|
||||
"PM1": {
|
||||
"mode": ["r"],
|
||||
"type": "number"
|
||||
},
|
||||
"PM10": {
|
||||
"mode": ["r"],
|
||||
"type": "number"
|
||||
},
|
||||
"PM2": {
|
||||
"mode": ["r"],
|
||||
"type": "number"
|
||||
},
|
||||
"humidity": {
|
||||
"mode": ["r"],
|
||||
"type": "number"
|
||||
},
|
||||
"monitoringEnabled": {
|
||||
"mode": ["r", "w"],
|
||||
"type": "enum",
|
||||
"value": {
|
||||
"r": ["ON_WORKING", "ALWAYS"],
|
||||
"w": ["ON_WORKING", "ALWAYS"]
|
||||
}
|
||||
},
|
||||
"oder": {
|
||||
"mode": ["r"],
|
||||
"type": "number"
|
||||
},
|
||||
"totalPollution": {
|
||||
"mode": ["r"],
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"operation": {
|
||||
"airCleanOperationMode": {
|
||||
"mode": ["w"],
|
||||
"type": "enum",
|
||||
"value": {
|
||||
"w": ["START", "STOP"]
|
||||
}
|
||||
},
|
||||
"airConOperationMode": {
|
||||
"mode": ["r", "w"],
|
||||
"type": "enum",
|
||||
"value": {
|
||||
"r": ["POWER_ON", "POWER_OFF"],
|
||||
"w": ["POWER_ON", "POWER_OFF"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"powerSave": {
|
||||
"powerSaveEnabled": {
|
||||
"mode": ["r", "w"],
|
||||
"type": "boolean",
|
||||
"value": {
|
||||
"r": [false, true],
|
||||
"w": [false, true]
|
||||
}
|
||||
}
|
||||
},
|
||||
"temperature": {
|
||||
"coolTargetTemperature": {
|
||||
"mode": ["w"],
|
||||
"type": "range",
|
||||
"value": {
|
||||
"w": {
|
||||
"max": 30,
|
||||
"min": 18,
|
||||
"step": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
"currentTemperature": {
|
||||
"mode": ["r"],
|
||||
"type": "number"
|
||||
},
|
||||
"targetTemperature": {
|
||||
"mode": ["r", "w"],
|
||||
"type": "range",
|
||||
"value": {
|
||||
"r": {
|
||||
"max": 30,
|
||||
"min": 18,
|
||||
"step": 1
|
||||
},
|
||||
"w": {
|
||||
"max": 30,
|
||||
"min": 18,
|
||||
"step": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
"unit": {
|
||||
"mode": ["r"],
|
||||
"type": "enum",
|
||||
"value": {
|
||||
"r": ["C", "F"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"timer": {
|
||||
"relativeHourToStart": {
|
||||
"mode": ["r", "w"],
|
||||
"type": "number"
|
||||
},
|
||||
"relativeHourToStop": {
|
||||
"mode": ["r", "w"],
|
||||
"type": "number"
|
||||
},
|
||||
"relativeMinuteToStart": {
|
||||
"mode": ["r", "w"],
|
||||
"type": "number"
|
||||
},
|
||||
"relativeMinuteToStop": {
|
||||
"mode": ["r", "w"],
|
||||
"type": "number"
|
||||
},
|
||||
"absoluteHourToStart": {
|
||||
"mode": ["r", "w"],
|
||||
"type": "number"
|
||||
},
|
||||
"absoluteMinuteToStart": {
|
||||
"mode": ["r", "w"],
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"airConJobMode": {
|
||||
"currentJobMode": "COOL"
|
||||
},
|
||||
"airFlow": {
|
||||
"windStrength": "MID"
|
||||
},
|
||||
"airQualitySensor": {
|
||||
"PM1": 12,
|
||||
"PM10": 7,
|
||||
"PM2": 24,
|
||||
"humidity": 40,
|
||||
"monitoringEnabled": "ON_WORKING",
|
||||
"totalPollution": 3,
|
||||
"totalPollutionLevel": "GOOD"
|
||||
},
|
||||
"filterInfo": {
|
||||
"filterLifetime": 540,
|
||||
"usedTime": 180
|
||||
},
|
||||
"operation": {
|
||||
"airConOperationMode": "POWER_ON"
|
||||
},
|
||||
"powerSave": {
|
||||
"powerSaveEnabled": false
|
||||
},
|
||||
"sleepTimer": {
|
||||
"relativeStopTimer": "UNSET"
|
||||
},
|
||||
"temperature": {
|
||||
"currentTemperature": 25,
|
||||
"targetTemperature": 19,
|
||||
"unit": "C"
|
||||
},
|
||||
"timer": {
|
||||
"relativeStartTimer": "UNSET",
|
||||
"relativeStopTimer": "UNSET",
|
||||
"absoluteStartTimer": "SET",
|
||||
"absoluteStopTimer": "UNSET",
|
||||
"absoluteHourToStart": 13,
|
||||
"absoluteMinuteToStart": 14
|
||||
}
|
||||
}
|
||||
86
tests/components/lg_thinq/snapshots/test_climate.ambr
Normal file
86
tests/components/lg_thinq/snapshots/test_climate.ambr
Normal file
@@ -0,0 +1,86 @@
|
||||
# serializer version: 1
|
||||
# name: test_all_entities[climate.test_air_conditioner-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'fan_modes': list([
|
||||
'low',
|
||||
'high',
|
||||
'mid',
|
||||
]),
|
||||
'hvac_modes': list([
|
||||
<HVACMode.OFF: 'off'>,
|
||||
<HVACMode.COOL: 'cool'>,
|
||||
<HVACMode.DRY: 'dry'>,
|
||||
]),
|
||||
'max_temp': 30,
|
||||
'min_temp': 18,
|
||||
'preset_modes': list([
|
||||
'air_clean',
|
||||
]),
|
||||
'target_temp_step': 1,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'climate',
|
||||
'entity_category': None,
|
||||
'entity_id': 'climate.test_air_conditioner',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': None,
|
||||
'platform': 'lg_thinq',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': <ClimateEntityFeature: 409>,
|
||||
'translation_key': <ExtendedProperty.CLIMATE_AIR_CONDITIONER: 'climate_air_conditioner'>,
|
||||
'unique_id': 'MW2-2E247F93-B570-46A6-B827-920E9E10F966_climate_air_conditioner',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[climate.test_air_conditioner-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'current_humidity': 40,
|
||||
'current_temperature': 25,
|
||||
'fan_mode': 'mid',
|
||||
'fan_modes': list([
|
||||
'low',
|
||||
'high',
|
||||
'mid',
|
||||
]),
|
||||
'friendly_name': 'Test air conditioner',
|
||||
'hvac_modes': list([
|
||||
<HVACMode.OFF: 'off'>,
|
||||
<HVACMode.COOL: 'cool'>,
|
||||
<HVACMode.DRY: 'dry'>,
|
||||
]),
|
||||
'max_temp': 30,
|
||||
'min_temp': 18,
|
||||
'preset_mode': None,
|
||||
'preset_modes': list([
|
||||
'air_clean',
|
||||
]),
|
||||
'supported_features': <ClimateEntityFeature: 409>,
|
||||
'target_temp_step': 1,
|
||||
'temperature': 19,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'climate.test_air_conditioner',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'cool',
|
||||
})
|
||||
# ---
|
||||
55
tests/components/lg_thinq/snapshots/test_event.ambr
Normal file
55
tests/components/lg_thinq/snapshots/test_event.ambr
Normal file
@@ -0,0 +1,55 @@
|
||||
# serializer version: 1
|
||||
# name: test_all_entities[event.test_air_conditioner_notification-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'event_types': list([
|
||||
'water_is_full',
|
||||
]),
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'event',
|
||||
'entity_category': None,
|
||||
'entity_id': 'event.test_air_conditioner_notification',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Notification',
|
||||
'platform': 'lg_thinq',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': <ThinQPropertyEx.NOTIFICATION: 'notification'>,
|
||||
'unique_id': 'MW2-2E247F93-B570-46A6-B827-920E9E10F966_notification',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[event.test_air_conditioner_notification-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'event_type': None,
|
||||
'event_types': list([
|
||||
'water_is_full',
|
||||
]),
|
||||
'friendly_name': 'Test air conditioner Notification',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'event.test_air_conditioner_notification',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'unknown',
|
||||
})
|
||||
# ---
|
||||
113
tests/components/lg_thinq/snapshots/test_number.ambr
Normal file
113
tests/components/lg_thinq/snapshots/test_number.ambr
Normal file
@@ -0,0 +1,113 @@
|
||||
# serializer version: 1
|
||||
# name: test_all_entities[number.test_air_conditioner_schedule_turn_off-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'max': 100.0,
|
||||
'min': 0.0,
|
||||
'mode': <NumberMode.BOX: 'box'>,
|
||||
'step': 1.0,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'number',
|
||||
'entity_category': None,
|
||||
'entity_id': 'number.test_air_conditioner_schedule_turn_off',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Schedule turn-off',
|
||||
'platform': 'lg_thinq',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': <Property.RELATIVE_HOUR_TO_STOP: 'relative_hour_to_stop'>,
|
||||
'unique_id': 'MW2-2E247F93-B570-46A6-B827-920E9E10F966_relative_hour_to_stop',
|
||||
'unit_of_measurement': <UnitOfTime.HOURS: 'h'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[number.test_air_conditioner_schedule_turn_off-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Test air conditioner Schedule turn-off',
|
||||
'max': 100.0,
|
||||
'min': 0.0,
|
||||
'mode': <NumberMode.BOX: 'box'>,
|
||||
'step': 1.0,
|
||||
'unit_of_measurement': <UnitOfTime.HOURS: 'h'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'number.test_air_conditioner_schedule_turn_off',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'unknown',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[number.test_air_conditioner_schedule_turn_on-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'max': 100.0,
|
||||
'min': 0.0,
|
||||
'mode': <NumberMode.BOX: 'box'>,
|
||||
'step': 1.0,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'number',
|
||||
'entity_category': None,
|
||||
'entity_id': 'number.test_air_conditioner_schedule_turn_on',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Schedule turn-on',
|
||||
'platform': 'lg_thinq',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': <Property.RELATIVE_HOUR_TO_START: 'relative_hour_to_start'>,
|
||||
'unique_id': 'MW2-2E247F93-B570-46A6-B827-920E9E10F966_relative_hour_to_start',
|
||||
'unit_of_measurement': <UnitOfTime.HOURS: 'h'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[number.test_air_conditioner_schedule_turn_on-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Test air conditioner Schedule turn-on',
|
||||
'max': 100.0,
|
||||
'min': 0.0,
|
||||
'mode': <NumberMode.BOX: 'box'>,
|
||||
'step': 1.0,
|
||||
'unit_of_measurement': <UnitOfTime.HOURS: 'h'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'number.test_air_conditioner_schedule_turn_on',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'unknown',
|
||||
})
|
||||
# ---
|
||||
205
tests/components/lg_thinq/snapshots/test_sensor.ambr
Normal file
205
tests/components/lg_thinq/snapshots/test_sensor.ambr
Normal file
@@ -0,0 +1,205 @@
|
||||
# serializer version: 1
|
||||
# name: test_all_entities[sensor.test_air_conditioner_humidity-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.test_air_conditioner_humidity',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.HUMIDITY: 'humidity'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Humidity',
|
||||
'platform': 'lg_thinq',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': None,
|
||||
'unique_id': 'MW2-2E247F93-B570-46A6-B827-920E9E10F966_humidity',
|
||||
'unit_of_measurement': '%',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.test_air_conditioner_humidity-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'humidity',
|
||||
'friendly_name': 'Test air conditioner Humidity',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': '%',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.test_air_conditioner_humidity',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '40',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.test_air_conditioner_pm1-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.test_air_conditioner_pm1',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.PM1: 'pm1'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'PM1',
|
||||
'platform': 'lg_thinq',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': None,
|
||||
'unique_id': 'MW2-2E247F93-B570-46A6-B827-920E9E10F966_pm1',
|
||||
'unit_of_measurement': 'µg/m³',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.test_air_conditioner_pm1-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'pm1',
|
||||
'friendly_name': 'Test air conditioner PM1',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': 'µg/m³',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.test_air_conditioner_pm1',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '12',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.test_air_conditioner_pm10-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.test_air_conditioner_pm10',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.PM10: 'pm10'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'PM10',
|
||||
'platform': 'lg_thinq',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': None,
|
||||
'unique_id': 'MW2-2E247F93-B570-46A6-B827-920E9E10F966_pm10',
|
||||
'unit_of_measurement': 'µg/m³',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.test_air_conditioner_pm10-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'pm10',
|
||||
'friendly_name': 'Test air conditioner PM10',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': 'µg/m³',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.test_air_conditioner_pm10',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '7',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.test_air_conditioner_pm2_5-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.test_air_conditioner_pm2_5',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.PM25: 'pm25'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'PM2.5',
|
||||
'platform': 'lg_thinq',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': None,
|
||||
'unique_id': 'MW2-2E247F93-B570-46A6-B827-920E9E10F966_pm2',
|
||||
'unit_of_measurement': 'µg/m³',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.test_air_conditioner_pm2_5-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'pm25',
|
||||
'friendly_name': 'Test air conditioner PM2.5',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': 'µg/m³',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.test_air_conditioner_pm2_5',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '24',
|
||||
})
|
||||
# ---
|
||||
29
tests/components/lg_thinq/test_climate.py
Normal file
29
tests/components/lg_thinq/test_climate.py
Normal file
@@ -0,0 +1,29 @@
|
||||
"""Tests for the LG Thinq climate platform."""
|
||||
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
from syrupy import SnapshotAssertion
|
||||
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from . import setup_integration
|
||||
|
||||
from tests.common import MockConfigEntry, snapshot_platform
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||
async def test_all_entities(
|
||||
hass: HomeAssistant,
|
||||
snapshot: SnapshotAssertion,
|
||||
mock_thinq_api: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test all entities."""
|
||||
with patch("homeassistant.components.lg_thinq.PLATFORMS", [Platform.CLIMATE]):
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
|
||||
@@ -14,7 +14,10 @@ from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def test_config_flow(
|
||||
hass: HomeAssistant, mock_thinq_api: AsyncMock, mock_uuid: AsyncMock
|
||||
hass: HomeAssistant,
|
||||
mock_thinq_api: AsyncMock,
|
||||
mock_uuid: AsyncMock,
|
||||
mock_setup_entry: AsyncMock,
|
||||
) -> None:
|
||||
"""Test that an thinq entry is normally created."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
|
||||
29
tests/components/lg_thinq/test_event.py
Normal file
29
tests/components/lg_thinq/test_event.py
Normal file
@@ -0,0 +1,29 @@
|
||||
"""Tests for the LG Thinq event platform."""
|
||||
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
from syrupy import SnapshotAssertion
|
||||
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from . import setup_integration
|
||||
|
||||
from tests.common import MockConfigEntry, snapshot_platform
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||
async def test_all_entities(
|
||||
hass: HomeAssistant,
|
||||
snapshot: SnapshotAssertion,
|
||||
mock_thinq_api: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test all entities."""
|
||||
with patch("homeassistant.components.lg_thinq.PLATFORMS", [Platform.EVENT]):
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
|
||||
26
tests/components/lg_thinq/test_init.py
Normal file
26
tests/components/lg_thinq/test_init.py
Normal file
@@ -0,0 +1,26 @@
|
||||
"""Tests for the LG ThinQ integration."""
|
||||
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def test_load_unload_entry(
|
||||
hass: HomeAssistant,
|
||||
mock_thinq_api: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test load and unload entry."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
assert await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert mock_config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
await hass.config_entries.async_remove(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert mock_config_entry.state is ConfigEntryState.NOT_LOADED
|
||||
29
tests/components/lg_thinq/test_number.py
Normal file
29
tests/components/lg_thinq/test_number.py
Normal file
@@ -0,0 +1,29 @@
|
||||
"""Tests for the LG Thinq number platform."""
|
||||
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
from syrupy import SnapshotAssertion
|
||||
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from . import setup_integration
|
||||
|
||||
from tests.common import MockConfigEntry, snapshot_platform
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||
async def test_all_entities(
|
||||
hass: HomeAssistant,
|
||||
snapshot: SnapshotAssertion,
|
||||
mock_thinq_api: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test all entities."""
|
||||
with patch("homeassistant.components.lg_thinq.PLATFORMS", [Platform.NUMBER]):
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
|
||||
29
tests/components/lg_thinq/test_sensor.py
Normal file
29
tests/components/lg_thinq/test_sensor.py
Normal file
@@ -0,0 +1,29 @@
|
||||
"""Tests for the LG Thinq sensor platform."""
|
||||
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
from syrupy import SnapshotAssertion
|
||||
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from . import setup_integration
|
||||
|
||||
from tests.common import MockConfigEntry, snapshot_platform
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||
async def test_all_entities(
|
||||
hass: HomeAssistant,
|
||||
snapshot: SnapshotAssertion,
|
||||
mock_thinq_api: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test all entities."""
|
||||
with patch("homeassistant.components.lg_thinq.PLATFORMS", [Platform.SENSOR]):
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
|
||||
@@ -3,10 +3,72 @@
|
||||
from homeassistant.components.utility_meter.const import DOMAIN
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def test_select_entity_name_config_entry(
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
"""Test for Utility Meter select platform."""
|
||||
|
||||
config_entry_config = {
|
||||
"cycle": "none",
|
||||
"delta_values": False,
|
||||
"name": "Energy bill",
|
||||
"net_consumption": False,
|
||||
"offset": 0,
|
||||
"periodically_resetting": True,
|
||||
"source": "sensor.energy",
|
||||
"tariffs": ["peak", "offpeak"],
|
||||
}
|
||||
|
||||
source_config_entry = MockConfigEntry()
|
||||
source_config_entry.add_to_hass(hass)
|
||||
utility_meter_config_entry = MockConfigEntry(
|
||||
data={},
|
||||
domain=DOMAIN,
|
||||
options=config_entry_config,
|
||||
title=config_entry_config["name"],
|
||||
)
|
||||
|
||||
utility_meter_config_entry.add_to_hass(hass)
|
||||
|
||||
assert await hass.config_entries.async_setup(utility_meter_config_entry.entry_id)
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("select.energy_bill")
|
||||
assert state is not None
|
||||
assert state.attributes.get("friendly_name") == "Energy bill"
|
||||
|
||||
|
||||
async def test_select_entity_name_yaml(
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
"""Test for Utility Meter select platform."""
|
||||
|
||||
yaml_config = {
|
||||
"utility_meter": {
|
||||
"energy_bill": {
|
||||
"name": "Energy bill",
|
||||
"source": "sensor.energy",
|
||||
"tariffs": ["peak", "offpeak"],
|
||||
"unique_id": "1234abcd",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assert await async_setup_component(hass, DOMAIN, yaml_config)
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("select.energy_bill")
|
||||
assert state is not None
|
||||
assert state.attributes.get("friendly_name") == "Energy bill"
|
||||
|
||||
|
||||
async def test_device_id(
|
||||
hass: HomeAssistant,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
|
||||
Reference in New Issue
Block a user