This commit is contained in:
Paulus Schoutsen
2025-08-21 20:22:43 +02:00
committed by GitHub
71 changed files with 1154 additions and 194 deletions

View File

@@ -37,7 +37,7 @@ on:
type: boolean
env:
CACHE_VERSION: 4
CACHE_VERSION: 6
UV_CACHE_VERSION: 1
MYPY_CACHE_VERSION: 1
HA_SHORT_VERSION: "2025.8"

View File

@@ -1,11 +1,11 @@
"""Alexa Devices integration."""
from homeassistant.const import Platform
from homeassistant.const import CONF_COUNTRY, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import aiohttp_client, config_validation as cv
from homeassistant.helpers.typing import ConfigType
from .const import DOMAIN
from .const import _LOGGER, COUNTRY_DOMAINS, DOMAIN
from .coordinator import AmazonConfigEntry, AmazonDevicesCoordinator
from .services import async_setup_services
@@ -40,6 +40,32 @@ async def async_setup_entry(hass: HomeAssistant, entry: AmazonConfigEntry) -> bo
return True
async def async_migrate_entry(hass: HomeAssistant, entry: AmazonConfigEntry) -> bool:
"""Migrate old entry."""
if entry.version == 1 and entry.minor_version == 0:
_LOGGER.debug(
"Migrating from version %s.%s", entry.version, entry.minor_version
)
# Convert country in domain
country = entry.data[CONF_COUNTRY]
domain = COUNTRY_DOMAINS.get(country, country)
# Save domain and remove country
new_data = entry.data.copy()
new_data.update({"site": f"https://www.amazon.{domain}"})
hass.config_entries.async_update_entry(
entry, data=new_data, version=1, minor_version=1
)
_LOGGER.info(
"Migration to version %s.%s successful", entry.version, entry.minor_version
)
return True
async def async_unload_entry(hass: HomeAssistant, entry: AmazonConfigEntry) -> bool:
"""Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)

View File

@@ -10,16 +10,14 @@ from aioamazondevices.exceptions import (
CannotAuthenticate,
CannotConnect,
CannotRetrieveData,
WrongCountry,
)
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_CODE, CONF_COUNTRY, CONF_PASSWORD, CONF_USERNAME
from homeassistant.const import CONF_CODE, CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
from homeassistant.helpers import aiohttp_client
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.selector import CountrySelector
from .const import CONF_LOGIN_DATA, DOMAIN
@@ -37,7 +35,6 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str,
session = aiohttp_client.async_create_clientsession(hass)
api = AmazonEchoApi(
session,
data[CONF_COUNTRY],
data[CONF_USERNAME],
data[CONF_PASSWORD],
)
@@ -48,6 +45,9 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str,
class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Alexa Devices."""
VERSION = 1
MINOR_VERSION = 1
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
@@ -62,8 +62,6 @@ class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN):
errors["base"] = "invalid_auth"
except CannotRetrieveData:
errors["base"] = "cannot_retrieve_data"
except WrongCountry:
errors["base"] = "wrong_country"
else:
await self.async_set_unique_id(data["customer_info"]["user_id"])
self._abort_if_unique_id_configured()
@@ -78,9 +76,6 @@ class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN):
errors=errors,
data_schema=vol.Schema(
{
vol.Required(
CONF_COUNTRY, default=self.hass.config.country
): CountrySelector(),
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Required(CONF_CODE): cv.string,

View File

@@ -6,3 +6,22 @@ _LOGGER = logging.getLogger(__package__)
DOMAIN = "alexa_devices"
CONF_LOGIN_DATA = "login_data"
DEFAULT_DOMAIN = {"domain": "com"}
COUNTRY_DOMAINS = {
"ar": DEFAULT_DOMAIN,
"at": DEFAULT_DOMAIN,
"au": {"domain": "com.au"},
"be": {"domain": "com.be"},
"br": DEFAULT_DOMAIN,
"gb": {"domain": "co.uk"},
"il": DEFAULT_DOMAIN,
"jp": {"domain": "co.jp"},
"mx": {"domain": "com.mx"},
"no": DEFAULT_DOMAIN,
"nz": {"domain": "com.au"},
"pl": DEFAULT_DOMAIN,
"tr": {"domain": "com.tr"},
"us": DEFAULT_DOMAIN,
"za": {"domain": "co.za"},
}

View File

@@ -11,7 +11,7 @@ from aioamazondevices.exceptions import (
from aiohttp import ClientSession
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_COUNTRY, CONF_PASSWORD, CONF_USERNAME
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
@@ -44,7 +44,6 @@ class AmazonDevicesCoordinator(DataUpdateCoordinator[dict[str, AmazonDevice]]):
)
self.api = AmazonEchoApi(
session,
entry.data[CONF_COUNTRY],
entry.data[CONF_USERNAME],
entry.data[CONF_PASSWORD],
entry.data[CONF_LOGIN_DATA],

View File

@@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["aioamazondevices"],
"quality_scale": "silver",
"requirements": ["aioamazondevices==4.0.0"]
"requirements": ["aioamazondevices==5.0.0"]
}

View File

@@ -1,7 +1,6 @@
{
"common": {
"data_code": "One-time password (OTP code)",
"data_description_country": "The country where your Amazon account is registered.",
"data_description_username": "The email address of your Amazon account.",
"data_description_password": "The password of your Amazon account.",
"data_description_code": "The one-time password to log in to your account. Currently, only tokens from OTP applications are supported.",
@@ -12,13 +11,11 @@
"step": {
"user": {
"data": {
"country": "[%key:common::config_flow::data::country%]",
"username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]",
"code": "[%key:component::alexa_devices::common::data_code%]"
},
"data_description": {
"country": "[%key:component::alexa_devices::common::data_description_country%]",
"username": "[%key:component::alexa_devices::common::data_description_username%]",
"password": "[%key:component::alexa_devices::common::data_description_password%]",
"code": "[%key:component::alexa_devices::common::data_description_code%]"
@@ -46,7 +43,6 @@
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"cannot_retrieve_data": "Unable to retrieve data from Amazon. Please try again later.",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"wrong_country": "Wrong country selected. Please select the country where your Amazon account is registered.",
"unknown": "[%key:common::config_flow::error::unknown%]"
}
},

View File

@@ -16,7 +16,7 @@ from homeassistant.helpers.selector import (
SelectSelectorMode,
)
from .const import CONF_SITE_ID, CONF_SITE_NAME, DOMAIN
from .const import CONF_SITE_ID, CONF_SITE_NAME, DOMAIN, REQUEST_TIMEOUT
API_URL = "https://app.amber.com.au/developers"
@@ -64,7 +64,9 @@ class AmberElectricConfigFlow(ConfigFlow, domain=DOMAIN):
api = amberelectric.AmberApi(api_client)
try:
sites: list[Site] = filter_sites(api.get_sites())
sites: list[Site] = filter_sites(
api.get_sites(_request_timeout=REQUEST_TIMEOUT)
)
except amberelectric.ApiException as api_exception:
if api_exception.status == 403:
self._errors[CONF_API_TOKEN] = "invalid_api_token"

View File

@@ -22,3 +22,5 @@ SERVICE_GET_FORECASTS = "get_forecasts"
GENERAL_CHANNEL = "general"
CONTROLLED_LOAD_CHANNEL = "controlled_load"
FEED_IN_CHANNEL = "feed_in"
REQUEST_TIMEOUT = 15

View File

@@ -16,7 +16,7 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import LOGGER
from .const import LOGGER, REQUEST_TIMEOUT
from .helpers import normalize_descriptor
type AmberConfigEntry = ConfigEntry[AmberUpdateCoordinator]
@@ -82,7 +82,11 @@ class AmberUpdateCoordinator(DataUpdateCoordinator):
"grid": {},
}
try:
data = self._api.get_current_prices(self.site_id, next=288)
data = self._api.get_current_prices(
self.site_id,
next=288,
_request_timeout=REQUEST_TIMEOUT,
)
intervals = [interval.actual_instance for interval in data]
except ApiException as api_exception:
raise UpdateFailed("Missing price data, skipping update") from api_exception

View File

@@ -16,7 +16,7 @@
"quality_scale": "internal",
"requirements": [
"bleak==1.0.1",
"bleak-retry-connector==4.0.1",
"bleak-retry-connector==4.0.2",
"bluetooth-adapters==2.0.0",
"bluetooth-auto-recovery==1.5.2",
"bluetooth-data-tools==1.28.2",

View File

@@ -69,12 +69,7 @@ class SHCEntity(SHCBaseEntity):
manufacturer=device.manufacturer,
model=device.device_model,
name=device.name,
via_device=(
DOMAIN,
device.parent_device_id
if device.parent_device_id is not None
else parent_id,
),
via_device=(DOMAIN, device.root_device_id),
)
super().__init__(device=device, parent_id=parent_id, entry_id=entry_id)

View File

@@ -7,7 +7,7 @@
"documentation": "https://www.home-assistant.io/integrations/bosch_shc",
"iot_class": "local_push",
"loggers": ["boschshcpy"],
"requirements": ["boschshcpy==0.2.91"],
"requirements": ["boschshcpy==0.2.107"],
"zeroconf": [
{
"type": "_http._tcp.local.",

View File

@@ -17,7 +17,7 @@ DEFAULT_NEW_CONFIG_ALLOW_ALLOW_SERVICE_CALLS = False
DEFAULT_PORT: Final = 6053
STABLE_BLE_VERSION_STR = "2025.5.0"
STABLE_BLE_VERSION_STR = "2025.8.0"
STABLE_BLE_VERSION = AwesomeVersion(STABLE_BLE_VERSION_STR)
PROJECT_URLS = {
"esphome.bluetooth-proxy": "https://esphome.github.io/bluetooth-proxies/",

View File

@@ -50,7 +50,7 @@ CONF_EXTRA_JS_URL_ES5 = "extra_js_url_es5"
CONF_FRONTEND_REPO = "development_repo"
CONF_JS_VERSION = "javascript_version"
DEFAULT_THEME_COLOR = "#03A9F4"
DEFAULT_THEME_COLOR = "#2980b9"
DATA_PANELS: HassKey[dict[str, Panel]] = HassKey("frontend_panels")

View File

@@ -20,5 +20,5 @@
"documentation": "https://www.home-assistant.io/integrations/frontend",
"integration_type": "system",
"quality_scale": "internal",
"requirements": ["home-assistant-frontend==20250811.0"]
"requirements": ["home-assistant-frontend==20250811.1"]
}

View File

@@ -61,18 +61,19 @@ PLACEHOLDER_KEY_REASON = "reason"
UNSUPPORTED_REASONS = {
"apparmor",
"cgroup_version",
"connectivity_check",
"content_trust",
"dbus",
"dns_server",
"docker_configuration",
"docker_version",
"cgroup_version",
"job_conditions",
"lxc",
"network_manager",
"os",
"os_agent",
"os_version",
"restart_policy",
"software",
"source_mods",
@@ -80,6 +81,7 @@ UNSUPPORTED_REASONS = {
"systemd",
"systemd_journal",
"systemd_resolved",
"virtualization_image",
}
# Some unsupported reasons also mark the system as unhealthy. If the unsupported reason
# provides no additional information beyond the unhealthy one then skip that repair.

View File

@@ -5,5 +5,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/holiday",
"iot_class": "local_polling",
"requirements": ["holidays==0.78", "babel==2.15.0"]
"requirements": ["holidays==0.79", "babel==2.15.0"]
}

View File

@@ -8,7 +8,7 @@ from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.helpers import config_validation as cv
from homeassistant.util import slugify
from .account import IcloudAccount
from .account import IcloudAccount, IcloudConfigEntry
from .const import (
ATTR_ACCOUNT,
ATTR_DEVICE_NAME,
@@ -92,8 +92,10 @@ def lost_device(service: ServiceCall) -> None:
def update_account(service: ServiceCall) -> None:
"""Call the update function of an iCloud account."""
if (account := service.data.get(ATTR_ACCOUNT)) is None:
for account in service.hass.data[DOMAIN].values():
account.keep_alive()
# Update all accounts when no specific account is provided
entry: IcloudConfigEntry
for entry in service.hass.config_entries.async_loaded_entries(DOMAIN):
entry.runtime_data.keep_alive()
else:
_get_account(service.hass, account).keep_alive()
@@ -102,17 +104,12 @@ def _get_account(hass: HomeAssistant, account_identifier: str) -> IcloudAccount:
if account_identifier is None:
return None
icloud_account: IcloudAccount | None = hass.data[DOMAIN].get(account_identifier)
if icloud_account is None:
for account in hass.data[DOMAIN].values():
if account.username == account_identifier:
icloud_account = account
entry: IcloudConfigEntry
for entry in hass.config_entries.async_loaded_entries(DOMAIN):
if entry.runtime_data.username == account_identifier:
return entry.runtime_data
if icloud_account is None:
raise ValueError(
f"No iCloud account with username or name {account_identifier}"
)
return icloud_account
raise ValueError(f"No iCloud account with username or name {account_identifier}")
@callback

View File

@@ -75,13 +75,11 @@ class InverterCoordinator(DataUpdateCoordinator[dict[str, str | float | int]]):
data: dict[str, str | float | int] = {}
async with timeout(TIMEOUT):
await self._api.login(
self.config_entry.data[CONF_USERNAME],
self.config_entry.data[CONF_PASSWORD],
)
# Fetch data using distant API
try:
await self._api.login(
self.config_entry.data[CONF_USERNAME],
self.config_entry.data[CONF_PASSWORD],
)
await self._api.update()
except (ValueError, ClientError) as e:
raise UpdateFailed(e) from e

View File

@@ -14,7 +14,6 @@ from homeassistant.const import (
EntityCategory,
UnitOfElectricCurrent,
UnitOfElectricPotential,
UnitOfEnergy,
UnitOfFrequency,
UnitOfPower,
UnitOfTemperature,
@@ -50,8 +49,8 @@ SENSOR_DESCRIPTIONS = (
SensorEntityDescription(
key="battery_stored",
translation_key="battery_stored",
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
device_class=SensorDeviceClass.ENERGY_STORAGE,
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
),
# Grid
@@ -238,16 +237,16 @@ SENSOR_DESCRIPTIONS = (
SensorEntityDescription(
key="pv_consumed",
translation_key="pv_consumed",
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL,
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key="pv_injected",
translation_key="pv_injected",
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL,
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key="pv_power_1",
@@ -290,14 +289,14 @@ SENSOR_DESCRIPTIONS = (
key="monitoring_self_consumption",
translation_key="monitoring_self_consumption",
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
state_class=SensorStateClass.TOTAL,
suggested_display_precision=2,
),
SensorEntityDescription(
key="monitoring_self_sufficiency",
translation_key="monitoring_self_sufficiency",
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
state_class=SensorStateClass.TOTAL,
suggested_display_precision=2,
),
# Monitoring (instant minute data)

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/imgw_pib",
"iot_class": "cloud_polling",
"quality_scale": "silver",
"requirements": ["imgw_pib==1.5.3"]
"requirements": ["imgw_pib==1.5.4"]
}

View File

@@ -37,7 +37,7 @@ class DeviceDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
name=f"{DOMAIN}_{ha_bridge.device.device_id}",
)
self.data = {}
self.data = ha_bridge.update_status(None)
self.api = ha_bridge
self.device_id = ha_bridge.device.device_id
self.sub_id = ha_bridge.sub_id

View File

@@ -52,9 +52,12 @@ class MatterValve(MatterEntity, ValveEntity):
async def async_set_valve_position(self, position: int) -> None:
"""Move the valve to a specific position."""
await self.send_device_command(
ValveConfigurationAndControl.Commands.Open(targetLevel=position)
)
if position > 0:
await self.send_device_command(
ValveConfigurationAndControl.Commands.Open(targetLevel=position)
)
return
await self.send_device_command(ValveConfigurationAndControl.Commands.Close())
@callback
def _update_from_device(self) -> None:

View File

@@ -8,6 +8,6 @@
"iot_class": "calculated",
"loggers": ["yt_dlp"],
"quality_scale": "internal",
"requirements": ["yt-dlp[default]==2025.07.21"],
"requirements": ["yt-dlp[default]==2025.08.11"],
"single_config_entry": true
}

View File

@@ -7,5 +7,5 @@
"documentation": "https://www.home-assistant.io/integrations/mystrom",
"iot_class": "local_polling",
"loggers": ["pymystrom"],
"requirements": ["python-mystrom==2.4.0"]
"requirements": ["python-mystrom==2.5.0"]
}

View File

@@ -10,7 +10,12 @@ from typing import Any, Final, cast
from aionanoleaf import InvalidToken, Nanoleaf, Unauthorized, Unavailable
import voluptuous as vol
from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlow, ConfigFlowResult
from homeassistant.config_entries import (
SOURCE_REAUTH,
SOURCE_USER,
ConfigFlow,
ConfigFlowResult,
)
from homeassistant.const import CONF_HOST, CONF_TOKEN
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.json import save_json
@@ -200,7 +205,9 @@ class NanoleafConfigFlow(ConfigFlow, domain=DOMAIN):
return self.async_abort(reason="unknown")
name = self.nanoleaf.name
await self.async_set_unique_id(name)
await self.async_set_unique_id(
name, raise_on_progress=self.source != SOURCE_USER
)
self._abort_if_unique_id_configured({CONF_HOST: self.nanoleaf.host})
if discovery_integration_import:

View File

@@ -12,5 +12,5 @@
"integration_type": "hub",
"iot_class": "cloud_polling",
"loggers": ["pyatmo"],
"requirements": ["pyatmo==9.2.1"]
"requirements": ["pyatmo==9.2.3"]
}

View File

@@ -8,5 +8,5 @@
"documentation": "https://www.home-assistant.io/integrations/onvif",
"iot_class": "local_push",
"loggers": ["onvif", "wsdiscovery", "zeep"],
"requirements": ["onvif-zeep-async==4.0.3", "WSDiscovery==2.1.2"]
"requirements": ["onvif-zeep-async==4.0.4", "WSDiscovery==2.1.2"]
}

View File

@@ -91,6 +91,8 @@ MAX_TOOL_ITERATIONS = 10
def _adjust_schema(schema: dict[str, Any]) -> None:
"""Adjust the schema to be compatible with OpenAI API."""
if schema["type"] == "object":
schema.setdefault("strict", True)
schema.setdefault("additionalProperties", False)
if "properties" not in schema:
return
@@ -124,8 +126,6 @@ def _format_structured_output(
_adjust_schema(result)
result["strict"] = True
result["additionalProperties"] = False
return result

View File

@@ -7,5 +7,5 @@
"documentation": "https://www.home-assistant.io/integrations/opower",
"iot_class": "cloud_polling",
"loggers": ["opower"],
"requirements": ["opower==0.15.1"]
"requirements": ["opower==0.15.2"]
}

View File

@@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["renault_api"],
"quality_scale": "silver",
"requirements": ["renault-api==0.3.1"]
"requirements": ["renault-api==0.4.0"]
}

View File

@@ -7,6 +7,6 @@
"iot_class": "local_push",
"loggers": ["aiorussound"],
"quality_scale": "silver",
"requirements": ["aiorussound==4.8.0"],
"requirements": ["aiorussound==4.8.1"],
"zeroconf": ["_rio._tcp.local."]
}

View File

@@ -30,5 +30,5 @@
"iot_class": "cloud_push",
"loggers": ["pysmartthings"],
"quality_scale": "bronze",
"requirements": ["pysmartthings==3.2.8"]
"requirements": ["pysmartthings==3.2.9"]
}

View File

@@ -326,7 +326,7 @@ class SqueezeBoxMediaPlayerEntity(SqueezeboxEntity, MediaPlayerEntity):
def volume_level(self) -> float | None:
"""Volume level of the media player (0..1)."""
if self._player.volume is not None:
return int(float(self._player.volume)) / 100.0
return float(self._player.volume) / 100.0
return None
@@ -435,7 +435,7 @@ class SqueezeBoxMediaPlayerEntity(SqueezeboxEntity, MediaPlayerEntity):
async def async_set_volume_level(self, volume: float) -> None:
"""Set volume level, range 0..1."""
volume_percent = str(int(volume * 100))
volume_percent = str(round(volume * 100))
await self._player.async_set_volume(volume_percent)
await self.coordinator.async_refresh()

View File

@@ -260,6 +260,8 @@ class VolvoMediumIntervalCoordinator(VolvoBaseCoordinator):
"Volvo medium interval coordinator",
)
self._supported_capabilities: list[str] = []
async def _async_determine_api_calls(
self,
) -> list[Callable[[], Coroutine[Any, Any, Any]]]:
@@ -267,6 +269,31 @@ class VolvoMediumIntervalCoordinator(VolvoBaseCoordinator):
capabilities = await self.api.async_get_energy_capabilities()
if capabilities.get("isSupported", False):
return [self.api.async_get_energy_state]
self._supported_capabilities = [
key
for key, value in capabilities.items()
if isinstance(value, dict) and value.get("isSupported", False)
]
return [self._async_get_energy_state]
return []
async def _async_get_energy_state(
self,
) -> dict[str, VolvoCarsValueStatusField | None]:
def _mark_ok(
field: VolvoCarsValueStatusField | None,
) -> VolvoCarsValueStatusField | None:
if field:
field.status = "OK"
return field
energy_state = await self.api.async_get_energy_state()
return {
key: _mark_ok(value)
for key, value in energy_state.items()
if key in self._supported_capabilities
}

View File

@@ -67,8 +67,8 @@ def _calculate_time_to_service(field: VolvoCarsValue) -> int:
def _charging_power_value(field: VolvoCarsValue) -> int:
return (
int(field.value)
if isinstance(field, VolvoCarsValueStatusField) and field.status == "OK"
field.value
if isinstance(field, VolvoCarsValueStatusField) and isinstance(field.value, int)
else 0
)

View File

@@ -2,16 +2,23 @@
from __future__ import annotations
from dataclasses import asdict
from typing import Any
from yarl import URL
from homeassistant.components.diagnostics import async_redact_data
from homeassistant.components.webhook import async_generate_url as webhook_generate_url
from homeassistant.const import CONF_WEBHOOK_ID
from homeassistant.core import HomeAssistant
from . import CONF_CLOUDHOOK_URL, WithingsConfigEntry
TO_REDACT = {
"device_id",
"hashed_device_id",
}
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, entry: WithingsConfigEntry
@@ -53,4 +60,8 @@ async def async_get_config_entry_diagnostics(
"received_sleep_data": withings_data.sleep_coordinator.data is not None,
"received_workout_data": withings_data.workout_coordinator.data is not None,
"received_activity_data": withings_data.activity_coordinator.data is not None,
"devices": async_redact_data(
[asdict(v) for v in withings_data.device_coordinator.data.values()],
TO_REDACT,
),
}

View File

@@ -7,5 +7,5 @@
"iot_class": "local_polling",
"loggers": ["holidays"],
"quality_scale": "internal",
"requirements": ["holidays==0.78"]
"requirements": ["holidays==0.79"]
}

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/zimi",
"iot_class": "local_push",
"quality_scale": "bronze",
"requirements": ["zcc-helper==3.5.2"]
"requirements": ["zcc-helper==3.6"]
}

