Compare commits

..

19 Commits

Author SHA1 Message Date
Mike Degatano 645cdf5692 Backup method to aiohasupervisor 2024-11-11 19:49:24 +00:00
J. Nick Koston e388e9f396 Fix missing title placeholders in powerwall reauth (#130389) 2024-11-11 20:48:49 +01:00
Markus Lanthaler 96c12fdd10 Update tuya-device-sharing-sdk to version 0.2.1 (#130333) 2024-11-11 20:40:37 +01:00
Noah Husby e97a5f927c Bump aiorussound to 4.1.0 (#130382) 2024-11-11 20:26:45 +01:00
epenet 313309a7e0 Remove deprecated YAML loaders (#130364) 2024-11-11 20:24:51 +01:00
Barry vd. Heuvel ebe62501d6 Bump Weheat wh-python to 2024.11.02 (#130337) 2024-11-11 20:14:12 +01:00
Robert Resch c54369fe93 Add go2rtc to devcontainer (#130380) 2024-11-11 20:13:20 +01:00
Marc Mueller c89bf6a9aa Update pillow to 11.0.0 (#130194) 2024-11-11 20:12:32 +01:00
epenet 906bdda6fa Use report_usage in integrations (#130366) 2024-11-11 20:09:26 +01:00
Andre Lengwenus f3708549f0 Code cleanup for LCN integration (#130385) 2024-11-11 20:08:38 +01:00
Andre Lengwenus 3f34ddd74f Bump lcn-frontend to 0.2.2 (#130383) 2024-11-11 20:07:12 +01:00
Marc Mueller b19c44b4a5 Update pydantic to 1.10.19 (#130373) 2024-11-11 12:01:47 -06:00
Erik Montnemery 0cc50bc7bc Fix copy-paste error in STATISTIC_UNIT_TO_UNIT_CONVERTER (#130375) 2024-11-11 11:09:06 -06:00
Joost Lekkerkerker e56dec2c8e Bump spotifyaio to 0.8.8 (#130372) 2024-11-11 17:35:54 +01:00
Olivier Corradi e797149a16 Rename "CO2 Signal" display name to Electricity Maps for consistency (#130242)
* Update strings.json for Electricity Maps

* Update strings.json

* Update config_flow.py

* Update test_config_flow.py

* Fix test
2024-11-11 17:34:29 +01:00
Simon Lamon c96f1c87a6 Bump python-linkplay to 0.0.20 (#130348) 2024-11-11 17:30:27 +01:00
Erik Elkins 388c5807ea Add Switchbot Hub 2, Switchbot Meter Pro and Switchbot Meter Pro (CO2) devices to Switchbot Cloud integration. (#130295) 2024-11-11 16:10:52 +01:00
Robert Resch 41c6eeedca Bump deebot-client to 8.4.1 (#130357) 2024-11-11 15:41:18 +01:00
Lennard Beers 829632b0af Add binary sensor platform to eq3btsmart (#130352) 2024-11-11 14:27:52 +01:00
62 changed files with 366 additions and 513 deletions
+3
View File
@@ -35,6 +35,9 @@ RUN \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
# Add go2rtc binary
COPY --from=ghcr.io/alexxit/go2rtc:latest /usr/local/bin/go2rtc /bin/go2rtc
# Install uv
RUN pip3 install uv
-1
View File
@@ -31,7 +31,6 @@ PREF_GOOGLE_REPORT_STATE = "google_report_state"
PREF_ALEXA_ENTITY_CONFIGS = "alexa_entity_configs"
PREF_ALEXA_REPORT_STATE = "alexa_report_state"
PREF_DISABLE_2FA = "disable_2fa"
PREF_ENABLE_BACKUP_SYNC = "backup_sync_enabled"
PREF_INSTANCE_ID = "instance_id"
PREF_SHOULD_EXPOSE = "should_expose"
PREF_GOOGLE_LOCAL_WEBHOOK_ID = "google_local_webhook_id"
@@ -42,7 +42,6 @@ from .const import (
PREF_ALEXA_REPORT_STATE,
PREF_DISABLE_2FA,
PREF_ENABLE_ALEXA,
PREF_ENABLE_BACKUP_SYNC,
PREF_ENABLE_CLOUD_ICE_SERVERS,
PREF_ENABLE_GOOGLE,
PREF_GOOGLE_REPORT_STATE,
@@ -443,7 +442,6 @@ def validate_language_voice(value: tuple[str, str]) -> tuple[str, str]:
vol.Required("type"): "cloud/update_prefs",
vol.Optional(PREF_ALEXA_REPORT_STATE): bool,
vol.Optional(PREF_ENABLE_ALEXA): bool,
vol.Optional(PREF_ENABLE_BACKUP_SYNC): bool,
vol.Optional(PREF_ENABLE_CLOUD_ICE_SERVERS): bool,
vol.Optional(PREF_ENABLE_GOOGLE): bool,
vol.Optional(PREF_GOOGLE_REPORT_STATE): bool,
-11
View File
@@ -32,7 +32,6 @@ from .const import (
PREF_CLOUD_USER,
PREF_CLOUDHOOKS,
PREF_ENABLE_ALEXA,
PREF_ENABLE_BACKUP_SYNC,
PREF_ENABLE_CLOUD_ICE_SERVERS,
PREF_ENABLE_GOOGLE,
PREF_ENABLE_REMOTE,
@@ -167,7 +166,6 @@ class CloudPreferences:
alexa_enabled: bool | UndefinedType = UNDEFINED,
alexa_report_state: bool | UndefinedType = UNDEFINED,
alexa_settings_version: int | UndefinedType = UNDEFINED,
backup_sync_enabled: bool | UndefinedType = UNDEFINED,
cloud_ice_servers_enabled: bool | UndefinedType = UNDEFINED,
cloud_user: str | UndefinedType = UNDEFINED,
cloudhooks: dict[str, dict[str, str | bool]] | UndefinedType = UNDEFINED,
@@ -193,7 +191,6 @@ class CloudPreferences:
(PREF_CLOUD_USER, cloud_user),
(PREF_CLOUDHOOKS, cloudhooks),
(PREF_ENABLE_ALEXA, alexa_enabled),
(PREF_ENABLE_BACKUP_SYNC, backup_sync_enabled),
(PREF_ENABLE_CLOUD_ICE_SERVERS, cloud_ice_servers_enabled),
(PREF_ENABLE_GOOGLE, google_enabled),
(PREF_ENABLE_REMOTE, remote_enabled),
@@ -245,7 +242,6 @@ class CloudPreferences:
PREF_ALEXA_REPORT_STATE: self.alexa_report_state,
PREF_CLOUDHOOKS: self.cloudhooks,
PREF_ENABLE_ALEXA: self.alexa_enabled,
PREF_ENABLE_BACKUP_SYNC: self.backup_sync_enabled,
PREF_ENABLE_CLOUD_ICE_SERVERS: self.cloud_ice_servers_enabled,
PREF_ENABLE_GOOGLE: self.google_enabled,
PREF_ENABLE_REMOTE: self.remote_enabled,
@@ -378,12 +374,6 @@ class CloudPreferences:
)
return cloud_ice_servers_enabled
@property
def backup_sync_enabled(self) -> bool:
"""Return if backup sync is enabled."""
backup_sync_enabled: bool = self._prefs.get(PREF_ENABLE_BACKUP_SYNC, False)
return backup_sync_enabled
async def get_cloud_user(self) -> str:
"""Return ID of Home Assistant Cloud system user."""
user = await self._load_cloud_user()
@@ -429,7 +419,6 @@ class CloudPreferences:
PREF_CLOUD_USER: None,
PREF_CLOUDHOOKS: {},
PREF_ENABLE_ALEXA: True,
PREF_ENABLE_BACKUP_SYNC: True,
PREF_ENABLE_GOOGLE: True,
PREF_ENABLE_REMOTE: False,
PREF_ENABLE_CLOUD_ICE_SERVERS: True,
@@ -168,7 +168,7 @@ class ElectricityMapsConfigFlow(ConfigFlow, domain=DOMAIN):
)
return self.async_create_entry(
title=get_extra_name(data) or "CO2 Signal",
title=get_extra_name(data) or "Electricity Maps",
data=data,
)
+1 -1
View File
@@ -5,5 +5,5 @@
"documentation": "https://www.home-assistant.io/integrations/doods",
"iot_class": "local_polling",
"loggers": ["pydoods"],
"requirements": ["pydoods==1.0.2", "Pillow==10.4.0"]
"requirements": ["pydoods==1.0.2", "Pillow==11.0.0"]
}
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/ecovacs",
"iot_class": "cloud_push",
"loggers": ["sleekxmppfs", "sucks", "deebot_client"],
"requirements": ["py-sucks==0.9.10", "deebot-client==8.4.0"]
"requirements": ["py-sucks==0.9.10", "deebot-client==8.4.1"]
}
@@ -19,6 +19,7 @@ from .const import SIGNAL_THERMOSTAT_CONNECTED, SIGNAL_THERMOSTAT_DISCONNECTED
from .models import Eq3Config, Eq3ConfigEntryData
PLATFORMS = [
Platform.BINARY_SENSOR,
Platform.CLIMATE,
]
@@ -0,0 +1,86 @@
"""Platform for eq3 binary sensor entities."""
from collections.abc import Callable
from dataclasses import dataclass
from typing import TYPE_CHECKING
from eq3btsmart.models import Status
from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
BinarySensorEntity,
BinarySensorEntityDescription,
)
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import Eq3ConfigEntry
from .const import ENTITY_KEY_BATTERY, ENTITY_KEY_DST, ENTITY_KEY_WINDOW
from .entity import Eq3Entity
@dataclass(frozen=True, kw_only=True)
class Eq3BinarySensorEntityDescription(BinarySensorEntityDescription):
"""Entity description for eq3 binary sensors."""
value_func: Callable[[Status], bool]
BINARY_SENSOR_ENTITY_DESCRIPTIONS = [
Eq3BinarySensorEntityDescription(
value_func=lambda status: status.is_low_battery,
key=ENTITY_KEY_BATTERY,
device_class=BinarySensorDeviceClass.BATTERY,
entity_category=EntityCategory.DIAGNOSTIC,
),
Eq3BinarySensorEntityDescription(
value_func=lambda status: status.is_window_open,
key=ENTITY_KEY_WINDOW,
device_class=BinarySensorDeviceClass.WINDOW,
),
Eq3BinarySensorEntityDescription(
value_func=lambda status: status.is_dst,
key=ENTITY_KEY_DST,
translation_key=ENTITY_KEY_DST,
entity_category=EntityCategory.DIAGNOSTIC,
),
]
async def async_setup_entry(
hass: HomeAssistant,
entry: Eq3ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the entry."""
async_add_entities(
Eq3BinarySensorEntity(entry, entity_description)
for entity_description in BINARY_SENSOR_ENTITY_DESCRIPTIONS
)
class Eq3BinarySensorEntity(Eq3Entity, BinarySensorEntity):
"""Base class for eQ-3 binary sensor entities."""
entity_description: Eq3BinarySensorEntityDescription
def __init__(
self,
entry: Eq3ConfigEntry,
entity_description: Eq3BinarySensorEntityDescription,
) -> None:
"""Initialize the entity."""
super().__init__(entry, entity_description.key)
self.entity_description = entity_description
@property
def is_on(self) -> bool:
"""Return the state of the binary sensor."""
if TYPE_CHECKING:
assert self._thermostat.status is not None
return self.entity_description.value_func(self._thermostat.status)
@@ -18,6 +18,10 @@ DOMAIN = "eq3btsmart"
MANUFACTURER = "eQ-3 AG"
DEVICE_MODEL = "CC-RT-BLE-EQ"
ENTITY_KEY_DST = "dst"
ENTITY_KEY_BATTERY = "battery"
ENTITY_KEY_WINDOW = "window"
GET_DEVICE_TIMEOUT = 5 # seconds
EQ_TO_HA_HVAC: dict[OperationMode, HVACMode] = {
+11 -1
View File
@@ -24,7 +24,11 @@ class Eq3Entity(Entity):
_attr_has_entity_name = True
def __init__(self, entry: Eq3ConfigEntry, unique_id_key: str | None = None) -> None:
def __init__(
self,
entry: Eq3ConfigEntry,
unique_id_key: str | None = None,
) -> None:
"""Initialize the eq3 entity."""
self._eq3_config = entry.runtime_data.eq3_config
@@ -81,3 +85,9 @@ class Eq3Entity(Entity):
self._attr_available = True
self.async_write_ha_state()
@property
def available(self) -> bool:
"""Whether the entity is available."""
return self._thermostat.status is not None and self._attr_available
@@ -18,5 +18,12 @@
"error": {
"invalid_mac_address": "Invalid MAC address"
}
},
"entity": {
"binary_sensor": {
"dst": {
"name": "Daylight saving time"
}
}
}
}
@@ -7,5 +7,5 @@
"documentation": "https://www.home-assistant.io/integrations/generic",
"integration_type": "device",
"iot_class": "local_push",
"requirements": ["av==13.1.0", "Pillow==10.4.0"]
"requirements": ["av==13.1.0", "Pillow==11.0.0"]
}
@@ -116,7 +116,6 @@ from .discovery import async_setup_discovery_view # noqa: F401
from .handler import ( # noqa: F401
HassIO,
HassioAPIError,
async_create_backup,
async_get_green_settings,
async_get_yellow_settings,
async_reboot_host,
@@ -15,13 +15,14 @@ from aiohasupervisor.models import (
AddonsOptions,
AddonState as SupervisorAddonState,
InstalledAddonComplete,
PartialBackupOptions,
StoreAddonUpdate,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from .handler import HassioAPIError, async_create_backup, get_supervisor_client
from .handler import get_supervisor_client
type _FuncType[_T, **_P, _R] = Callable[Concatenate[_T, _P], Awaitable[_R]]
type _ReturnFuncType[_T, **_P, _R] = Callable[
@@ -31,18 +32,15 @@ type _ReturnFuncType[_T, **_P, _R] = Callable[
def api_error[_AddonManagerT: AddonManager, **_P, _R](
error_message: str,
*,
expected_error_type: type[HassioAPIError | SupervisorError] | None = None,
) -> Callable[
[_FuncType[_AddonManagerT, _P, _R]], _ReturnFuncType[_AddonManagerT, _P, _R]
]:
"""Handle HassioAPIError and raise a specific AddonError."""
error_type = expected_error_type or (HassioAPIError, SupervisorError)
"""Handle SupervisorError and raise a specific AddonError."""
def handle_hassio_api_error(
def handle_supervisor_api_error(
func: _FuncType[_AddonManagerT, _P, _R],
) -> _ReturnFuncType[_AddonManagerT, _P, _R]:
"""Handle a HassioAPIError."""
"""Handle a SupervisorError."""
@wraps(func)
async def wrapper(
@@ -51,7 +49,7 @@ def api_error[_AddonManagerT: AddonManager, **_P, _R](
"""Wrap an add-on manager method."""
try:
return_value = await func(self, *args, **kwargs)
except error_type as err:
except SupervisorError as err:
raise AddonError(
f"{error_message.format(addon_name=self.addon_name)}: {err}"
) from err
@@ -60,7 +58,7 @@ def api_error[_AddonManagerT: AddonManager, **_P, _R](
return wrapper
return handle_hassio_api_error
return handle_supervisor_api_error
@dataclass
@@ -123,10 +121,7 @@ class AddonManager:
)
)
@api_error(
"Failed to get the {addon_name} add-on discovery info",
expected_error_type=SupervisorError,
)
@api_error("Failed to get the {addon_name} add-on discovery info")
async def async_get_addon_discovery_info(self) -> dict:
"""Return add-on discovery info."""
discovery_info = next(
@@ -143,10 +138,7 @@ class AddonManager:
return discovery_info.config
@api_error(
"Failed to get the {addon_name} add-on info",
expected_error_type=SupervisorError,
)
@api_error("Failed to get the {addon_name} add-on info")
async def async_get_addon_info(self) -> AddonInfo:
"""Return and cache manager add-on info."""
addon_store_info = await self._supervisor_client.store.addon_info(
@@ -188,10 +180,7 @@ class AddonManager:
return addon_state
@api_error(
"Failed to set the {addon_name} add-on options",
expected_error_type=SupervisorError,
)
@api_error("Failed to set the {addon_name} add-on options")
async def async_set_addon_options(self, config: dict) -> None:
"""Set manager add-on options."""
await self._supervisor_client.addons.set_addon_options(
@@ -203,9 +192,7 @@ class AddonManager:
if not addon_info.available:
raise AddonError(f"{self.addon_name} add-on is not available")
@api_error(
"Failed to install the {addon_name} add-on", expected_error_type=SupervisorError
)
@api_error("Failed to install the {addon_name} add-on")
async def async_install_addon(self) -> None:
"""Install the managed add-on."""
addon_info = await self.async_get_addon_info()
@@ -214,10 +201,7 @@ class AddonManager:
await self._supervisor_client.store.install_addon(self.addon_slug)
@api_error(
"Failed to uninstall the {addon_name} add-on",
expected_error_type=SupervisorError,
)
@api_error("Failed to uninstall the {addon_name} add-on")
async def async_uninstall_addon(self) -> None:
"""Uninstall the managed add-on."""
await self._supervisor_client.addons.uninstall_addon(self.addon_slug)
@@ -240,23 +224,17 @@ class AddonManager:
self.addon_slug, StoreAddonUpdate(backup=False)
)
@api_error(
"Failed to start the {addon_name} add-on", expected_error_type=SupervisorError
)
@api_error("Failed to start the {addon_name} add-on")
async def async_start_addon(self) -> None:
"""Start the managed add-on."""
await self._supervisor_client.addons.start_addon(self.addon_slug)
@api_error(
"Failed to restart the {addon_name} add-on", expected_error_type=SupervisorError
)
@api_error("Failed to restart the {addon_name} add-on")
async def async_restart_addon(self) -> None:
"""Restart the managed add-on."""
await self._supervisor_client.addons.restart_addon(self.addon_slug)
@api_error(
"Failed to stop the {addon_name} add-on", expected_error_type=SupervisorError
)
@api_error("Failed to stop the {addon_name} add-on")
async def async_stop_addon(self) -> None:
"""Stop the managed add-on."""
await self._supervisor_client.addons.stop_addon(self.addon_slug)
@@ -268,10 +246,8 @@ class AddonManager:
name = f"addon_{self.addon_slug}_{addon_info.version}"
self._logger.debug("Creating backup: %s", name)
await async_create_backup(
self._hass,
{"name": name, "addons": [self.addon_slug]},
partial=True,
await self._supervisor_client.backups.partial_backup(
PartialBackupOptions(name=name, addons={self.addon_slug})
)
async def async_configure_addon(
@@ -76,21 +76,6 @@ async def async_update_diagnostics(hass: HomeAssistant, diagnostics: bool) -> bo
return await hassio.update_diagnostics(diagnostics)
@bind_hass
@api_data
async def async_create_backup(
hass: HomeAssistant, payload: dict, partial: bool = False
) -> dict:
"""Create a full or partial backup.
The caller of the function should handle HassioAPIError.
"""
hassio: HassIO = hass.data[DOMAIN]
backup_type = "partial" if partial else "full"
command = f"/backups/new/{backup_type}"
return await hassio.send_command(command, payload=payload, timeout=None)
@api_data
async def async_get_green_settings(hass: HomeAssistant) -> dict[str, bool]:
"""Return settings specific to Home Assistant Green."""
@@ -7,5 +7,5 @@
"documentation": "https://www.home-assistant.io/integrations/image_upload",
"integration_type": "system",
"quality_scale": "internal",
"requirements": ["Pillow==10.4.0"]
"requirements": ["Pillow==11.0.0"]
}
-136
View File
@@ -9,7 +9,6 @@ import re
from typing import cast
import pypck
import voluptuous as vol
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
@@ -19,17 +18,12 @@ from homeassistant.const import (
CONF_DEVICES,
CONF_DOMAIN,
CONF_ENTITIES,
CONF_HOST,
CONF_IP_ADDRESS,
CONF_LIGHTS,
CONF_NAME,
CONF_PASSWORD,
CONF_PORT,
CONF_RESOURCE,
CONF_SENSORS,
CONF_SOURCE,
CONF_SWITCHES,
CONF_USERNAME,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr, entity_registry as er
@@ -37,19 +31,13 @@ from homeassistant.helpers.typing import ConfigType
from .const import (
BINSENSOR_PORTS,
CONF_ACKNOWLEDGE,
CONF_CLIMATES,
CONF_CONNECTIONS,
CONF_DIM_MODE,
CONF_DOMAIN_DATA,
CONF_HARDWARE_SERIAL,
CONF_HARDWARE_TYPE,
CONF_OUTPUT,
CONF_SCENES,
CONF_SK_NUM_TRIES,
CONF_SOFTWARE_SERIAL,
CONNECTION,
DEFAULT_NAME,
DOMAIN,
LED_PORTS,
LOGICOP_PORTS,
@@ -146,110 +134,6 @@ def generate_unique_id(
return unique_id
def import_lcn_config(lcn_config: ConfigType) -> list[ConfigType]:
"""Convert lcn settings from configuration.yaml to config_entries data.
Create a list of config_entry data structures like:
"data": {
"host": "pchk",
"ip_address": "192.168.2.41",
"port": 4114,
"username": "lcn",
"password": "lcn,
"sk_num_tries: 0,
"dim_mode: "STEPS200",
"acknowledge": False,
"devices": [
{
"address": (0, 7, False)
"name": "",
"hardware_serial": -1,
"software_serial": -1,
"hardware_type": -1
}, ...
],
"entities": [
{
"address": (0, 7, False)
"name": "Light_Output1",
"resource": "output1",
"domain": "light",
"domain_data": {
"output": "OUTPUT1",
"dimmable": True,
"transition": 5000.0
}
}, ...
]
}
"""
data = {}
for connection in lcn_config[CONF_CONNECTIONS]:
host = {
CONF_HOST: connection[CONF_NAME],
CONF_IP_ADDRESS: connection[CONF_HOST],
CONF_PORT: connection[CONF_PORT],
CONF_USERNAME: connection[CONF_USERNAME],
CONF_PASSWORD: connection[CONF_PASSWORD],
CONF_SK_NUM_TRIES: connection[CONF_SK_NUM_TRIES],
CONF_DIM_MODE: connection[CONF_DIM_MODE],
CONF_ACKNOWLEDGE: False,
CONF_DEVICES: [],
CONF_ENTITIES: [],
}
data[connection[CONF_NAME]] = host
for confkey, domain_config in lcn_config.items():
if confkey == CONF_CONNECTIONS:
continue
domain = DOMAIN_LOOKUP[confkey]
# loop over entities in configuration.yaml
for domain_data in domain_config:
# remove name and address from domain_data
entity_name = domain_data.pop(CONF_NAME)
address, host_name = domain_data.pop(CONF_ADDRESS)
if host_name is None:
host_name = DEFAULT_NAME
# check if we have a new device config
for device_config in data[host_name][CONF_DEVICES]:
if address == device_config[CONF_ADDRESS]:
break
else: # create new device_config
device_config = {
CONF_ADDRESS: address,
CONF_NAME: "",
CONF_HARDWARE_SERIAL: -1,
CONF_SOFTWARE_SERIAL: -1,
CONF_HARDWARE_TYPE: -1,
}
data[host_name][CONF_DEVICES].append(device_config)
# insert entity config
resource = get_resource(domain, domain_data).lower()
for entity_config in data[host_name][CONF_ENTITIES]:
if (
address == entity_config[CONF_ADDRESS]
and resource == entity_config[CONF_RESOURCE]
and domain == entity_config[CONF_DOMAIN]
):
break
else: # create new entity_config
entity_config = {
CONF_ADDRESS: address,
CONF_NAME: entity_name,
CONF_RESOURCE: resource,
CONF_DOMAIN: domain,
CONF_DOMAIN_DATA: domain_data.copy(),
}
data[host_name][CONF_ENTITIES].append(entity_config)
return list(data.values())
def purge_entity_registry(
hass: HomeAssistant, entry_id: str, imported_entry_data: ConfigType
) -> None:
@@ -436,26 +320,6 @@ def get_device_config(
return None
def has_unique_host_names(hosts: list[ConfigType]) -> list[ConfigType]:
"""Validate that all connection names are unique.
Use 'pchk' as default connection_name (or add a numeric suffix if
pchk' is already in use.
"""
suffix = 0
for host in hosts:
if host.get(CONF_NAME) is None:
if suffix == 0:
host[CONF_NAME] = DEFAULT_NAME
else:
host[CONF_NAME] = f"{DEFAULT_NAME}{suffix:d}"
suffix += 1
schema = vol.Schema(vol.Unique())
schema([host.get(CONF_NAME) for host in hosts])
return hosts
def is_address(value: str) -> tuple[AddressType, str]:
"""Validate the given address string.
+1 -1
View File
@@ -8,5 +8,5 @@
"documentation": "https://www.home-assistant.io/integrations/lcn",
"iot_class": "local_push",
"loggers": ["pypck"],
"requirements": ["pypck==0.7.24", "lcn-frontend==0.2.1"]
"requirements": ["pypck==0.7.24", "lcn-frontend==0.2.2"]
}
-12
View File
@@ -63,18 +63,6 @@
}
},
"issues": {
"authentication_error": {
"title": "Authentication failed.",
"description": "Configuring LCN using YAML is being removed but there was an error importing your YAML configuration.\n\nEnsure username and password are correct.\n\nConsider removing the LCN YAML configuration from your configuration.yaml file and continue to [set up the integration]({url}) manually."
},
"license_error": {
"title": "Maximum number of connections was reached.",
"description": "Configuring LCN using YAML is being removed but there was an error importing your YAML configuration.\n\nEnsure sufficient PCHK licenses are registered and restart Home Assistant.\n\nConsider removing the LCN YAML configuration from your configuration.yaml file and continue to [set up the integration]({url}) manually."
},
"connection_refused": {
"title": "Unable to connect to PCHK.",
"description": "Configuring LCN using YAML is being removed but there was an error importing your YAML configuration.\n\nEnsure the connection (IP and port) to the LCN bus coupler is correct.\n\nConsider removing the LCN YAML configuration from your configuration.yaml file and continue to [set up the integration]({url}) manually."
},
"deprecated_regulatorlock_sensor": {
"title": "Deprecated LCN regulator lock binary sensor",
"description": "Your LCN regulator lock binary sensor entity `{entity}` is beeing used in automations or scripts. A regulator lock switch entity is available and should be used going forward.\n\nPlease adjust your automations or scripts to fix this issue."
@@ -7,6 +7,6 @@
"integration_type": "hub",
"iot_class": "local_polling",
"loggers": ["linkplay"],
"requirements": ["python-linkplay==0.0.18"],
"requirements": ["python-linkplay==0.0.20"],
"zeroconf": ["_linkplay._tcp.local."]
}
@@ -5,5 +5,5 @@
"documentation": "https://www.home-assistant.io/integrations/matrix",
"iot_class": "cloud_push",
"loggers": ["matrix_client"],
"requirements": ["matrix-nio==0.25.2", "Pillow==10.4.0"]
"requirements": ["matrix-nio==0.25.2", "Pillow==11.0.0"]
}
@@ -18,7 +18,7 @@ from homeassistant.components.media_player import (
from homeassistant.components.websocket_api import ActiveConnection
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.frame import report
from homeassistant.helpers.frame import report_usage
from homeassistant.helpers.integration_platform import (
async_process_integration_platforms,
)
@@ -156,7 +156,7 @@ async def async_resolve_media(
raise Unresolvable("Media Source not loaded")
if target_media_player is UNDEFINED:
report(
report_usage(
"calls media_source.async_resolve_media without passing an entity_id",
exclude_integrations={DOMAIN},
)
@@ -251,8 +251,8 @@ class PowerwallConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle reauth confirmation."""
errors: dict[str, str] | None = {}
description_placeholders: dict[str, str] = {}
reauth_entry = self._get_reauth_entry()
if user_input is not None:
reauth_entry = self._get_reauth_entry()
errors, _, description_placeholders = await self._async_try_connect(
{CONF_IP_ADDRESS: reauth_entry.data[CONF_IP_ADDRESS], **user_input}
)
@@ -261,6 +261,10 @@ class PowerwallConfigFlow(ConfigFlow, domain=DOMAIN):
reauth_entry, data_updates=user_input
)
self.context["title_placeholders"] = {
"name": reauth_entry.title,
"ip_address": reauth_entry.data[CONF_IP_ADDRESS],
}
return self.async_show_form(
step_id="reauth_confirm",
data_schema=vol.Schema({vol.Optional(CONF_PASSWORD): str}),
+1 -1
View File
@@ -3,5 +3,5 @@
"name": "Camera Proxy",
"codeowners": [],
"documentation": "https://www.home-assistant.io/integrations/proxy",
"requirements": ["Pillow==10.4.0"]
"requirements": ["Pillow==11.0.0"]
}
@@ -5,5 +5,5 @@
"documentation": "https://www.home-assistant.io/integrations/qrcode",
"iot_class": "calculated",
"loggers": ["pyzbar"],
"requirements": ["Pillow==10.4.0", "pyzbar==0.1.7"]
"requirements": ["Pillow==11.0.0", "pyzbar==0.1.7"]
}
+3 -3
View File
@@ -16,7 +16,7 @@ from sqlalchemy.pool import (
StaticPool,
)
from homeassistant.helpers.frame import report
from homeassistant.helpers.frame import ReportBehavior, report_usage
from homeassistant.util.loop import raise_for_blocking_call
_LOGGER = logging.getLogger(__name__)
@@ -108,14 +108,14 @@ class RecorderPool(SingletonThreadPool, NullPool):
# raise_for_blocking_call will raise an exception
def _do_get_db_connection_protected(self) -> ConnectionPoolEntry:
report(
report_usage(
(
"accesses the database without the database executor; "
f"{ADVISE_MSG} "
"for faster database operations"
),
exclude_integrations={"recorder"},
error_if_core=False,
core_behavior=ReportBehavior.LOG,
)
return NullPool._create_connection(self) # noqa: SLF001
@@ -134,7 +134,6 @@ STATISTIC_UNIT_TO_UNIT_CONVERTER: dict[str | None, type[BaseUnitConverter]] = {
for unit in BloodGlugoseConcentrationConverter.VALID_UNITS
},
**{unit: ConductivityConverter for unit in ConductivityConverter.VALID_UNITS},
**{unit: ConductivityConverter for unit in ConductivityConverter.VALID_UNITS},
**{unit: DataRateConverter for unit in DataRateConverter.VALID_UNITS},
**{unit: DistanceConverter for unit in DistanceConverter.VALID_UNITS},
**{unit: DurationConverter for unit in DurationConverter.VALID_UNITS},
@@ -17,7 +17,7 @@ RUSSOUND_RIO_EXCEPTIONS = (
)
CONNECT_TIMEOUT = 5
CONNECT_TIMEOUT = 15
MP_FEATURES_BY_FLAG = {
FeatureFlag.COMMANDS_ZONE_MUTE_OFF_ON: MediaPlayerEntityFeature.VOLUME_MUTE
@@ -7,5 +7,5 @@
"iot_class": "local_push",
"loggers": ["aiorussound"],
"quality_scale": "silver",
"requirements": ["aiorussound==4.0.5"]
"requirements": ["aiorussound==4.1.0"]
}
@@ -5,7 +5,7 @@ from __future__ import annotations
import logging
from aiorussound import Controller
from aiorussound.models import Source
from aiorussound.models import PlayStatus, Source
from aiorussound.rio import ZoneControlSurface
from homeassistant.components.media_player import (
@@ -132,20 +132,18 @@ class RussoundZoneDevice(RussoundBaseEntity, MediaPlayerEntity):
def state(self) -> MediaPlayerState | None:
"""Return the state of the device."""
status = self._zone.status
mode = self._source.mode
if status == "ON":
if mode == "playing":
return MediaPlayerState.PLAYING
if mode == "paused":
return MediaPlayerState.PAUSED
if mode == "transitioning":
return MediaPlayerState.BUFFERING
if mode == "stopped":
return MediaPlayerState.IDLE
return MediaPlayerState.ON
if status == "OFF":
play_status = self._source.play_status
if not status:
return MediaPlayerState.OFF
return None
if play_status == PlayStatus.PLAYING:
return MediaPlayerState.PLAYING
if play_status == PlayStatus.PAUSED:
return MediaPlayerState.PAUSED
if play_status == PlayStatus.TRANSITIONING:
return MediaPlayerState.BUFFERING
if play_status == PlayStatus.STOPPED:
return MediaPlayerState.IDLE
return MediaPlayerState.ON
@property
def source(self):
@@ -184,7 +182,7 @@ class RussoundZoneDevice(RussoundBaseEntity, MediaPlayerEntity):
Value is returned based on a range (0..50).
Therefore float divide by 50 to get to the required range.
"""
return float(self._zone.volume or "0") / 50.0
return self._zone.volume / 50.0
@command
async def async_turn_off(self) -> None:
@@ -4,5 +4,5 @@
"codeowners": ["@fabaff"],
"documentation": "https://www.home-assistant.io/integrations/seven_segments",
"iot_class": "local_polling",
"requirements": ["Pillow==10.4.0"]
"requirements": ["Pillow==11.0.0"]
}
@@ -5,5 +5,5 @@
"documentation": "https://www.home-assistant.io/integrations/sighthound",
"iot_class": "cloud_polling",
"loggers": ["simplehound"],
"requirements": ["Pillow==10.4.0", "simplehound==0.3"]
"requirements": ["Pillow==11.0.0", "simplehound==0.3"]
}
@@ -9,6 +9,6 @@
"iot_class": "cloud_polling",
"loggers": ["spotipy"],
"quality_scale": "silver",
"requirements": ["spotifyaio==0.8.7"],
"requirements": ["spotifyaio==0.8.8"],
"zeroconf": ["_spotify-connect._tcp.local."]
}
@@ -85,6 +85,9 @@ def make_device_data(
"Meter",
"MeterPlus",
"WoIOSensor",
"Hub 2",
"MeterPro",
"MeterPro(CO2)",
]:
devices_data.sensors.append(
prepare_device(hass, api, device, coordinators_by_id)
@@ -9,7 +9,11 @@ from homeassistant.components.sensor import (
SensorStateClass,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import PERCENTAGE, UnitOfTemperature
from homeassistant.const import (
CONCENTRATION_PARTS_PER_MILLION,
PERCENTAGE,
UnitOfTemperature,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
@@ -21,6 +25,7 @@ from .entity import SwitchBotCloudEntity
SENSOR_TYPE_TEMPERATURE = "temperature"
SENSOR_TYPE_HUMIDITY = "humidity"
SENSOR_TYPE_BATTERY = "battery"
SENSOR_TYPE_CO2 = "CO2"
METER_PLUS_SENSOR_DESCRIPTIONS = (
SensorEntityDescription(
@@ -43,6 +48,16 @@ METER_PLUS_SENSOR_DESCRIPTIONS = (
),
)
METER_PRO_CO2_SENSOR_DESCRIPTIONS = (
*METER_PLUS_SENSOR_DESCRIPTIONS,
SensorEntityDescription(
key=SENSOR_TYPE_CO2,
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.CO2,
),
)
async def async_setup_entry(
hass: HomeAssistant,
@@ -55,7 +70,11 @@ async def async_setup_entry(
async_add_entities(
SwitchBotCloudSensor(data.api, device, coordinator, description)
for device, coordinator in data.devices.sensors
for description in METER_PLUS_SENSOR_DESCRIPTIONS
for description in (
METER_PRO_CO2_SENSOR_DESCRIPTIONS
if device.device_type == "MeterPro(CO2)"
else METER_PLUS_SENSOR_DESCRIPTIONS
)
)
@@ -10,6 +10,6 @@
"tf-models-official==2.5.0",
"pycocotools==2.0.6",
"numpy==2.1.3",
"Pillow==10.4.0"
"Pillow==11.0.0"
]
}
+10 -3
View File
@@ -146,14 +146,21 @@ class DeviceListener(SharingDeviceListener):
self.hass = hass
self.manager = manager
def update_device(self, device: CustomerDevice) -> None:
def update_device(
self, device: CustomerDevice, updated_status_properties: list[str] | None
) -> None:
"""Update device status."""
LOGGER.debug(
"Received update for device %s: %s",
"Received update for device %s: %s (updated properties: %s)",
device.id,
self.manager.device_map[device.id].status,
updated_status_properties,
)
dispatcher_send(
self.hass,
f"{TUYA_HA_SIGNAL_UPDATE_ENTITY}_{device.id}",
updated_status_properties,
)
dispatcher_send(self.hass, f"{TUYA_HA_SIGNAL_UPDATE_ENTITY}_{device.id}")
def add_device(self, device: CustomerDevice) -> None:
"""Add device added listener."""
+6 -1
View File
@@ -283,10 +283,15 @@ class TuyaEntity(Entity):
async_dispatcher_connect(
self.hass,
f"{TUYA_HA_SIGNAL_UPDATE_ENTITY}_{self.device.id}",
self.async_write_ha_state,
self._handle_state_update,
)
)
async def _handle_state_update(
self, updated_status_properties: list[str] | None
) -> None:
self.async_write_ha_state()
def _send_command(self, commands: list[dict[str, Any]]) -> None:
"""Send command to the device."""
LOGGER.debug("Sending commands for device %s: %s", self.device.id, commands)
+1 -1
View File
@@ -43,5 +43,5 @@
"integration_type": "hub",
"iot_class": "cloud_push",
"loggers": ["tuya_iot"],
"requirements": ["tuya-device-sharing-sdk==0.1.9"]
"requirements": ["tuya-device-sharing-sdk==0.2.1"]
}
@@ -6,5 +6,5 @@
"dependencies": ["application_credentials"],
"documentation": "https://www.home-assistant.io/integrations/weheat",
"iot_class": "cloud_polling",
"requirements": ["weheat==2024.09.23"]
"requirements": ["weheat==2024.11.02"]
}
+3 -3
View File
@@ -4,7 +4,7 @@ from typing import Any
import zeroconf
from homeassistant.helpers.frame import report
from homeassistant.helpers.frame import ReportBehavior, report_usage
from .models import HaZeroconf
@@ -16,14 +16,14 @@ def install_multiple_zeroconf_catcher(hass_zc: HaZeroconf) -> None:
"""
def new_zeroconf_new(self: zeroconf.Zeroconf, *k: Any, **kw: Any) -> HaZeroconf:
report(
report_usage(
(
"attempted to create another Zeroconf instance. Please use the shared"
" Zeroconf via await"
" homeassistant.components.zeroconf.async_get_instance(hass)"
),
exclude_integrations={"zeroconf"},
error_if_core=False,
core_behavior=ReportBehavior.LOG,
)
return hass_zc
+2 -2
View File
@@ -44,7 +44,7 @@ mutagen==1.47.0
orjson==3.10.11
packaging>=23.1
paho-mqtt==1.6.1
Pillow==10.4.0
Pillow==11.0.0
propcache==0.2.0
psutil-home-assistant==0.0.1
PyJWT==2.9.0
@@ -127,7 +127,7 @@ backoff>=2.0
# Required to avoid breaking (#101042).
# v2 has breaking changes (#99218).
pydantic==1.10.18
pydantic==1.10.19
# Required for Python 3.12.4 compatibility (#119223).
mashumaro>=3.13.1
-63
View File
@@ -25,7 +25,6 @@ except ImportError:
from propcache import cached_property
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.frame import report
from .const import SECRET_YAML
from .objects import Input, NodeDictClass, NodeListClass, NodeStrClass
@@ -144,37 +143,6 @@ class FastSafeLoader(FastestAvailableSafeLoader, _LoaderMixin):
self.secrets = secrets
class SafeLoader(FastSafeLoader):
"""Provided for backwards compatibility. Logs when instantiated."""
def __init__(*args: Any, **kwargs: Any) -> None:
"""Log a warning and call super."""
SafeLoader.__report_deprecated()
FastSafeLoader.__init__(*args, **kwargs)
@classmethod
def add_constructor(cls, tag: str, constructor: Callable) -> None:
"""Log a warning and call super."""
SafeLoader.__report_deprecated()
FastSafeLoader.add_constructor(tag, constructor)
@classmethod
def add_multi_constructor(
cls, tag_prefix: str, multi_constructor: Callable
) -> None:
"""Log a warning and call super."""
SafeLoader.__report_deprecated()
FastSafeLoader.add_multi_constructor(tag_prefix, multi_constructor)
@staticmethod
def __report_deprecated() -> None:
"""Log deprecation warning."""
report(
"uses deprecated 'SafeLoader' instead of 'FastSafeLoader', "
"which will stop working in HA Core 2024.6,"
)
class PythonSafeLoader(yaml.SafeLoader, _LoaderMixin):
"""Python safe loader."""
@@ -184,37 +152,6 @@ class PythonSafeLoader(yaml.SafeLoader, _LoaderMixin):
self.secrets = secrets
class SafeLineLoader(PythonSafeLoader):
"""Provided for backwards compatibility. Logs when instantiated."""
def __init__(*args: Any, **kwargs: Any) -> None:
"""Log a warning and call super."""
SafeLineLoader.__report_deprecated()
PythonSafeLoader.__init__(*args, **kwargs)
@classmethod
def add_constructor(cls, tag: str, constructor: Callable) -> None:
"""Log a warning and call super."""
SafeLineLoader.__report_deprecated()
PythonSafeLoader.add_constructor(tag, constructor)
@classmethod
def add_multi_constructor(
cls, tag_prefix: str, multi_constructor: Callable
) -> None:
"""Log a warning and call super."""
SafeLineLoader.__report_deprecated()
PythonSafeLoader.add_multi_constructor(tag_prefix, multi_constructor)
@staticmethod
def __report_deprecated() -> None:
"""Log deprecation warning."""
report(
"uses deprecated 'SafeLineLoader' instead of 'PythonSafeLoader', "
"which will stop working in HA Core 2024.6,"
)
type LoaderType = FastSafeLoader | PythonSafeLoader
+1 -1
View File
@@ -55,7 +55,7 @@ dependencies = [
"PyJWT==2.9.0",
# PyJWT has loose dependency. We want the latest one.
"cryptography==43.0.1",
"Pillow==10.4.0",
"Pillow==11.0.0",
"propcache==0.2.0",
"pyOpenSSL==24.2.1",
"orjson==3.10.11",
+1 -1
View File
@@ -27,7 +27,7 @@ Jinja2==3.1.4
lru-dict==1.3.0
PyJWT==2.9.0
cryptography==43.0.1
Pillow==10.4.0
Pillow==11.0.0
propcache==0.2.0
pyOpenSSL==24.2.1
orjson==3.10.11
+8 -8
View File
@@ -33,7 +33,7 @@ Mastodon.py==1.8.1
# homeassistant.components.seven_segments
# homeassistant.components.sighthound
# homeassistant.components.tensorflow
Pillow==10.4.0
Pillow==11.0.0
# homeassistant.components.plex
PlexAPI==4.15.16
@@ -357,7 +357,7 @@ aioridwell==2024.01.0
aioruckus==0.41
# homeassistant.components.russound_rio
aiorussound==4.0.5
aiorussound==4.1.0
# homeassistant.components.ruuvi_gateway
aioruuvigateway==0.1.0
@@ -735,7 +735,7 @@ debugpy==1.8.6
# decora==0.6
# homeassistant.components.ecovacs
deebot-client==8.4.0
deebot-client==8.4.1
# homeassistant.components.ihc
# homeassistant.components.namecheapdns
@@ -1268,7 +1268,7 @@ lakeside==0.13
laundrify-aio==1.2.2
# homeassistant.components.lcn
lcn-frontend==0.2.1
lcn-frontend==0.2.2
# homeassistant.components.ld2410_ble
ld2410-ble==0.1.1
@@ -2362,7 +2362,7 @@ python-juicenet==1.1.0
python-kasa[speedups]==0.7.7
# homeassistant.components.linkplay
python-linkplay==0.0.18
python-linkplay==0.0.20
# homeassistant.components.lirc
# python-lirc==1.2.3
@@ -2713,7 +2713,7 @@ speak2mary==1.4.0
speedtest-cli==2.1.3
# homeassistant.components.spotify
spotifyaio==0.8.7
spotifyaio==0.8.8
# homeassistant.components.sql
sqlparse==0.5.0
@@ -2873,7 +2873,7 @@ ttls==1.8.3
ttn_client==1.2.0
# homeassistant.components.tuya
tuya-device-sharing-sdk==0.1.9
tuya-device-sharing-sdk==0.2.1
# homeassistant.components.twentemilieu
twentemilieu==2.0.1
@@ -2987,7 +2987,7 @@ webio-api==0.1.8
webmin-xmlrpc==0.0.2
# homeassistant.components.weheat
weheat==2024.09.23
weheat==2024.11.02
# homeassistant.components.whirlpool
whirlpool-sixth-sense==0.18.8
+1 -1
View File
@@ -14,7 +14,7 @@ license-expression==30.4.0
mock-open==1.4.0
mypy-dev==1.14.0a2
pre-commit==4.0.0
pydantic==1.10.18
pydantic==1.10.19
pylint==3.3.1
pylint-per-file-ignores==1.3.2
pipdeptree==2.23.4
+8 -8
View File
@@ -33,7 +33,7 @@ Mastodon.py==1.8.1
# homeassistant.components.seven_segments
# homeassistant.components.sighthound
# homeassistant.components.tensorflow
Pillow==10.4.0
Pillow==11.0.0
# homeassistant.components.plex
PlexAPI==4.15.16
@@ -339,7 +339,7 @@ aioridwell==2024.01.0
aioruckus==0.41
# homeassistant.components.russound_rio
aiorussound==4.0.5
aiorussound==4.1.0
# homeassistant.components.ruuvi_gateway
aioruuvigateway==0.1.0
@@ -625,7 +625,7 @@ dbus-fast==2.24.3
debugpy==1.8.6
# homeassistant.components.ecovacs
deebot-client==8.4.0
deebot-client==8.4.1
# homeassistant.components.ihc
# homeassistant.components.namecheapdns
@@ -1064,7 +1064,7 @@ lacrosse-view==1.0.3
laundrify-aio==1.2.2
# homeassistant.components.lcn
lcn-frontend==0.2.1
lcn-frontend==0.2.2
# homeassistant.components.ld2410_ble
ld2410-ble==0.1.1
@@ -1889,7 +1889,7 @@ python-juicenet==1.1.0
python-kasa[speedups]==0.7.7
# homeassistant.components.linkplay
python-linkplay==0.0.18
python-linkplay==0.0.20
# homeassistant.components.matter
python-matter-server==6.6.0
@@ -2165,7 +2165,7 @@ speak2mary==1.4.0
speedtest-cli==2.1.3
# homeassistant.components.spotify
spotifyaio==0.8.7
spotifyaio==0.8.8
# homeassistant.components.sql
sqlparse==0.5.0
@@ -2286,7 +2286,7 @@ ttls==1.8.3
ttn_client==1.2.0
# homeassistant.components.tuya
tuya-device-sharing-sdk==0.1.9
tuya-device-sharing-sdk==0.2.1
# homeassistant.components.twentemilieu
twentemilieu==2.0.1
@@ -2382,7 +2382,7 @@ webio-api==0.1.8
webmin-xmlrpc==0.0.2
# homeassistant.components.weheat
weheat==2024.09.23
weheat==2024.11.02
# homeassistant.components.whirlpool
whirlpool-sixth-sense==0.18.8
+1 -1
View File
@@ -160,7 +160,7 @@ backoff>=2.0
# Required to avoid breaking (#101042).
# v2 has breaking changes (#99218).
pydantic==1.10.18
pydantic==1.10.19
# Required for Python 3.12.4 compatibility (#119223).
mashumaro>=3.13.1
+1
View File
@@ -84,6 +84,7 @@ OSI_APPROVED_LICENSES_SPDX = {
"LGPL-3.0-only",
"LGPL-3.0-or-later",
"MIT",
"MIT-CMU",
"MPL-1.1",
"MPL-2.0",
"PSF-2.0",
-4
View File
@@ -786,7 +786,6 @@ async def test_websocket_status(
"remote_enabled": False,
"cloud_ice_servers_enabled": True,
"tts_default_voice": ["en-US", "JennyNeural"],
"backup_sync_enabled": True,
},
"alexa_entities": {
"include_domains": [],
@@ -906,7 +905,6 @@ async def test_websocket_update_preferences(
assert cloud.client.prefs.google_secure_devices_pin is None
assert cloud.client.prefs.remote_allow_remote_enable is True
assert cloud.client.prefs.cloud_ice_servers_enabled is True
assert cloud.client.prefs.backup_sync_enabled is True
client = await hass_ws_client(hass)
@@ -919,7 +917,6 @@ async def test_websocket_update_preferences(
"tts_default_voice": ["en-GB", "RyanNeural"],
"remote_allow_remote_enable": False,
"cloud_ice_servers_enabled": False,
"backup_sync_enabled": False,
}
)
response = await client.receive_json()
@@ -930,7 +927,6 @@ async def test_websocket_update_preferences(
assert cloud.client.prefs.google_secure_devices_pin == "1234"
assert cloud.client.prefs.remote_allow_remote_enable is False
assert cloud.client.prefs.cloud_ice_servers_enabled is False
assert cloud.client.prefs.backup_sync_enabled is False
assert cloud.client.prefs.tts_default_voice == ("en-GB", "RyanNeural")
@@ -44,7 +44,7 @@ async def test_form_home(hass: HomeAssistant) -> None:
await hass.async_block_till_done()
assert result2["type"] is FlowResultType.CREATE_ENTRY
assert result2["title"] == "CO2 Signal"
assert result2["title"] == "Electricity Maps"
assert result2["data"] == {
"api_key": "api_key",
}
@@ -185,7 +185,7 @@ async def test_form_error_handling(
await hass.async_block_till_done()
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["title"] == "CO2 Signal"
assert result["title"] == "Electricity Maps"
assert result["data"] == {
"api_key": "api_key",
}
+17 -6
View File
@@ -11,6 +11,7 @@ from unittest.mock import AsyncMock, MagicMock, patch
from aiohasupervisor.models import (
Discovery,
NewBackup,
Repository,
ResolutionInfo,
StoreAddon,
@@ -417,13 +418,22 @@ def uninstall_addon_fixture(supervisor_client: AsyncMock) -> AsyncMock:
return supervisor_client.addons.uninstall_addon
@pytest.fixture(name="create_backup")
def create_backup_fixture() -> Generator[AsyncMock]:
"""Mock create backup."""
# pylint: disable-next=import-outside-toplevel
from .hassio.common import mock_create_backup
@pytest.fixture(name="create_partial_backup")
def create_partial_backup_fixture(supervisor_client: AsyncMock) -> AsyncMock:
"""Mock create partial backup."""
supervisor_client.backups.partial_backup.return_value = NewBackup(
"job123", "backup123"
)
return supervisor_client.backups.partial_backup
yield from mock_create_backup()
@pytest.fixture(name="create_full_backup")
def create_backup_fixture(supervisor_client: AsyncMock) -> AsyncMock:
"""Mock create full backup."""
supervisor_client.backups.full_backup.return_value = NewBackup(
"job123", "backup123"
)
return supervisor_client.backups.full_backup
@pytest.fixture(name="update_addon")
@@ -505,6 +515,7 @@ def supervisor_client() -> Generator[AsyncMock]:
"""Mock the supervisor client."""
supervisor_client = AsyncMock()
supervisor_client.addons = AsyncMock()
supervisor_client.backups = AsyncMock()
supervisor_client.discovery = AsyncMock()
supervisor_client.homeassistant = AsyncMock()
supervisor_client.os = AsyncMock()
+1 -10
View File
@@ -2,12 +2,11 @@
from __future__ import annotations
from collections.abc import Generator
from dataclasses import fields
import logging
from types import MethodType
from typing import Any
from unittest.mock import AsyncMock, Mock, patch
from unittest.mock import AsyncMock, Mock
from aiohasupervisor.models import (
AddonsOptions,
@@ -198,14 +197,6 @@ def mock_set_addon_options_side_effect(addon_options: dict[str, Any]) -> Any | N
return set_addon_options
def mock_create_backup() -> Generator[AsyncMock]:
"""Mock create backup."""
with patch(
"homeassistant.components.hassio.addon_manager.async_create_backup"
) as create_backup:
yield create_backup
def mock_addon_stats(supervisor_client: AsyncMock) -> AsyncMock:
"""Mock addon stats."""
supervisor_client.addons.addon_stats.return_value = addon_stats = Mock(
+35 -41
View File
@@ -8,7 +8,7 @@ from unittest.mock import AsyncMock, call
from uuid import uuid4
from aiohasupervisor import SupervisorError
from aiohasupervisor.models import AddonsOptions, Discovery
from aiohasupervisor.models import AddonsOptions, Discovery, PartialBackupOptions
import pytest
from homeassistant.components.hassio.addon_manager import (
@@ -17,7 +17,6 @@ from homeassistant.components.hassio.addon_manager import (
AddonManager,
AddonState,
)
from homeassistant.components.hassio.handler import HassioAPIError
from homeassistant.core import HomeAssistant
@@ -502,7 +501,7 @@ async def test_update_addon(
addon_manager: AddonManager,
addon_info: AsyncMock,
addon_installed: AsyncMock,
create_backup: AsyncMock,
create_partial_backup: AsyncMock,
update_addon: AsyncMock,
) -> None:
"""Test update addon."""
@@ -511,9 +510,8 @@ async def test_update_addon(
await addon_manager.async_update_addon()
assert addon_info.call_count == 2
assert create_backup.call_count == 1
assert create_backup.call_args == call(
hass, {"name": "addon_test_addon_1.0.0", "addons": ["test_addon"]}, partial=True
create_partial_backup.assert_called_once_with(
PartialBackupOptions(name="addon_test_addon_1.0.0", addons={"test_addon"})
)
assert update_addon.call_count == 1
@@ -522,7 +520,7 @@ async def test_update_addon_no_update(
addon_manager: AddonManager,
addon_info: AsyncMock,
addon_installed: AsyncMock,
create_backup: AsyncMock,
create_partial_backup: AsyncMock,
update_addon: AsyncMock,
) -> None:
"""Test update addon without update available."""
@@ -531,7 +529,7 @@ async def test_update_addon_no_update(
await addon_manager.async_update_addon()
assert addon_info.call_count == 1
assert create_backup.call_count == 0
assert create_partial_backup.call_count == 0
assert update_addon.call_count == 0
@@ -540,7 +538,7 @@ async def test_update_addon_error(
addon_manager: AddonManager,
addon_info: AsyncMock,
addon_installed: AsyncMock,
create_backup: AsyncMock,
create_partial_backup: AsyncMock,
update_addon: AsyncMock,
) -> None:
"""Test update addon raises error."""
@@ -553,9 +551,8 @@ async def test_update_addon_error(
assert str(err.value) == "Failed to update the Test add-on: Boom"
assert addon_info.call_count == 2
assert create_backup.call_count == 1
assert create_backup.call_args == call(
hass, {"name": "addon_test_addon_1.0.0", "addons": ["test_addon"]}, partial=True
create_partial_backup.assert_called_once_with(
PartialBackupOptions(name="addon_test_addon_1.0.0", addons={"test_addon"})
)
assert update_addon.call_count == 1
@@ -565,7 +562,7 @@ async def test_schedule_update_addon(
addon_manager: AddonManager,
addon_info: AsyncMock,
addon_installed: AsyncMock,
create_backup: AsyncMock,
create_partial_backup: AsyncMock,
update_addon: AsyncMock,
) -> None:
"""Test schedule update addon."""
@@ -591,9 +588,8 @@ async def test_schedule_update_addon(
assert addon_manager.task_in_progress() is False
assert addon_info.call_count == 3
assert create_backup.call_count == 1
assert create_backup.call_args == call(
hass, {"name": "addon_test_addon_1.0.0", "addons": ["test_addon"]}, partial=True
create_partial_backup.assert_called_once_with(
PartialBackupOptions(name="addon_test_addon_1.0.0", addons={"test_addon"})
)
assert update_addon.call_count == 1
@@ -607,15 +603,15 @@ async def test_schedule_update_addon(
@pytest.mark.parametrize(
(
"create_backup_error",
"create_backup_calls",
"create_partial_backup_error",
"create_partial_backup_calls",
"update_addon_error",
"update_addon_calls",
"error_message",
),
[
(
HassioAPIError("Boom"),
SupervisorError("Boom"),
1,
None,
0,
@@ -633,17 +629,17 @@ async def test_schedule_update_addon(
async def test_schedule_update_addon_error(
addon_manager: AddonManager,
addon_installed: AsyncMock,
create_backup: AsyncMock,
create_partial_backup: AsyncMock,
update_addon: AsyncMock,
create_backup_error: Exception | None,
create_backup_calls: int,
create_partial_backup_error: Exception | None,
create_partial_backup_calls: int,
update_addon_error: Exception | None,
update_addon_calls: int,
error_message: str,
) -> None:
"""Test schedule update addon raises error."""
addon_installed.return_value.update_available = True
create_backup.side_effect = create_backup_error
create_partial_backup.side_effect = create_partial_backup_error
update_addon.side_effect = update_addon_error
with pytest.raises(AddonError) as err:
@@ -651,21 +647,21 @@ async def test_schedule_update_addon_error(
assert str(err.value) == error_message
assert create_backup.call_count == create_backup_calls
assert create_partial_backup.call_count == create_partial_backup_calls
assert update_addon.call_count == update_addon_calls
@pytest.mark.parametrize(
(
"create_backup_error",
"create_backup_calls",
"create_partial_backup_error",
"create_partial_backup_calls",
"update_addon_error",
"update_addon_calls",
"error_log",
),
[
(
HassioAPIError("Boom"),
SupervisorError("Boom"),
1,
None,
0,
@@ -683,10 +679,10 @@ async def test_schedule_update_addon_error(
async def test_schedule_update_addon_logs_error(
addon_manager: AddonManager,
addon_installed: AsyncMock,
create_backup: AsyncMock,
create_partial_backup: AsyncMock,
update_addon: AsyncMock,
create_backup_error: Exception | None,
create_backup_calls: int,
create_partial_backup_error: Exception | None,
create_partial_backup_calls: int,
update_addon_error: Exception | None,
update_addon_calls: int,
error_log: str,
@@ -694,13 +690,13 @@ async def test_schedule_update_addon_logs_error(
) -> None:
"""Test schedule update addon logs error."""
addon_installed.return_value.update_available = True
create_backup.side_effect = create_backup_error
create_partial_backup.side_effect = create_partial_backup_error
update_addon.side_effect = update_addon_error
await addon_manager.async_schedule_update_addon(catch_error=True)
assert error_log in caplog.text
assert create_backup.call_count == create_backup_calls
assert create_partial_backup.call_count == create_partial_backup_calls
assert update_addon.call_count == update_addon_calls
@@ -709,15 +705,14 @@ async def test_create_backup(
addon_manager: AddonManager,
addon_info: AsyncMock,
addon_installed: AsyncMock,
create_backup: AsyncMock,
create_partial_backup: AsyncMock,
) -> None:
"""Test creating a backup of the addon."""
await addon_manager.async_create_backup()
assert addon_info.call_count == 1
assert create_backup.call_count == 1
assert create_backup.call_args == call(
hass, {"name": "addon_test_addon_1.0.0", "addons": ["test_addon"]}, partial=True
create_partial_backup.assert_called_once_with(
PartialBackupOptions(name="addon_test_addon_1.0.0", addons={"test_addon"})
)
@@ -726,10 +721,10 @@ async def test_create_backup_error(
addon_manager: AddonManager,
addon_info: AsyncMock,
addon_installed: AsyncMock,
create_backup: AsyncMock,
create_partial_backup: AsyncMock,
) -> None:
"""Test creating a backup of the addon raises error."""
create_backup.side_effect = HassioAPIError("Boom")
create_partial_backup.side_effect = SupervisorError("Boom")
with pytest.raises(AddonError) as err:
await addon_manager.async_create_backup()
@@ -737,9 +732,8 @@ async def test_create_backup_error(
assert str(err.value) == "Failed to create a backup of the Test add-on: Boom"
assert addon_info.call_count == 1
assert create_backup.call_count == 1
assert create_backup.call_args == call(
hass, {"name": "addon_test_addon_1.0.0", "addons": ["test_addon"]}, partial=True
create_partial_backup.assert_called_once_with(
PartialBackupOptions(name="addon_test_addon_1.0.0", addons={"test_addon"})
)
+26 -29
View File
@@ -7,6 +7,7 @@ from collections.abc import Generator
from unittest.mock import AsyncMock, MagicMock, call, patch
from aiohasupervisor import SupervisorError
from aiohasupervisor.models import PartialBackupOptions
from matter_server.client.exceptions import (
CannotConnect,
NotConnected,
@@ -16,7 +17,6 @@ from matter_server.client.exceptions import (
from matter_server.common.errors import MatterError
import pytest
from homeassistant.components.hassio import HassioAPIError
from homeassistant.components.matter.const import DOMAIN
from homeassistant.config_entries import ConfigEntryDisabler, ConfigEntryState
from homeassistant.const import STATE_UNAVAILABLE
@@ -377,7 +377,7 @@ async def test_addon_info_failure(
"update_calls",
"backup_calls",
"update_addon_side_effect",
"create_backup_side_effect",
"create_partial_backup_side_effect",
"connect_side_effect",
),
[
@@ -399,7 +399,7 @@ async def test_addon_info_failure(
0,
1,
None,
HassioAPIError("Boom"),
SupervisorError("Boom"),
ServerVersionTooOld("Invalid version"),
),
],
@@ -411,7 +411,7 @@ async def test_update_addon(
addon_info: AsyncMock,
install_addon: AsyncMock,
start_addon: AsyncMock,
create_backup: AsyncMock,
create_partial_backup: AsyncMock,
update_addon: AsyncMock,
matter_client: MagicMock,
addon_version: str,
@@ -419,13 +419,13 @@ async def test_update_addon(
update_calls: int,
backup_calls: int,
update_addon_side_effect: Exception | None,
create_backup_side_effect: Exception | None,
create_partial_backup_side_effect: Exception | None,
connect_side_effect: Exception,
) -> None:
"""Test update the Matter add-on during entry setup."""
addon_info.return_value.version = addon_version
addon_info.return_value.update_available = update_available
create_backup.side_effect = create_backup_side_effect
create_partial_backup.side_effect = create_partial_backup_side_effect
update_addon.side_effect = update_addon_side_effect
matter_client.connect.side_effect = connect_side_effect
entry = MockConfigEntry(
@@ -442,7 +442,7 @@ async def test_update_addon(
await hass.async_block_till_done()
assert entry.state is ConfigEntryState.SETUP_RETRY
assert create_backup.call_count == backup_calls
assert create_partial_backup.call_count == backup_calls
assert update_addon.call_count == update_calls
@@ -548,7 +548,7 @@ async def test_remove_entry(
hass: HomeAssistant,
addon_installed: AsyncMock,
stop_addon: AsyncMock,
create_backup: AsyncMock,
create_partial_backup: AsyncMock,
uninstall_addon: AsyncMock,
caplog: pytest.LogCaptureFixture,
) -> None:
@@ -581,18 +581,17 @@ async def test_remove_entry(
assert stop_addon.call_count == 1
assert stop_addon.call_args == call("core_matter_server")
assert create_backup.call_count == 1
assert create_backup.call_args == call(
hass,
{"name": "addon_core_matter_server_1.0.0", "addons": ["core_matter_server"]},
partial=True,
create_partial_backup.assert_called_once_with(
PartialBackupOptions(
name="addon_core_matter_server_1.0.0", addons={"core_matter_server"}
)
)
assert uninstall_addon.call_count == 1
assert uninstall_addon.call_args == call("core_matter_server")
assert entry.state is ConfigEntryState.NOT_LOADED
assert len(hass.config_entries.async_entries(DOMAIN)) == 0
stop_addon.reset_mock()
create_backup.reset_mock()
create_partial_backup.reset_mock()
uninstall_addon.reset_mock()
# test add-on stop failure
@@ -604,38 +603,37 @@ async def test_remove_entry(
assert stop_addon.call_count == 1
assert stop_addon.call_args == call("core_matter_server")
assert create_backup.call_count == 0
assert create_partial_backup.call_count == 0
assert uninstall_addon.call_count == 0
assert entry.state is ConfigEntryState.NOT_LOADED
assert len(hass.config_entries.async_entries(DOMAIN)) == 0
assert "Failed to stop the Matter Server add-on" in caplog.text
stop_addon.side_effect = None
stop_addon.reset_mock()
create_backup.reset_mock()
create_partial_backup.reset_mock()
uninstall_addon.reset_mock()
# test create backup failure
entry.add_to_hass(hass)
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
create_backup.side_effect = HassioAPIError()
create_partial_backup.side_effect = SupervisorError()
await hass.config_entries.async_remove(entry.entry_id)
assert stop_addon.call_count == 1
assert stop_addon.call_args == call("core_matter_server")
assert create_backup.call_count == 1
assert create_backup.call_args == call(
hass,
{"name": "addon_core_matter_server_1.0.0", "addons": ["core_matter_server"]},
partial=True,
create_partial_backup.assert_called_once_with(
PartialBackupOptions(
name="addon_core_matter_server_1.0.0", addons={"core_matter_server"}
)
)
assert uninstall_addon.call_count == 0
assert entry.state is ConfigEntryState.NOT_LOADED
assert len(hass.config_entries.async_entries(DOMAIN)) == 0
assert "Failed to create a backup of the Matter Server add-on" in caplog.text
create_backup.side_effect = None
create_partial_backup.side_effect = None
stop_addon.reset_mock()
create_backup.reset_mock()
create_partial_backup.reset_mock()
uninstall_addon.reset_mock()
# test add-on uninstall failure
@@ -647,11 +645,10 @@ async def test_remove_entry(
assert stop_addon.call_count == 1
assert stop_addon.call_args == call("core_matter_server")
assert create_backup.call_count == 1
assert create_backup.call_args == call(
hass,
{"name": "addon_core_matter_server_1.0.0", "addons": ["core_matter_server"]},
partial=True,
create_partial_backup.assert_called_once_with(
PartialBackupOptions(
name="addon_core_matter_server_1.0.0", addons={"core_matter_server"}
)
)
assert uninstall_addon.call_count == 1
assert uninstall_addon.call_args == call("core_matter_server")
@@ -339,6 +339,11 @@ async def test_form_reauth(hass: HomeAssistant) -> None:
result = await entry.start_reauth_flow(hass)
assert result["type"] is FlowResultType.FORM
assert result["errors"] == {}
flow = hass.config_entries.flow.async_get(result["flow_id"])
assert flow["context"]["title_placeholders"] == {
"ip_address": VALID_CONFIG[CONF_IP_ADDRESS],
"name": entry.title,
}
mock_powerwall = await _mock_powerwall_site_name(hass, "My site")
@@ -2,7 +2,7 @@
from unittest.mock import AsyncMock
from aiorussound.models import CallbackType
from aiorussound.models import CallbackType, PlayStatus
import pytest
from homeassistant.const import (
@@ -28,29 +28,29 @@ async def mock_state_update(client: AsyncMock) -> None:
@pytest.mark.parametrize(
("zone_status", "source_mode", "media_player_state"),
("zone_status", "source_play_status", "media_player_state"),
[
("ON", None, STATE_ON),
("ON", "playing", STATE_PLAYING),
("ON", "paused", STATE_PAUSED),
("ON", "transitioning", STATE_BUFFERING),
("ON", "stopped", STATE_IDLE),
("OFF", None, STATE_OFF),
("OFF", "stopped", STATE_OFF),
(True, None, STATE_ON),
(True, PlayStatus.PLAYING, STATE_PLAYING),
(True, PlayStatus.PAUSED, STATE_PAUSED),
(True, PlayStatus.TRANSITIONING, STATE_BUFFERING),
(True, PlayStatus.STOPPED, STATE_IDLE),
(False, None, STATE_OFF),
(False, PlayStatus.STOPPED, STATE_OFF),
],
)
async def test_entity_state(
hass: HomeAssistant,
mock_russound_client: AsyncMock,
mock_config_entry: MockConfigEntry,
zone_status: str,
source_mode: str | None,
zone_status: bool,
source_play_status: PlayStatus | None,
media_player_state: str,
) -> None:
"""Test media player state."""
await setup_integration(hass, mock_config_entry)
mock_russound_client.controllers[1].zones[1].status = zone_status
mock_russound_client.sources[1].mode = source_mode
mock_russound_client.sources[1].play_status = source_play_status
await mock_state_update(mock_russound_client)
await hass.async_block_till_done()
@@ -50,6 +50,18 @@ async def test_setup_entry_success(
remoteType="DIY Plug",
hubDeviceId="test-hub-id",
),
Remote(
deviceId="meter-pro-1",
deviceName="meter-pro-name-1",
deviceType="MeterPro(CO2)",
hubDeviceId="test-hub-id",
),
Remote(
deviceId="hub2-1",
deviceName="hub2-name-1",
deviceType="Hub 2",
hubDeviceId="test-hub-id",
),
]
mock_get_status.return_value = {"power": PowerState.ON.value}
entry = configure_integration(hass)
+20 -30
View File
@@ -6,7 +6,7 @@ import logging
from unittest.mock import AsyncMock, call, patch
from aiohasupervisor import SupervisorError
from aiohasupervisor.models import AddonsOptions
from aiohasupervisor.models import AddonsOptions, PartialBackupOptions
import pytest
from zwave_js_server.client import Client
from zwave_js_server.event import Event
@@ -14,7 +14,6 @@ from zwave_js_server.exceptions import BaseZwaveJSServerError, InvalidServerVers
from zwave_js_server.model.node import Node
from zwave_js_server.model.version import VersionInfo
from homeassistant.components.hassio import HassioAPIError
from homeassistant.components.logger import DOMAIN as LOGGER_DOMAIN, SERVICE_SET_LEVEL
from homeassistant.components.persistent_notification import async_dismiss
from homeassistant.components.zwave_js import DOMAIN
@@ -743,13 +742,13 @@ async def test_addon_options_changed(
"update_calls",
"backup_calls",
"update_addon_side_effect",
"create_backup_side_effect",
"create_partial_backup_side_effect",
),
[
("1.0.0", True, 1, 1, None, None),
("1.0.0", False, 0, 0, None, None),
("1.0.0", True, 1, 1, SupervisorError("Boom"), None),
("1.0.0", True, 0, 1, None, HassioAPIError("Boom")),
("1.0.0", True, 0, 1, None, SupervisorError("Boom")),
],
)
async def test_update_addon(
@@ -758,7 +757,7 @@ async def test_update_addon(
addon_info,
addon_installed,
addon_running,
create_backup,
create_partial_backup,
update_addon,
addon_options,
addon_version,
@@ -766,7 +765,7 @@ async def test_update_addon(
update_calls,
backup_calls,
update_addon_side_effect,
create_backup_side_effect,
create_partial_backup_side_effect,
version_state,
) -> None:
"""Test update the Z-Wave JS add-on during entry setup."""
@@ -776,7 +775,7 @@ async def test_update_addon(
addon_options["network_key"] = network_key
addon_info.return_value.version = addon_version
addon_info.return_value.update_available = update_available
create_backup.side_effect = create_backup_side_effect
create_partial_backup.side_effect = create_partial_backup_side_effect
update_addon.side_effect = update_addon_side_effect
client.connect.side_effect = InvalidServerVersion(
VersionInfo("a", "b", 1, 1, 1), 1, "Invalid version"
@@ -797,7 +796,7 @@ async def test_update_addon(
await hass.async_block_till_done()
assert entry.state is ConfigEntryState.SETUP_RETRY
assert create_backup.call_count == backup_calls
assert create_partial_backup.call_count == backup_calls
assert update_addon.call_count == update_calls
@@ -897,7 +896,7 @@ async def test_remove_entry(
hass: HomeAssistant,
addon_installed,
stop_addon,
create_backup,
create_partial_backup,
uninstall_addon,
caplog: pytest.LogCaptureFixture,
) -> None:
@@ -930,18 +929,15 @@ async def test_remove_entry(
assert stop_addon.call_count == 1
assert stop_addon.call_args == call("core_zwave_js")
assert create_backup.call_count == 1
assert create_backup.call_args == call(
hass,
{"name": "addon_core_zwave_js_1.0.0", "addons": ["core_zwave_js"]},
partial=True,
create_partial_backup.assert_called_once_with(
PartialBackupOptions(name="addon_core_zwave_js_1.0.0", addons={"core_zwave_js"})
)
assert uninstall_addon.call_count == 1
assert uninstall_addon.call_args == call("core_zwave_js")
assert entry.state is ConfigEntryState.NOT_LOADED
assert len(hass.config_entries.async_entries(DOMAIN)) == 0
stop_addon.reset_mock()
create_backup.reset_mock()
create_partial_backup.reset_mock()
uninstall_addon.reset_mock()
# test add-on stop failure
@@ -953,38 +949,35 @@ async def test_remove_entry(
assert stop_addon.call_count == 1
assert stop_addon.call_args == call("core_zwave_js")
assert create_backup.call_count == 0
assert create_partial_backup.call_count == 0
assert uninstall_addon.call_count == 0
assert entry.state is ConfigEntryState.NOT_LOADED
assert len(hass.config_entries.async_entries(DOMAIN)) == 0
assert "Failed to stop the Z-Wave JS add-on" in caplog.text
stop_addon.side_effect = None
stop_addon.reset_mock()
create_backup.reset_mock()
create_partial_backup.reset_mock()
uninstall_addon.reset_mock()
# test create backup failure
entry.add_to_hass(hass)
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
create_backup.side_effect = HassioAPIError()
create_partial_backup.side_effect = SupervisorError()
await hass.config_entries.async_remove(entry.entry_id)
assert stop_addon.call_count == 1
assert stop_addon.call_args == call("core_zwave_js")
assert create_backup.call_count == 1
assert create_backup.call_args == call(
hass,
{"name": "addon_core_zwave_js_1.0.0", "addons": ["core_zwave_js"]},
partial=True,
create_partial_backup.assert_called_once_with(
PartialBackupOptions(name="addon_core_zwave_js_1.0.0", addons={"core_zwave_js"})
)
assert uninstall_addon.call_count == 0
assert entry.state is ConfigEntryState.NOT_LOADED
assert len(hass.config_entries.async_entries(DOMAIN)) == 0
assert "Failed to create a backup of the Z-Wave JS add-on" in caplog.text
create_backup.side_effect = None
create_partial_backup.side_effect = None
stop_addon.reset_mock()
create_backup.reset_mock()
create_partial_backup.reset_mock()
uninstall_addon.reset_mock()
# test add-on uninstall failure
@@ -996,11 +989,8 @@ async def test_remove_entry(
assert stop_addon.call_count == 1
assert stop_addon.call_args == call("core_zwave_js")
assert create_backup.call_count == 1
assert create_backup.call_args == call(
hass,
{"name": "addon_core_zwave_js_1.0.0", "addons": ["core_zwave_js"]},
partial=True,
create_partial_backup.assert_called_once_with(
PartialBackupOptions(name="addon_core_zwave_js_1.0.0", addons={"core_zwave_js"})
)
assert uninstall_addon.call_count == 1
assert uninstall_addon.call_args == call("core_zwave_js")
-25
View File
@@ -494,31 +494,6 @@ def mock_integration_frame() -> Generator[Mock]:
yield correct_frame
@pytest.mark.parametrize(
("loader_class", "message"),
[
(yaml.loader.SafeLoader, "'SafeLoader' instead of 'FastSafeLoader'"),
(
yaml.loader.SafeLineLoader,
"'SafeLineLoader' instead of 'PythonSafeLoader'",
),
],
)
@pytest.mark.usefixtures("mock_integration_frame")
async def test_deprecated_loaders(
caplog: pytest.LogCaptureFixture,
loader_class: type,
message: str,
) -> None:
"""Test instantiating the deprecated yaml loaders logs a warning."""
with (
pytest.raises(TypeError),
patch("homeassistant.helpers.frame._REPORTED_INTEGRATIONS", set()),
):
loader_class()
assert (f"Detected that integration 'hue' uses deprecated {message}") in caplog.text
@pytest.mark.usefixtures("try_both_loaders")
def test_string_annotated() -> None:
"""Test strings are annotated with file + line."""