View File

@@ -35,6 +35,7 @@ from homeassistant.const import CONF_NAME, CONF_URL
from homeassistant.core import HomeAssistant, callback
from homeassistant.data_entry_flow import AbortFlow
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import selector
from homeassistant.helpers.hassio import is_hassio
from homeassistant.helpers.service_info.hassio import HassioServiceInfo
from homeassistant.helpers.service_info.usb import UsbServiceInfo
@@ -88,6 +89,8 @@ ADDON_USER_INPUT_MAP = {
CONF_ADDON_LR_S2_AUTHENTICATED_KEY: CONF_LR_S2_AUTHENTICATED_KEY,
}
CONF_ADDON_RF_REGION = "rf_region"
EXAMPLE_SERVER_URL = "ws://localhost:3000"
ON_SUPERVISOR_SCHEMA = vol.Schema({vol.Optional(CONF_USE_ADDON, default=True): bool})
MIN_MIGRATION_SDK_VERSION = AwesomeVersion("6.61")
@@ -103,6 +106,19 @@ ZWAVE_JS_UI_MIGRATION_INSTRUCTIONS = (
"#how-to-migrate-from-one-adapter-to-a-new-adapter-using-z-wave-js-ui"
)
RF_REGIONS = [
"Australia/New Zealand",
"China",
"Europe",
"Hong Kong",
"India",
"Israel",
"Japan",
"Korea",
"Russia",
"USA",
]
def get_manual_schema(user_input: dict[str, Any]) -> vol.Schema:
"""Return a schema for the manual step."""
@@ -195,10 +211,12 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN):
self.backup_data: bytes | None = None
self.backup_filepath: Path | None = None
self.use_addon = False
self._addon_config_updates: dict[str, Any] = {}
self._migrating = False
self._reconfigure_config_entry: ZwaveJSConfigEntry | None = None
self._usb_discovery = False
self._recommended_install = False
self._rf_region: str | None = None
async def async_step_install_addon(
self, user_input: dict[str, Any] | None = None
@@ -236,6 +254,21 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN):
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Start Z-Wave JS add-on."""
if self.hass.config.country is None and (
not self._rf_region or self._rf_region == "Automatic"
):
# If the country is not set, we need to check the RF region add-on config.
addon_info = await self._async_get_addon_info()
rf_region: str | None = addon_info.options.get(CONF_ADDON_RF_REGION)
self._rf_region = rf_region
if rf_region is None or rf_region == "Automatic":
# If the RF region is not set, we need to ask the user to select it.
return await self.async_step_rf_region()
if config_updates := self._addon_config_updates:
# If we have updates to the add-on config, set them before starting the add-on.
self._addon_config_updates = {}
await self._async_set_addon_config(config_updates)
if not self.start_task:
self.start_task = self.hass.async_create_task(self._async_start_addon())
@@ -629,6 +662,33 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN):
return await self.async_step_on_supervisor({CONF_USE_ADDON: True})
return await self.async_step_on_supervisor()
async def async_step_rf_region(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle RF region selection step."""
if user_input is not None:
# Store the selected RF region
self._addon_config_updates[CONF_ADDON_RF_REGION] = self._rf_region = (
user_input["rf_region"]
)
return await self.async_step_start_addon()
schema = vol.Schema(
{
vol.Required("rf_region"): selector.SelectSelector(
selector.SelectSelectorConfig(
options=RF_REGIONS,
mode=selector.SelectSelectorMode.DROPDOWN,
)
),
}
)
return self.async_show_form(
step_id="rf_region",
data_schema=schema,
)
async def async_step_on_supervisor(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
@@ -728,7 +788,7 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN):
CONF_ADDON_LR_S2_AUTHENTICATED_KEY: self.lr_s2_authenticated_key,
}
await self._async_set_addon_config(addon_config_updates)
self._addon_config_updates = addon_config_updates
return await self.async_step_start_addon()
# Network already exists, go to security keys step
@@ -799,7 +859,7 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN):
CONF_ADDON_LR_S2_AUTHENTICATED_KEY: self.lr_s2_authenticated_key,
}
await self._async_set_addon_config(addon_config_updates)
self._addon_config_updates = addon_config_updates
return await self.async_step_start_addon()
data_schema = vol.Schema(
@@ -1004,7 +1064,7 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN):
if user_input is not None:
if self.usb_path:
# USB discovery was used, so the device is already known.
await self._async_set_addon_config({CONF_ADDON_DEVICE: self.usb_path})
self._addon_config_updates[CONF_ADDON_DEVICE] = self.usb_path
return await self.async_step_start_addon()
# Now that the old controller is gone, we can scan for serial ports again
return await self.async_step_choose_serial_port()
@@ -1136,6 +1196,8 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN):
CONF_ADDON_LR_S2_AUTHENTICATED_KEY: self.lr_s2_authenticated_key,
}
addon_config_updates = self._addon_config_updates | addon_config_updates
self._addon_config_updates = {}
await self._async_set_addon_config(addon_config_updates)
if addon_info.state == AddonState.RUNNING and not self.restart_addon:
@@ -1207,7 +1269,7 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN):
"""Choose a serial port."""
if user_input is not None:
self.usb_path = user_input[CONF_USB_PATH]
await self._async_set_addon_config({CONF_ADDON_DEVICE: self.usb_path})
self._addon_config_updates[CONF_ADDON_DEVICE] = self.usb_path
return await self.async_step_start_addon()
try:

View File

@@ -4,15 +4,15 @@ from __future__ import annotations
from collections.abc import Callable, Mapping
from dataclasses import dataclass
from typing import Any
from typing import Any, cast
import voluptuous as vol
from zwave_js_server.const import CommandClass
from zwave_js_server.const import CommandClass, RssiError
from zwave_js_server.const.command_class.meter import (
RESET_METER_OPTION_TARGET_VALUE,
RESET_METER_OPTION_TYPE,
)
from zwave_js_server.exceptions import BaseZwaveJSServerError
from zwave_js_server.exceptions import BaseZwaveJSServerError, RssiErrorReceived
from zwave_js_server.model.controller import Controller
from zwave_js_server.model.controller.statistics import ControllerStatistics
from zwave_js_server.model.driver import Driver
@@ -1049,7 +1049,7 @@ class ZWaveStatisticsSensor(SensorEntity):
self,
config_entry: ZwaveJSConfigEntry,
driver: Driver,
statistics_src: ZwaveNode | Controller,
statistics_src: Controller | ZwaveNode,
description: ZWaveJSStatisticsSensorEntityDescription,
) -> None:
"""Initialize a Z-Wave statistics entity."""
@@ -1080,13 +1080,31 @@ class ZWaveStatisticsSensor(SensorEntity):
)
@callback
def statistics_updated(self, event_data: dict) -> None:
def _statistics_updated(self, event_data: dict) -> None:
"""Call when statistics updated event is received."""
self._attr_native_value = self.entity_description.convert(
event_data["statistics_updated"], self.entity_description.key
statistics = cast(
ControllerStatistics | NodeStatistics, event_data["statistics_updated"]
)
self._set_statistics(statistics)
self.async_write_ha_state()
@callback
def _set_statistics(
self, statistics: ControllerStatistics | NodeStatistics
) -> None:
"""Set updated statistics."""
try:
self._attr_native_value = self.entity_description.convert(
statistics, self.entity_description.key
)
except RssiErrorReceived as err:
if err.error is RssiError.NOT_AVAILABLE:
self._attr_available = False
return
self._attr_native_value = None
# Reset available state.
self._attr_available = True
async def async_added_to_hass(self) -> None:
"""Call when entity is added."""
self.async_on_remove(
@@ -1104,10 +1122,8 @@ class ZWaveStatisticsSensor(SensorEntity):
)
)
self.async_on_remove(
self.statistics_src.on("statistics updated", self.statistics_updated)
self.statistics_src.on("statistics updated", self._statistics_updated)
)
# Set initial state
self._attr_native_value = self.entity_description.convert(
self.statistics_src.statistics, self.entity_description.key
)
self._set_statistics(self.statistics_src.statistics)

View File

@@ -113,6 +113,16 @@
"description": "[%key:component::zwave_js::config::step::on_supervisor::description%]",
"title": "[%key:component::zwave_js::config::step::on_supervisor::title%]"
},
"rf_region": {
"title": "Z-Wave region",
"description": "Select the RF region for your Z-Wave network.",
"data": {
"rf_region": "RF region"
},
"data_description": {
"rf_region": "The radio frequency region for your Z-Wave network. This must match the region of your Z-Wave devices."
}
},
"start_addon": {
"title": "Configuring add-on"
},

View File

@@ -25,7 +25,7 @@ if TYPE_CHECKING:
APPLICATION_NAME: Final = "HomeAssistant"
MAJOR_VERSION: Final = 2025
MINOR_VERSION: Final = 8
PATCH_VERSION: Final = "2"
PATCH_VERSION: Final = "3"
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 13, 2)

View File

@@ -20,7 +20,7 @@ audioop-lts==0.2.1
av==13.1.0
awesomeversion==25.5.0
bcrypt==4.3.0
bleak-retry-connector==4.0.1
bleak-retry-connector==4.0.2
bleak==1.0.1
bluetooth-adapters==2.0.0
bluetooth-auto-recovery==1.5.2
@@ -38,7 +38,7 @@ habluetooth==4.0.2
hass-nabucasa==0.111.2
hassil==2.2.3
home-assistant-bluetooth==1.13.1
home-assistant-frontend==20250811.0
home-assistant-frontend==20250811.1
home-assistant-intents==2025.7.30
httpx==0.28.1
ifaddr==0.2.0
@@ -221,3 +221,6 @@ num2words==0.5.14
# downgraded or upgraded by custom components
# This ensures all use the same version
pymodbus==3.9.2
# Some packages don't support gql 4.0.0 yet
gql<4.0.0

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "homeassistant"
version = "2025.8.2"
version = "2025.8.3"
license = "Apache-2.0"
license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"]
description = "Open-source home automation platform running on Python 3."

30
requirements_all.txt generated
View File

@@ -185,7 +185,7 @@ aioairzone-cloud==0.7.1
aioairzone==1.0.0
# homeassistant.components.alexa_devices
aioamazondevices==4.0.0
aioamazondevices==5.0.0
# homeassistant.components.ambient_network
# homeassistant.components.ambient_station
@@ -375,7 +375,7 @@ aioridwell==2024.01.0
aioruckus==0.42
# homeassistant.components.russound_rio
aiorussound==4.8.0
aiorussound==4.8.1
# homeassistant.components.ruuvi_gateway
aioruuvigateway==0.1.0
@@ -625,7 +625,7 @@ bizkaibus==0.1.1
bleak-esphome==3.1.0
# homeassistant.components.bluetooth
bleak-retry-connector==4.0.1
bleak-retry-connector==4.0.2
# homeassistant.components.bluetooth
bleak==1.0.1
@@ -667,7 +667,7 @@ bond-async==0.2.1
bosch-alarm-mode2==0.4.6
# homeassistant.components.bosch_shc
boschshcpy==0.2.91
boschshcpy==0.2.107
# homeassistant.components.amazon_polly
# homeassistant.components.route53
@@ -1171,10 +1171,10 @@ hole==0.9.0
# homeassistant.components.holiday
# homeassistant.components.workday
holidays==0.78
holidays==0.79
# homeassistant.components.frontend
home-assistant-frontend==20250811.0
home-assistant-frontend==20250811.1
# homeassistant.components.conversation
home-assistant-intents==2025.7.30
@@ -1240,7 +1240,7 @@ ihcsdk==2.8.5
imeon_inverter_api==0.3.14
# homeassistant.components.imgw_pib
imgw_pib==1.5.3
imgw_pib==1.5.4
# homeassistant.components.incomfort
incomfort-client==0.6.9
@@ -1594,7 +1594,7 @@ ondilo==0.5.0
onedrive-personal-sdk==0.0.14
# homeassistant.components.onvif
onvif-zeep-async==4.0.3
onvif-zeep-async==4.0.4
# homeassistant.components.opengarage
open-garage==0.2.0
@@ -1628,7 +1628,7 @@ openwrt-luci-rpc==1.1.17
openwrt-ubus-rpc==0.0.2
# homeassistant.components.opower
opower==0.15.1
opower==0.15.2
# homeassistant.components.oralb
oralb-ble==0.17.6
@@ -1852,7 +1852,7 @@ pyasuswrt==0.1.21
pyatag==0.3.5.3
# homeassistant.components.netatmo
pyatmo==9.2.1
pyatmo==9.2.3
# homeassistant.components.apple_tv
pyatv==0.16.1
@@ -2352,7 +2352,7 @@ pysmappee==0.2.29
pysmarlaapi==0.9.1
# homeassistant.components.smartthings
pysmartthings==3.2.8
pysmartthings==3.2.9
# homeassistant.components.smarty
pysmarty2==0.10.2
@@ -2478,7 +2478,7 @@ python-miio==0.5.12
python-mpd2==3.1.1
# homeassistant.components.mystrom
python-mystrom==2.4.0
python-mystrom==2.5.0
# homeassistant.components.open_router
python-open-router==0.3.1
@@ -2660,7 +2660,7 @@ refoss-ha==1.2.5
regenmaschine==2024.03.0
# homeassistant.components.renault
renault-api==0.3.1
renault-api==0.4.0
# homeassistant.components.renson
renson-endura-delta==1.7.2
@@ -3185,7 +3185,7 @@ youless-api==2.2.0
youtubeaio==2.0.0
# homeassistant.components.media_extractor
yt-dlp[default]==2025.07.21
yt-dlp[default]==2025.08.11
# homeassistant.components.zabbix
zabbix-utils==2.0.2
@@ -3194,7 +3194,7 @@ zabbix-utils==2.0.2
zamg==0.3.6
# homeassistant.components.zimi
zcc-helper==3.5.2
zcc-helper==3.6
# homeassistant.components.zeroconf
zeroconf==0.147.0

View File

@@ -173,7 +173,7 @@ aioairzone-cloud==0.7.1
aioairzone==1.0.0
# homeassistant.components.alexa_devices
aioamazondevices==4.0.0
aioamazondevices==5.0.0
# homeassistant.components.ambient_network
# homeassistant.components.ambient_station
@@ -357,7 +357,7 @@ aioridwell==2024.01.0
aioruckus==0.42
# homeassistant.components.russound_rio
aiorussound==4.8.0
aiorussound==4.8.1
# homeassistant.components.ruuvi_gateway
aioruuvigateway==0.1.0
@@ -559,7 +559,7 @@ bimmer-connected[china]==0.17.2
bleak-esphome==3.1.0
# homeassistant.components.bluetooth
bleak-retry-connector==4.0.1
bleak-retry-connector==4.0.2
# homeassistant.components.bluetooth
bleak==1.0.1
@@ -598,7 +598,7 @@ bond-async==0.2.1
bosch-alarm-mode2==0.4.6
# homeassistant.components.bosch_shc
boschshcpy==0.2.91
boschshcpy==0.2.107
# homeassistant.components.aws
botocore==1.37.1
@@ -1020,10 +1020,10 @@ hole==0.9.0
# homeassistant.components.holiday
# homeassistant.components.workday
holidays==0.78
holidays==0.79
# homeassistant.components.frontend
home-assistant-frontend==20250811.0
home-assistant-frontend==20250811.1
# homeassistant.components.conversation
home-assistant-intents==2025.7.30
@@ -1074,7 +1074,7 @@ igloohome-api==0.1.1
imeon_inverter_api==0.3.14
# homeassistant.components.imgw_pib
imgw_pib==1.5.3
imgw_pib==1.5.4
# homeassistant.components.incomfort
incomfort-client==0.6.9
@@ -1362,7 +1362,7 @@ ondilo==0.5.0
onedrive-personal-sdk==0.0.14
# homeassistant.components.onvif
onvif-zeep-async==4.0.3
onvif-zeep-async==4.0.4
# homeassistant.components.opengarage
open-garage==0.2.0
@@ -1384,7 +1384,7 @@ openhomedevice==2.2.0
openwebifpy==4.3.1
# homeassistant.components.opower
opower==0.15.1
opower==0.15.2
# homeassistant.components.oralb
oralb-ble==0.17.6
@@ -1557,7 +1557,7 @@ pyasuswrt==0.1.21
pyatag==0.3.5.3
# homeassistant.components.netatmo
pyatmo==9.2.1
pyatmo==9.2.3
# homeassistant.components.apple_tv
pyatv==0.16.1
@@ -1955,7 +1955,7 @@ pysmappee==0.2.29
pysmarlaapi==0.9.1
# homeassistant.components.smartthings
pysmartthings==3.2.8
pysmartthings==3.2.9
# homeassistant.components.smarty
pysmarty2==0.10.2
@@ -2051,7 +2051,7 @@ python-miio==0.5.12
python-mpd2==3.1.1
# homeassistant.components.mystrom
python-mystrom==2.4.0
python-mystrom==2.5.0
# homeassistant.components.open_router
python-open-router==0.3.1
@@ -2206,7 +2206,7 @@ refoss-ha==1.2.5
regenmaschine==2024.03.0
# homeassistant.components.renault
renault-api==0.3.1
renault-api==0.4.0
# homeassistant.components.renson
renson-endura-delta==1.7.2
@@ -2632,13 +2632,13 @@ youless-api==2.2.0
youtubeaio==2.0.0
# homeassistant.components.media_extractor
yt-dlp[default]==2025.07.21
yt-dlp[default]==2025.08.11
# homeassistant.components.zamg
zamg==0.3.6
# homeassistant.components.zimi
zcc-helper==3.5.2
zcc-helper==3.6
# homeassistant.components.zeroconf
zeroconf==0.147.0

View File

@@ -247,6 +247,9 @@ num2words==0.5.14
# downgraded or upgraded by custom components
# This ensures all use the same version
pymodbus==3.9.2
# Some packages don't support gql 4.0.0 yet
gql<4.0.0
"""
GENERATED_MESSAGE = (

View File

@@ -83,7 +83,6 @@ FORBIDDEN_PACKAGE_EXCEPTIONS: dict[str, dict[str, set[str]]] = {
# - reasonX should be the name of the invalid dependency
"adax": {"adax": {"async-timeout"}, "adax-local": {"async-timeout"}},
"airthings": {"airthings-cloud": {"async-timeout"}},
"alexa_devices": {"marisa-trie": {"setuptools"}},
"ampio": {"asmog": {"async-timeout"}},
"apache_kafka": {"aiokafka": {"async-timeout"}},
"apple_tv": {"pyatv": {"async-timeout"}},

View File

@@ -202,6 +202,7 @@ EXCEPTIONS = {
"pysabnzbd", # https://github.com/jeradM/pysabnzbd/pull/6
"sharp_aquos_rc", # https://github.com/jmoore987/sharp_aquos_rc/pull/14
"tapsaff", # https://github.com/bazwilliams/python-taps-aff/pull/5
"ujson", # https://github.com/ultrajson/ultrajson/blob/main/LICENSE.txt
}
# fmt: off

View File

@@ -8,9 +8,9 @@ from aioamazondevices.const import DEVICE_TYPE_TO_MODEL
import pytest
from homeassistant.components.alexa_devices.const import CONF_LOGIN_DATA, DOMAIN
from homeassistant.const import CONF_COUNTRY, CONF_PASSWORD, CONF_USERNAME
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from .const import TEST_COUNTRY, TEST_PASSWORD, TEST_SERIAL_NUMBER, TEST_USERNAME
from .const import TEST_PASSWORD, TEST_SERIAL_NUMBER, TEST_USERNAME
from tests.common import MockConfigEntry
@@ -80,7 +80,6 @@ def mock_config_entry() -> MockConfigEntry:
domain=DOMAIN,
title="Amazon Test Account",
data={
CONF_COUNTRY: TEST_COUNTRY,
CONF_USERNAME: TEST_USERNAME,
CONF_PASSWORD: TEST_PASSWORD,
CONF_LOGIN_DATA: {"session": "test-session"},

View File

@@ -47,7 +47,6 @@
}),
'entry': dict({
'data': dict({
'country': 'IT',
'login_data': dict({
'session': 'test-session',
}),

View File

@@ -6,17 +6,16 @@ from aioamazondevices.exceptions import (
CannotAuthenticate,
CannotConnect,
CannotRetrieveData,
WrongCountry,
)
import pytest
from homeassistant.components.alexa_devices.const import CONF_LOGIN_DATA, DOMAIN
from homeassistant.config_entries import SOURCE_USER
from homeassistant.const import CONF_CODE, CONF_COUNTRY, CONF_PASSWORD, CONF_USERNAME
from homeassistant.const import CONF_CODE, CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
from .const import TEST_CODE, TEST_COUNTRY, TEST_PASSWORD, TEST_USERNAME
from .const import TEST_CODE, TEST_PASSWORD, TEST_USERNAME
from tests.common import MockConfigEntry
@@ -37,7 +36,6 @@ async def test_full_flow(
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_COUNTRY: TEST_COUNTRY,
CONF_USERNAME: TEST_USERNAME,
CONF_PASSWORD: TEST_PASSWORD,
CONF_CODE: TEST_CODE,
@@ -46,7 +44,6 @@ async def test_full_flow(
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["title"] == TEST_USERNAME
assert result["data"] == {
CONF_COUNTRY: TEST_COUNTRY,
CONF_USERNAME: TEST_USERNAME,
CONF_PASSWORD: TEST_PASSWORD,
CONF_LOGIN_DATA: {
@@ -63,7 +60,6 @@ async def test_full_flow(
(CannotConnect, "cannot_connect"),
(CannotAuthenticate, "invalid_auth"),
(CannotRetrieveData, "cannot_retrieve_data"),
(WrongCountry, "wrong_country"),
],
)
async def test_flow_errors(
@@ -87,7 +83,6 @@ async def test_flow_errors(
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_COUNTRY: TEST_COUNTRY,
CONF_USERNAME: TEST_USERNAME,
CONF_PASSWORD: TEST_PASSWORD,
CONF_CODE: TEST_CODE,
@@ -102,7 +97,6 @@ async def test_flow_errors(
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_COUNTRY: TEST_COUNTRY,
CONF_USERNAME: TEST_USERNAME,
CONF_PASSWORD: TEST_PASSWORD,
CONF_CODE: TEST_CODE,
@@ -131,7 +125,6 @@ async def test_already_configured(
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_COUNTRY: TEST_COUNTRY,
CONF_USERNAME: TEST_USERNAME,
CONF_PASSWORD: TEST_PASSWORD,
CONF_CODE: TEST_CODE,

View File

@@ -4,12 +4,14 @@ from unittest.mock import AsyncMock
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.alexa_devices.const import DOMAIN
from homeassistant.components.alexa_devices.const import CONF_LOGIN_DATA, DOMAIN
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import CONF_COUNTRY, CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
from . import setup_integration
from .const import TEST_SERIAL_NUMBER
from .const import TEST_COUNTRY, TEST_PASSWORD, TEST_SERIAL_NUMBER, TEST_USERNAME
from tests.common import MockConfigEntry
@@ -28,3 +30,32 @@ async def test_device_info(
)
assert device_entry is not None
assert device_entry == snapshot
async def test_migrate_entry(
hass: HomeAssistant,
mock_amazon_devices_client: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test successful migration of entry data."""
config_entry = MockConfigEntry(
domain=DOMAIN,
title="Amazon Test Account",
data={
CONF_COUNTRY: TEST_COUNTRY,
CONF_USERNAME: TEST_USERNAME,
CONF_PASSWORD: TEST_PASSWORD,
CONF_LOGIN_DATA: {"session": "test-session"},
},
unique_id=TEST_USERNAME,
version=1,
minor_version=0,
)
config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
assert config_entry.state is ConfigEntryState.LOADED
assert config_entry.minor_version == 1
assert config_entry.data["site"] == f"https://www.amazon.{TEST_COUNTRY}"

View File

@@ -15,7 +15,11 @@ from amberelectric.models.spike_status import SpikeStatus
from dateutil import parser
import pytest
from homeassistant.components.amberelectric.const import CONF_SITE_ID, CONF_SITE_NAME
from homeassistant.components.amberelectric.const import (
CONF_SITE_ID,
CONF_SITE_NAME,
REQUEST_TIMEOUT,
)
from homeassistant.components.amberelectric.coordinator import AmberUpdateCoordinator
from homeassistant.const import CONF_API_TOKEN
from homeassistant.core import HomeAssistant
@@ -104,7 +108,9 @@ async def test_fetch_general_site(hass: HomeAssistant, current_price_api: Mock)
result = await data_service._async_update_data()
current_price_api.get_current_prices.assert_called_with(
GENERAL_ONLY_SITE_ID, next=288
GENERAL_ONLY_SITE_ID,
next=288,
_request_timeout=REQUEST_TIMEOUT,
)
assert result["current"].get("general") == GENERAL_CHANNEL[0].actual_instance
@@ -136,7 +142,9 @@ async def test_fetch_no_general_site(
await data_service._async_update_data()
current_price_api.get_current_prices.assert_called_with(
GENERAL_ONLY_SITE_ID, next=288
GENERAL_ONLY_SITE_ID,
next=288,
_request_timeout=REQUEST_TIMEOUT,
)
@@ -150,7 +158,9 @@ async def test_fetch_api_error(hass: HomeAssistant, current_price_api: Mock) ->
result = await data_service._async_update_data()
current_price_api.get_current_prices.assert_called_with(
GENERAL_ONLY_SITE_ID, next=288
GENERAL_ONLY_SITE_ID,
next=288,
_request_timeout=REQUEST_TIMEOUT,
)
assert result["current"].get("general") == GENERAL_CHANNEL[0].actual_instance
@@ -201,7 +211,9 @@ async def test_fetch_general_and_controlled_load_site(
result = await data_service._async_update_data()
current_price_api.get_current_prices.assert_called_with(
GENERAL_AND_CONTROLLED_SITE_ID, next=288
GENERAL_AND_CONTROLLED_SITE_ID,
next=288,
_request_timeout=REQUEST_TIMEOUT,
)
assert result["current"].get("general") == GENERAL_CHANNEL[0].actual_instance
@@ -241,7 +253,9 @@ async def test_fetch_general_and_feed_in_site(
result = await data_service._async_update_data()
current_price_api.get_current_prices.assert_called_with(
GENERAL_AND_FEED_IN_SITE_ID, next=288
GENERAL_AND_FEED_IN_SITE_ID,
next=288,
_request_timeout=REQUEST_TIMEOUT,
)
assert result["current"].get("general") == GENERAL_CHANNEL[0].actual_instance

View File

@@ -192,7 +192,7 @@
'suggested_display_precision': 0,
}),
}),
'original_device_class': <SensorDeviceClass.ENERGY_STORAGE: 'energy_storage'>,
'original_device_class': <SensorDeviceClass.POWER: 'power'>,
'original_icon': None,
'original_name': 'Battery stored',
'platform': 'imeon_inverter',
@@ -201,16 +201,16 @@
'supported_features': 0,
'translation_key': 'battery_stored',
'unique_id': '111111111111111_battery_stored',
'unit_of_measurement': <UnitOfEnergy.WATT_HOUR: 'Wh'>,
'unit_of_measurement': <UnitOfPower.WATT: 'W'>,
})
# ---
# name: test_sensors[sensor.imeon_inverter_battery_stored-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'energy_storage',
'device_class': 'power',
'friendly_name': 'Imeon inverter Battery stored',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfEnergy.WATT_HOUR: 'Wh'>,
'unit_of_measurement': <UnitOfPower.WATT: 'W'>,
}),
'context': <ANY>,
'entity_id': 'sensor.imeon_inverter_battery_stored',
@@ -1290,7 +1290,7 @@
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'state_class': <SensorStateClass.TOTAL: 'total'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -1328,7 +1328,7 @@
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Imeon inverter Monitoring self-consumption',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'state_class': <SensorStateClass.TOTAL: 'total'>,
'unit_of_measurement': '%',
}),
'context': <ANY>,
@@ -1345,7 +1345,7 @@
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'state_class': <SensorStateClass.TOTAL: 'total'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -1383,7 +1383,7 @@
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Imeon inverter Monitoring self-sufficiency',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'state_class': <SensorStateClass.TOTAL: 'total'>,
'unit_of_measurement': '%',
}),
'context': <ANY>,
@@ -2072,7 +2072,7 @@
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.TOTAL: 'total'>,
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -2094,7 +2094,7 @@
'suggested_display_precision': 0,
}),
}),
'original_device_class': <SensorDeviceClass.ENERGY: 'energy'>,
'original_device_class': <SensorDeviceClass.POWER: 'power'>,
'original_icon': None,
'original_name': 'PV consumed',
'platform': 'imeon_inverter',
@@ -2103,16 +2103,16 @@
'supported_features': 0,
'translation_key': 'pv_consumed',
'unique_id': '111111111111111_pv_consumed',
'unit_of_measurement': <UnitOfEnergy.WATT_HOUR: 'Wh'>,
'unit_of_measurement': <UnitOfPower.WATT: 'W'>,
})
# ---
# name: test_sensors[sensor.imeon_inverter_pv_consumed-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'energy',
'device_class': 'power',
'friendly_name': 'Imeon inverter PV consumed',
'state_class': <SensorStateClass.TOTAL: 'total'>,
'unit_of_measurement': <UnitOfEnergy.WATT_HOUR: 'Wh'>,
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfPower.WATT: 'W'>,
}),
'context': <ANY>,
'entity_id': 'sensor.imeon_inverter_pv_consumed',
@@ -2128,7 +2128,7 @@
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.TOTAL: 'total'>,
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -2150,7 +2150,7 @@
'suggested_display_precision': 0,
}),
}),
'original_device_class': <SensorDeviceClass.ENERGY: 'energy'>,
'original_device_class': <SensorDeviceClass.POWER: 'power'>,
'original_icon': None,
'original_name': 'PV injected',
'platform': 'imeon_inverter',
@@ -2159,16 +2159,16 @@
'supported_features': 0,
'translation_key': 'pv_injected',
'unique_id': '111111111111111_pv_injected',
'unit_of_measurement': <UnitOfEnergy.WATT_HOUR: 'Wh'>,
'unit_of_measurement': <UnitOfPower.WATT: 'W'>,
})
# ---
# name: test_sensors[sensor.imeon_inverter_pv_injected-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'energy',
'device_class': 'power',
'friendly_name': 'Imeon inverter PV injected',
'state_class': <SensorStateClass.TOTAL: 'total'>,
'unit_of_measurement': <UnitOfEnergy.WATT_HOUR: 'Wh'>,
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfPower.WATT: 'W'>,
}),
'context': <ANY>,
'entity_id': 'sensor.imeon_inverter_pv_injected',

View File

@@ -133,3 +133,22 @@ async def test_valve(
command=clusters.ValveConfigurationAndControl.Commands.Open(targetLevel=100),
)
matter_client.send_device_command.reset_mock()
# test using set_position action to close valve
await hass.services.async_call(
"valve",
"set_valve_position",
{
"entity_id": entity_id,
"position": 0,
},
blocking=True,
)
assert matter_client.send_device_command.call_count == 1
assert matter_client.send_device_command.call_args == call(
node_id=matter_node.node_id,
endpoint_id=1,
command=clusters.ValveConfigurationAndControl.Commands.Close(),
)
matter_client.send_device_command.reset_mock()

View File

@@ -10,6 +10,7 @@ import pytest
from homeassistant import config_entries
from homeassistant.components.nanoleaf.const import DOMAIN
from homeassistant.config_entries import SOURCE_USER
from homeassistant.const import CONF_HOST, CONF_TOKEN
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
@@ -463,3 +464,59 @@ async def test_ssdp_discovery(hass: HomeAssistant) -> None:
}
assert len(mock_setup_entry.mock_calls) == 1
async def test_abort_discovery_flow_with_user_flow(hass: HomeAssistant) -> None:
"""Test abort discovery flow if user flow is already in progress."""
with (
patch(
"homeassistant.components.nanoleaf.config_flow.load_json_object",
return_value={},
),
patch(
"homeassistant.components.nanoleaf.config_flow.Nanoleaf",
return_value=_mock_nanoleaf(TEST_HOST, TEST_TOKEN),
),
patch(
"homeassistant.components.nanoleaf.async_setup_entry",
return_value=True,
),
):
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_SSDP},
data=SsdpServiceInfo(
ssdp_usn="mock_usn",
ssdp_st="mock_st",
upnp={},
ssdp_headers={
"_host": TEST_HOST,
"nl-devicename": TEST_NAME,
"nl-deviceid": TEST_DEVICE_ID,
},
),
)
assert result["type"] is FlowResultType.FORM
assert result["errors"] is None
assert result["step_id"] == "link"
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_USER},
)
assert len(hass.config_entries.flow.async_progress(DOMAIN)) == 2
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "user"
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {CONF_HOST: TEST_HOST}
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "link"
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
assert result["type"] is FlowResultType.CREATE_ENTRY
# Verify the discovery flow was aborted
assert not hass.config_entries.flow.async_progress(DOMAIN)

View File

@@ -63,6 +63,8 @@ async def test_format_structured_output() -> None:
"item_value",
],
"type": "object",
"additionalProperties": False,
"strict": True,
},
"type": "array",
},

View File

@@ -9,7 +9,7 @@ from volvocarsapi.auth import TOKEN_URL
from volvocarsapi.models import (
VolvoCarsAvailableCommand,
VolvoCarsLocation,
VolvoCarsValueField,
VolvoCarsValueStatusField,
VolvoCarsVehicle,
)
@@ -98,7 +98,7 @@ async def mock_api(hass: HomeAssistant, full_model: str) -> AsyncGenerator[Async
hass, "energy_state", full_model
)
energy_state = {
key: VolvoCarsValueField.from_dict(value)
key: VolvoCarsValueStatusField.from_dict(value)
for key, value in energy_state_data.items()
}
engine_status = await async_load_fixture_as_value_field(

View File

@@ -9,7 +9,7 @@
"chargerConnectionStatus": {
"isSupported": true
},
"chargingSystemStatus": {
"chargingStatus": {
"isSupported": true
},
"chargingType": {
@@ -25,7 +25,7 @@
"isSupported": true
},
"chargingCurrentLimit": {
"isSupported": true
"isSupported": false
},
"chargingPower": {
"isSupported": true

View File

@@ -50,7 +50,7 @@
},
"chargingPower": {
"status": "ERROR",
"code": "NOT_SUPPORTED",
"message": "Resource is not supported for this vehicle"
"code": "PROPERTY_NOT_FOUND",
"message": "No valid value could be found for the requested property"
}
}

View File

@@ -9,7 +9,7 @@
"chargerConnectionStatus": {
"isSupported": true
},
"chargingSystemStatus": {
"chargingStatus": {
"isSupported": true
},
"chargingType": {

View File

@@ -9,7 +9,7 @@
"chargerConnectionStatus": {
"isSupported": true
},
"chargingSystemStatus": {
"chargingStatus": {
"isSupported": true
},
"chargingType": {

View File

@@ -40,9 +40,10 @@
"message": "Resource is not supported for this vehicle"
},
"targetBatteryChargeLevel": {
"status": "ERROR",
"code": "NOT_SUPPORTED",
"message": "Resource is not supported for this vehicle"
"status": "OK",
"value": 80,
"unit": "percentage",
"updatedAt": "2024-09-22T09:40:12Z"
},
"chargingPower": {
"status": "ERROR",

View File

@@ -232,6 +232,62 @@
'state': 'connected',
})
# ---
# name: test_sensor[ex30_2024][sensor.volvo_ex30_charging_power-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.volvo_ex30_charging_power',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
'sensor': dict({
'suggested_display_precision': 0,
}),
}),
'original_device_class': <SensorDeviceClass.POWER: 'power'>,
'original_icon': None,
'original_name': 'Charging power',
'platform': 'volvo',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'charging_power',
'unique_id': 'yv1abcdefg1234567_charging_power',
'unit_of_measurement': <UnitOfPower.WATT: 'W'>,
})
# ---
# name: test_sensor[ex30_2024][sensor.volvo_ex30_charging_power-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'power',
'friendly_name': 'Volvo EX30 Charging power',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfPower.WATT: 'W'>,
}),
'context': <ANY>,
'entity_id': 'sensor.volvo_ex30_charging_power',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '0',
})
# ---
# name: test_sensor[ex30_2024][sensor.volvo_ex30_charging_power_status-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
@@ -2164,7 +2220,7 @@
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '0',
'state': '1386',
})
# ---
# name: test_sensor[xc40_electric_2024][sensor.volvo_xc40_charging_power_status-entry]
@@ -3601,6 +3657,58 @@
'state': '30000',
})
# ---
# name: test_sensor[xc60_phev_2020][sensor.volvo_xc60_target_battery_charge_level-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.volvo_xc60_target_battery_charge_level',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
'sensor': dict({
'suggested_display_precision': 0,
}),
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Target battery charge level',
'platform': 'volvo',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'target_battery_charge_level',
'unique_id': 'yv1abcdefg1234567_target_battery_charge_level',
'unit_of_measurement': '%',
})
# ---
# name: test_sensor[xc60_phev_2020][sensor.volvo_xc60_target_battery_charge_level-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Volvo XC60 Target battery charge level',
'unit_of_measurement': '%',
}),
'context': <ANY>,
'entity_id': 'sensor.volvo_xc60_target_battery_charge_level',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '80',
})
# ---
# name: test_sensor[xc60_phev_2020][sensor.volvo_xc60_time_to_engine_service-entry]
EntityRegistryEntrySnapshot({
'aliases': set({

View File

@@ -68,4 +68,20 @@ async def test_skip_invalid_api_fields(
with patch("homeassistant.components.volvo.PLATFORMS", [Platform.SENSOR]):
assert await setup_integration()
assert not hass.states.get(f"sensor.volvo_{short_model}_charging_power")
assert not hass.states.get(f"sensor.volvo_{short_model}_charging_current_limit")
@pytest.mark.parametrize(
"full_model",
["ex30_2024"],
)
async def test_charging_power_value(
hass: HomeAssistant,
setup_integration: Callable[[], Awaitable[bool]],
) -> None:
"""Test if charging_power_value is zero if supported, but not charging."""
with patch("homeassistant.components.volvo.PLATFORMS", [Platform.SENSOR]):
assert await setup_integration()
assert hass.states.get("sensor.volvo_ex30_charging_power").state == "0"

View File

@@ -1,6 +1,18 @@
# serializer version: 1
# name: test_diagnostics_cloudhook_instance
dict({
'devices': list([
dict({
'battery': 'high',
'device_id': '**REDACTED**',
'device_type': 'Scale',
'first_session_date': None,
'hashed_device_id': '**REDACTED**',
'last_session_date': '2023-09-04T22:39:39+00:00',
'model': 5,
'raw_model': 'Body+',
}),
]),
'has_cloudhooks': True,
'has_valid_external_webhook_url': True,
'received_activity_data': False,
@@ -64,6 +76,18 @@
# ---
# name: test_diagnostics_polling_instance
dict({
'devices': list([
dict({
'battery': 'high',
'device_id': '**REDACTED**',
'device_type': 'Scale',
'first_session_date': None,
'hashed_device_id': '**REDACTED**',
'last_session_date': '2023-09-04T22:39:39+00:00',
'model': 5,
'raw_model': 'Body+',
}),
]),
'has_cloudhooks': False,
'has_valid_external_webhook_url': False,
'received_activity_data': False,
@@ -127,6 +151,18 @@
# ---
# name: test_diagnostics_webhook_instance
dict({
'devices': list([
dict({
'battery': 'high',
'device_id': '**REDACTED**',
'device_type': 'Scale',
'first_session_date': None,
'hashed_device_id': '**REDACTED**',
'last_session_date': '2023-09-04T22:39:39+00:00',
'model': 5,
'raw_model': 'Body+',
}),
]),
'has_cloudhooks': False,
'has_valid_external_webhook_url': True,
'received_activity_data': False,

View File

@@ -198,6 +198,17 @@ def mock_sdk_version(client: MagicMock) -> Generator[None]:
client.driver.controller.data["sdkVersion"] = original_sdk_version
@pytest.fixture(name="set_country", autouse=True)
def set_country_fixture(hass: HomeAssistant) -> Generator[None]:
"""Set the country for the test."""
original_country = hass.config.country
# Set a default country to avoid asking the user to select it.
hass.config.country = "US"
yield
# Reset the country after the test.
hass.config.country = original_country
async def test_manual(hass: HomeAssistant) -> None:
"""Test we create an entry with manual step."""
@@ -4601,3 +4612,324 @@ async def test_recommended_usb_discovery(
}
assert len(mock_setup.mock_calls) == 1
assert len(mock_setup_entry.mock_calls) == 1
@pytest.mark.usefixtures("supervisor", "addon_installed", "addon_info", "unload_entry")
async def test_addon_rf_region_new_network(
hass: HomeAssistant,
setup_entry: AsyncMock,
set_addon_options: AsyncMock,
start_addon: AsyncMock,
) -> None:
"""Test RF region selection for new network when country is None."""
device = "/test"
hass.config.country = None
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] is FlowResultType.MENU
assert result["step_id"] == "installation_type"
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {"next_step_id": "intent_recommended"}
)
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
"usb_path": device,
},
)
assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "rf_region"
# Check that all expected RF regions are available
data_schema = result["data_schema"]
assert data_schema is not None
schema = data_schema.schema
rf_region_field = schema["rf_region"]
selector_options = rf_region_field.config["options"]
expected_regions = [
"Australia/New Zealand",
"China",
"Europe",
"Hong Kong",
"India",
"Israel",
"Japan",
"Korea",
"Russia",
"USA",
]
assert selector_options == expected_regions
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {"rf_region": "Europe"}
)
assert result["type"] is FlowResultType.SHOW_PROGRESS
assert result["step_id"] == "start_addon"
# Verify RF region was set in addon config
assert set_addon_options.call_count == 1
assert set_addon_options.call_args == call(
"core_zwave_js",
AddonsOptions(
config={
"device": device,
"s0_legacy_key": "",
"s2_access_control_key": "",
"s2_authenticated_key": "",
"s2_unauthenticated_key": "",
"lr_s2_access_control_key": "",
"lr_s2_authenticated_key": "",
"rf_region": "Europe",
}
),
)
await hass.async_block_till_done()
result = await hass.config_entries.flow.async_configure(result["flow_id"])
await hass.async_block_till_done()
assert result["type"] is FlowResultType.CREATE_ENTRY
assert start_addon.call_count == 1
assert start_addon.call_args == call("core_zwave_js")
assert setup_entry.call_count == 1
# avoid unload entry in teardown
entry = result["result"]
await hass.config_entries.async_unload(entry.entry_id)
assert entry.state is config_entries.ConfigEntryState.NOT_LOADED
@pytest.mark.usefixtures("supervisor", "addon_running")
async def test_addon_rf_region_migrate_network(
hass: HomeAssistant,
client: MagicMock,
integration: MockConfigEntry,
restart_addon: AsyncMock,
addon_options: dict[str, Any],
set_addon_options: AsyncMock,
get_server_version: AsyncMock,
) -> None:
"""Test migration flow with add-on."""
hass.config.country = None
version_info = get_server_version.return_value
entry = integration
assert client.connect.call_count == 1
assert client.driver.controller.home_id == 3245146787
assert entry.unique_id == "3245146787"
hass.config_entries.async_update_entry(
entry,
data={
"url": "ws://localhost:3000",
"use_addon": True,
"usb_path": "/dev/ttyUSB0",
},
)
addon_options["device"] = "/dev/ttyUSB0"
async def mock_backup_nvm_raw():
await asyncio.sleep(0)
client.driver.controller.emit(
"nvm backup progress", {"bytesRead": 100, "total": 200}
)
return b"test_nvm_data"
client.driver.controller.async_backup_nvm_raw = AsyncMock(
side_effect=mock_backup_nvm_raw
)
async def mock_restore_nvm(data: bytes, options: dict[str, bool] | None = None):
client.driver.controller.emit(
"nvm convert progress",
{"event": "nvm convert progress", "bytesRead": 100, "total": 200},
)
await asyncio.sleep(0)
client.driver.controller.emit(
"nvm restore progress",
{"event": "nvm restore progress", "bytesWritten": 100, "total": 200},
)
client.driver.controller.data["homeId"] = 3245146787
client.driver.emit(
"driver ready", {"event": "driver ready", "source": "driver"}
)
client.driver.controller.async_restore_nvm = AsyncMock(side_effect=mock_restore_nvm)
events = async_capture_events(
hass, data_entry_flow.EVENT_DATA_ENTRY_FLOW_PROGRESS_UPDATE
)
result = await entry.start_reconfigure_flow(hass)
assert result["type"] is FlowResultType.MENU
assert result["step_id"] == "reconfigure"
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {"next_step_id": "intent_migrate"}
)
assert result["type"] is FlowResultType.SHOW_PROGRESS
assert result["step_id"] == "backup_nvm"
with patch("pathlib.Path.write_bytes") as mock_file:
await hass.async_block_till_done()
assert client.driver.controller.async_backup_nvm_raw.call_count == 1
assert mock_file.call_count == 1
assert len(events) == 1
assert events[0].data["progress"] == 0.5
events.clear()
result = await hass.config_entries.flow.async_configure(result["flow_id"])
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "instruct_unplug"
assert entry.state is config_entries.ConfigEntryState.NOT_LOADED
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "choose_serial_port"
data_schema = result["data_schema"]
assert data_schema is not None
assert data_schema.schema[CONF_USB_PATH]
# Ensure the old usb path is not in the list of options
with pytest.raises(InInvalid):
data_schema.schema[CONF_USB_PATH](addon_options["device"])
version_info.home_id = 5678
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={
CONF_USB_PATH: "/test",
},
)
assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "rf_region"
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {"rf_region": "Europe"}
)
assert result["type"] is FlowResultType.SHOW_PROGRESS
assert result["step_id"] == "start_addon"
assert set_addon_options.call_args == call(
"core_zwave_js",
AddonsOptions(
config={
"device": "/test",
"rf_region": "Europe",
}
),
)
await hass.async_block_till_done()
assert restart_addon.call_args == call("core_zwave_js")
result = await hass.config_entries.flow.async_configure(result["flow_id"])
assert entry.unique_id == "5678"
version_info.home_id = 3245146787
assert result["type"] is FlowResultType.SHOW_PROGRESS
assert result["step_id"] == "restore_nvm"
assert client.connect.call_count == 2
await hass.async_block_till_done()
assert client.connect.call_count == 4
assert entry.state is config_entries.ConfigEntryState.LOADED
assert client.driver.controller.async_restore_nvm.call_count == 1
assert len(events) == 2
assert events[0].data["progress"] == 0.25
assert events[1].data["progress"] == 0.75
result = await hass.config_entries.flow.async_configure(result["flow_id"])
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "migration_successful"
assert entry.data["url"] == "ws://host1:3001"
assert entry.data["usb_path"] == "/test"
assert entry.data["use_addon"] is True
assert entry.unique_id == "3245146787"
assert client.driver.controller.home_id == 3245146787
@pytest.mark.usefixtures("supervisor", "addon_installed", "unload_entry")
@pytest.mark.parametrize(("country", "rf_region"), [("US", "Automatic"), (None, "USA")])
async def test_addon_skip_rf_region(
hass: HomeAssistant,
setup_entry: AsyncMock,
addon_options: dict[str, Any],
set_addon_options: AsyncMock,
start_addon: AsyncMock,
country: str | None,
rf_region: str,
) -> None:
"""Test RF region selection is skipped if not needed."""
device = "/test"
addon_options["rf_region"] = rf_region
hass.config.country = country
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] is FlowResultType.MENU
assert result["step_id"] == "installation_type"
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {"next_step_id": "intent_recommended"}
)
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
"usb_path": device,
},
)
assert result["type"] is FlowResultType.SHOW_PROGRESS
assert result["step_id"] == "start_addon"
# Verify RF region was set in addon config
assert set_addon_options.call_count == 1
assert set_addon_options.call_args == call(
"core_zwave_js",
AddonsOptions(
config={
"device": device,
"s0_legacy_key": "",
"s2_access_control_key": "",
"s2_authenticated_key": "",
"s2_unauthenticated_key": "",
"lr_s2_access_control_key": "",
"lr_s2_authenticated_key": "",
"rf_region": rf_region,
}
),
)
await hass.async_block_till_done()
result = await hass.config_entries.flow.async_configure(result["flow_id"])
await hass.async_block_till_done()
assert result["type"] is FlowResultType.CREATE_ENTRY
assert start_addon.call_count == 1
assert start_addon.call_args == call("core_zwave_js")
assert setup_entry.call_count == 1
# avoid unload entry in teardown
entry = result["result"]
await hass.config_entries.async_unload(entry.entry_id)
assert entry.state is config_entries.ConfigEntryState.NOT_LOADED

View File

@@ -1045,6 +1045,183 @@ async def test_last_seen_statistics_sensors(
assert state.state == "2024-01-01T12:00:00+00:00"
async def test_rssi_sensor_error(
hass: HomeAssistant,
zp3111: Node,
integration: MockConfigEntry,
entity_registry: er.EntityRegistry,
) -> None:
"""Test rssi sensor error."""
entity_id = "sensor.4_in_1_sensor_signal_strength"
entity_registry.async_update_entity(entity_id, disabled_by=None)
# reload integration and check if entity is correctly there
await hass.config_entries.async_reload(integration.entry_id)
await hass.async_block_till_done()
state = hass.states.get(entity_id)
assert state
assert state.state == "unknown"
# Fire statistics updated event for node
event = Event(
"statistics updated",
{
"source": "node",
"event": "statistics updated",
"nodeId": zp3111.node_id,
"statistics": {
"commandsTX": 1,
"commandsRX": 2,
"commandsDroppedTX": 3,
"commandsDroppedRX": 4,
"timeoutResponse": 5,
"rtt": 6,
"rssi": 7, # baseline
"lwr": {
"protocolDataRate": 1,
"rssi": 1,
"repeaters": [],
"repeaterRSSI": [],
"routeFailedBetween": [],
},
"nlwr": {
"protocolDataRate": 2,
"rssi": 2,
"repeaters": [],
"repeaterRSSI": [],
"routeFailedBetween": [],
},
"lastSeen": "2024-01-01T00:00:00+0000",
},
},
)
zp3111.receive_event(event)
await hass.async_block_till_done()
state = hass.states.get(entity_id)
assert state
assert state.state == "7"
event = Event(
"statistics updated",
{
"source": "node",
"event": "statistics updated",
"nodeId": zp3111.node_id,
"statistics": {
"commandsTX": 1,
"commandsRX": 2,
"commandsDroppedTX": 3,
"commandsDroppedRX": 4,
"timeoutResponse": 5,
"rtt": 6,
"rssi": 125, # no signal detected
"lwr": {
"protocolDataRate": 1,
"rssi": 1,
"repeaters": [],
"repeaterRSSI": [],
"routeFailedBetween": [],
},
"nlwr": {
"protocolDataRate": 2,
"rssi": 2,
"repeaters": [],
"repeaterRSSI": [],
"routeFailedBetween": [],
},
"lastSeen": "2024-01-01T00:00:00+0000",
},
},
)
zp3111.receive_event(event)
await hass.async_block_till_done()
state = hass.states.get(entity_id)
assert state
assert state.state == "unknown"
event = Event(
"statistics updated",
{
"source": "node",
"event": "statistics updated",
"nodeId": zp3111.node_id,
"statistics": {
"commandsTX": 1,
"commandsRX": 2,
"commandsDroppedTX": 3,
"commandsDroppedRX": 4,
"timeoutResponse": 5,
"rtt": 6,
"rssi": 127, # not available
"lwr": {
"protocolDataRate": 1,
"rssi": 1,
"repeaters": [],
"repeaterRSSI": [],
"routeFailedBetween": [],
},
"nlwr": {
"protocolDataRate": 2,
"rssi": 2,
"repeaters": [],
"repeaterRSSI": [],
"routeFailedBetween": [],
},
"lastSeen": "2024-01-01T00:00:00+0000",
},
},
)
zp3111.receive_event(event)
await hass.async_block_till_done()
state = hass.states.get(entity_id)
assert state
assert state.state == "unavailable"
event = Event(
"statistics updated",
{
"source": "node",
"event": "statistics updated",
"nodeId": zp3111.node_id,
"statistics": {
"commandsTX": 1,
"commandsRX": 2,
"commandsDroppedTX": 3,
"commandsDroppedRX": 4,
"timeoutResponse": 5,
"rtt": 6,
"rssi": 126, # receiver saturated
"lwr": {
"protocolDataRate": 1,
"rssi": 1,
"repeaters": [],
"repeaterRSSI": [],
"routeFailedBetween": [],
},
"nlwr": {
"protocolDataRate": 2,
"rssi": 2,
"repeaters": [],
"repeaterRSSI": [],
"routeFailedBetween": [],
},
"lastSeen": "2024-01-01T00:00:00+0000",
},
},
)
zp3111.receive_event(event)
await hass.async_block_till_done()
state = hass.states.get(entity_id)
assert state
assert state.state == "unknown"
ENERGY_PRODUCTION_ENTITY_MAP = {
"energy_production_power": {
"state": 1.23,