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 type: boolean
env: env:
CACHE_VERSION: 4 CACHE_VERSION: 6
UV_CACHE_VERSION: 1 UV_CACHE_VERSION: 1
MYPY_CACHE_VERSION: 1 MYPY_CACHE_VERSION: 1
HA_SHORT_VERSION: "2025.8" HA_SHORT_VERSION: "2025.8"

View File

@@ -1,11 +1,11 @@
"""Alexa Devices integration.""" """Alexa Devices integration."""
from homeassistant.const import Platform from homeassistant.const import CONF_COUNTRY, Platform
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import aiohttp_client, config_validation as cv from homeassistant.helpers import aiohttp_client, config_validation as cv
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
from .const import DOMAIN from .const import _LOGGER, COUNTRY_DOMAINS, DOMAIN
from .coordinator import AmazonConfigEntry, AmazonDevicesCoordinator from .coordinator import AmazonConfigEntry, AmazonDevicesCoordinator
from .services import async_setup_services from .services import async_setup_services
@@ -40,6 +40,32 @@ async def async_setup_entry(hass: HomeAssistant, entry: AmazonConfigEntry) -> bo
return True 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: async def async_unload_entry(hass: HomeAssistant, entry: AmazonConfigEntry) -> bool:
"""Unload a config entry.""" """Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)

View File

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

View File

@@ -6,3 +6,22 @@ _LOGGER = logging.getLogger(__package__)
DOMAIN = "alexa_devices" DOMAIN = "alexa_devices"
CONF_LOGIN_DATA = "login_data" 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 aiohttp import ClientSession
from homeassistant.config_entries import ConfigEntry 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.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
@@ -44,7 +44,6 @@ class AmazonDevicesCoordinator(DataUpdateCoordinator[dict[str, AmazonDevice]]):
) )
self.api = AmazonEchoApi( self.api = AmazonEchoApi(
session, session,
entry.data[CONF_COUNTRY],
entry.data[CONF_USERNAME], entry.data[CONF_USERNAME],
entry.data[CONF_PASSWORD], entry.data[CONF_PASSWORD],
entry.data[CONF_LOGIN_DATA], entry.data[CONF_LOGIN_DATA],

View File

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

View File

@@ -1,7 +1,6 @@
{ {
"common": { "common": {
"data_code": "One-time password (OTP code)", "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_username": "The email address of your Amazon account.",
"data_description_password": "The password 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.", "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": { "step": {
"user": { "user": {
"data": { "data": {
"country": "[%key:common::config_flow::data::country%]",
"username": "[%key:common::config_flow::data::username%]", "username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]", "password": "[%key:common::config_flow::data::password%]",
"code": "[%key:component::alexa_devices::common::data_code%]" "code": "[%key:component::alexa_devices::common::data_code%]"
}, },
"data_description": { "data_description": {
"country": "[%key:component::alexa_devices::common::data_description_country%]",
"username": "[%key:component::alexa_devices::common::data_description_username%]", "username": "[%key:component::alexa_devices::common::data_description_username%]",
"password": "[%key:component::alexa_devices::common::data_description_password%]", "password": "[%key:component::alexa_devices::common::data_description_password%]",
"code": "[%key:component::alexa_devices::common::data_description_code%]" "code": "[%key:component::alexa_devices::common::data_description_code%]"
@@ -46,7 +43,6 @@
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"cannot_retrieve_data": "Unable to retrieve data from Amazon. Please try again later.", "cannot_retrieve_data": "Unable to retrieve data from Amazon. Please try again later.",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", "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%]" "unknown": "[%key:common::config_flow::error::unknown%]"
} }
}, },

View File

@@ -16,7 +16,7 @@ from homeassistant.helpers.selector import (
SelectSelectorMode, 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" API_URL = "https://app.amber.com.au/developers"
@@ -64,7 +64,9 @@ class AmberElectricConfigFlow(ConfigFlow, domain=DOMAIN):
api = amberelectric.AmberApi(api_client) api = amberelectric.AmberApi(api_client)
try: 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: except amberelectric.ApiException as api_exception:
if api_exception.status == 403: if api_exception.status == 403:
self._errors[CONF_API_TOKEN] = "invalid_api_token" self._errors[CONF_API_TOKEN] = "invalid_api_token"

View File

@@ -22,3 +22,5 @@ SERVICE_GET_FORECASTS = "get_forecasts"
GENERAL_CHANNEL = "general" GENERAL_CHANNEL = "general"
CONTROLLED_LOAD_CHANNEL = "controlled_load" CONTROLLED_LOAD_CHANNEL = "controlled_load"
FEED_IN_CHANNEL = "feed_in" 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.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import LOGGER from .const import LOGGER, REQUEST_TIMEOUT
from .helpers import normalize_descriptor from .helpers import normalize_descriptor
type AmberConfigEntry = ConfigEntry[AmberUpdateCoordinator] type AmberConfigEntry = ConfigEntry[AmberUpdateCoordinator]
@@ -82,7 +82,11 @@ class AmberUpdateCoordinator(DataUpdateCoordinator):
"grid": {}, "grid": {},
} }
try: 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] intervals = [interval.actual_instance for interval in data]
except ApiException as api_exception: except ApiException as api_exception:
raise UpdateFailed("Missing price data, skipping update") from api_exception raise UpdateFailed("Missing price data, skipping update") from api_exception

View File

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

View File

@@ -69,12 +69,7 @@ class SHCEntity(SHCBaseEntity):
manufacturer=device.manufacturer, manufacturer=device.manufacturer,
model=device.device_model, model=device.device_model,
name=device.name, name=device.name,
via_device=( via_device=(DOMAIN, device.root_device_id),
DOMAIN,
device.parent_device_id
if device.parent_device_id is not None
else parent_id,
),
) )
super().__init__(device=device, parent_id=parent_id, entry_id=entry_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", "documentation": "https://www.home-assistant.io/integrations/bosch_shc",
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["boschshcpy"], "loggers": ["boschshcpy"],
"requirements": ["boschshcpy==0.2.91"], "requirements": ["boschshcpy==0.2.107"],
"zeroconf": [ "zeroconf": [
{ {
"type": "_http._tcp.local.", "type": "_http._tcp.local.",

View File

@@ -17,7 +17,7 @@ DEFAULT_NEW_CONFIG_ALLOW_ALLOW_SERVICE_CALLS = False
DEFAULT_PORT: Final = 6053 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) STABLE_BLE_VERSION = AwesomeVersion(STABLE_BLE_VERSION_STR)
PROJECT_URLS = { PROJECT_URLS = {
"esphome.bluetooth-proxy": "https://esphome.github.io/bluetooth-proxies/", "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_FRONTEND_REPO = "development_repo"
CONF_JS_VERSION = "javascript_version" CONF_JS_VERSION = "javascript_version"
DEFAULT_THEME_COLOR = "#03A9F4" DEFAULT_THEME_COLOR = "#2980b9"
DATA_PANELS: HassKey[dict[str, Panel]] = HassKey("frontend_panels") DATA_PANELS: HassKey[dict[str, Panel]] = HassKey("frontend_panels")

View File

@@ -20,5 +20,5 @@
"documentation": "https://www.home-assistant.io/integrations/frontend", "documentation": "https://www.home-assistant.io/integrations/frontend",
"integration_type": "system", "integration_type": "system",
"quality_scale": "internal", "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 = { UNSUPPORTED_REASONS = {
"apparmor", "apparmor",
"cgroup_version",
"connectivity_check", "connectivity_check",
"content_trust", "content_trust",
"dbus", "dbus",
"dns_server", "dns_server",
"docker_configuration", "docker_configuration",
"docker_version", "docker_version",
"cgroup_version",
"job_conditions", "job_conditions",
"lxc", "lxc",
"network_manager", "network_manager",
"os", "os",
"os_agent", "os_agent",
"os_version",
"restart_policy", "restart_policy",
"software", "software",
"source_mods", "source_mods",
@@ -80,6 +81,7 @@ UNSUPPORTED_REASONS = {
"systemd", "systemd",
"systemd_journal", "systemd_journal",
"systemd_resolved", "systemd_resolved",
"virtualization_image",
} }
# Some unsupported reasons also mark the system as unhealthy. If the unsupported reason # 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. # provides no additional information beyond the unhealthy one then skip that repair.

View File

@@ -5,5 +5,5 @@
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/holiday", "documentation": "https://www.home-assistant.io/integrations/holiday",
"iot_class": "local_polling", "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.helpers import config_validation as cv
from homeassistant.util import slugify from homeassistant.util import slugify
from .account import IcloudAccount from .account import IcloudAccount, IcloudConfigEntry
from .const import ( from .const import (
ATTR_ACCOUNT, ATTR_ACCOUNT,
ATTR_DEVICE_NAME, ATTR_DEVICE_NAME,
@@ -92,8 +92,10 @@ def lost_device(service: ServiceCall) -> None:
def update_account(service: ServiceCall) -> None: def update_account(service: ServiceCall) -> None:
"""Call the update function of an iCloud account.""" """Call the update function of an iCloud account."""
if (account := service.data.get(ATTR_ACCOUNT)) is None: if (account := service.data.get(ATTR_ACCOUNT)) is None:
for account in service.hass.data[DOMAIN].values(): # Update all accounts when no specific account is provided
account.keep_alive() entry: IcloudConfigEntry
for entry in service.hass.config_entries.async_loaded_entries(DOMAIN):
entry.runtime_data.keep_alive()
else: else:
_get_account(service.hass, account).keep_alive() _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: if account_identifier is None:
return None return None
icloud_account: IcloudAccount | None = hass.data[DOMAIN].get(account_identifier) entry: IcloudConfigEntry
if icloud_account is None: for entry in hass.config_entries.async_loaded_entries(DOMAIN):
for account in hass.data[DOMAIN].values(): if entry.runtime_data.username == account_identifier:
if account.username == account_identifier: return entry.runtime_data
icloud_account = account
if icloud_account is None: raise ValueError(f"No iCloud account with username or name {account_identifier}")
raise ValueError(
f"No iCloud account with username or name {account_identifier}"
)
return icloud_account
@callback @callback

View File

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

View File

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

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/imgw_pib", "documentation": "https://www.home-assistant.io/integrations/imgw_pib",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"quality_scale": "silver", "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}", name=f"{DOMAIN}_{ha_bridge.device.device_id}",
) )
self.data = {} self.data = ha_bridge.update_status(None)
self.api = ha_bridge self.api = ha_bridge
self.device_id = ha_bridge.device.device_id self.device_id = ha_bridge.device.device_id
self.sub_id = ha_bridge.sub_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: async def async_set_valve_position(self, position: int) -> None:
"""Move the valve to a specific position.""" """Move the valve to a specific position."""
await self.send_device_command( if position > 0:
ValveConfigurationAndControl.Commands.Open(targetLevel=position) await self.send_device_command(
) ValveConfigurationAndControl.Commands.Open(targetLevel=position)
)
return
await self.send_device_command(ValveConfigurationAndControl.Commands.Close())
@callback @callback
def _update_from_device(self) -> None: def _update_from_device(self) -> None:

View File

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

View File

@@ -7,5 +7,5 @@
"documentation": "https://www.home-assistant.io/integrations/mystrom", "documentation": "https://www.home-assistant.io/integrations/mystrom",
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["pymystrom"], "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 from aionanoleaf import InvalidToken, Nanoleaf, Unauthorized, Unavailable
import voluptuous as vol 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.const import CONF_HOST, CONF_TOKEN
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.json import save_json from homeassistant.helpers.json import save_json
@@ -200,7 +205,9 @@ class NanoleafConfigFlow(ConfigFlow, domain=DOMAIN):
return self.async_abort(reason="unknown") return self.async_abort(reason="unknown")
name = self.nanoleaf.name 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}) self._abort_if_unique_id_configured({CONF_HOST: self.nanoleaf.host})
if discovery_integration_import: if discovery_integration_import:

View File

@@ -12,5 +12,5 @@
"integration_type": "hub", "integration_type": "hub",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["pyatmo"], "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", "documentation": "https://www.home-assistant.io/integrations/onvif",
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["onvif", "wsdiscovery", "zeep"], "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: def _adjust_schema(schema: dict[str, Any]) -> None:
"""Adjust the schema to be compatible with OpenAI API.""" """Adjust the schema to be compatible with OpenAI API."""
if schema["type"] == "object": if schema["type"] == "object":
schema.setdefault("strict", True)
schema.setdefault("additionalProperties", False)
if "properties" not in schema: if "properties" not in schema:
return return
@@ -124,8 +126,6 @@ def _format_structured_output(
_adjust_schema(result) _adjust_schema(result)
result["strict"] = True
result["additionalProperties"] = False
return result return result

View File

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

View File

@@ -8,5 +8,5 @@
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["renault_api"], "loggers": ["renault_api"],
"quality_scale": "silver", "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", "iot_class": "local_push",
"loggers": ["aiorussound"], "loggers": ["aiorussound"],
"quality_scale": "silver", "quality_scale": "silver",
"requirements": ["aiorussound==4.8.0"], "requirements": ["aiorussound==4.8.1"],
"zeroconf": ["_rio._tcp.local."] "zeroconf": ["_rio._tcp.local."]
} }

View File

@@ -30,5 +30,5 @@
"iot_class": "cloud_push", "iot_class": "cloud_push",
"loggers": ["pysmartthings"], "loggers": ["pysmartthings"],
"quality_scale": "bronze", "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: def volume_level(self) -> float | None:
"""Volume level of the media player (0..1).""" """Volume level of the media player (0..1)."""
if self._player.volume is not None: if self._player.volume is not None:
return int(float(self._player.volume)) / 100.0 return float(self._player.volume) / 100.0
return None return None
@@ -435,7 +435,7 @@ class SqueezeBoxMediaPlayerEntity(SqueezeboxEntity, MediaPlayerEntity):
async def async_set_volume_level(self, volume: float) -> None: async def async_set_volume_level(self, volume: float) -> None:
"""Set volume level, range 0..1.""" """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._player.async_set_volume(volume_percent)
await self.coordinator.async_refresh() await self.coordinator.async_refresh()

View File

@@ -260,6 +260,8 @@ class VolvoMediumIntervalCoordinator(VolvoBaseCoordinator):
"Volvo medium interval coordinator", "Volvo medium interval coordinator",
) )
self._supported_capabilities: list[str] = []
async def _async_determine_api_calls( async def _async_determine_api_calls(
self, self,
) -> list[Callable[[], Coroutine[Any, Any, Any]]]: ) -> list[Callable[[], Coroutine[Any, Any, Any]]]:
@@ -267,6 +269,31 @@ class VolvoMediumIntervalCoordinator(VolvoBaseCoordinator):
capabilities = await self.api.async_get_energy_capabilities() capabilities = await self.api.async_get_energy_capabilities()
if capabilities.get("isSupported", False): 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 [] 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: def _charging_power_value(field: VolvoCarsValue) -> int:
return ( return (
int(field.value) field.value
if isinstance(field, VolvoCarsValueStatusField) and field.status == "OK" if isinstance(field, VolvoCarsValueStatusField) and isinstance(field.value, int)
else 0 else 0
) )

View File

@@ -2,16 +2,23 @@
from __future__ import annotations from __future__ import annotations
from dataclasses import asdict
from typing import Any from typing import Any
from yarl import URL 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.components.webhook import async_generate_url as webhook_generate_url
from homeassistant.const import CONF_WEBHOOK_ID from homeassistant.const import CONF_WEBHOOK_ID
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from . import CONF_CLOUDHOOK_URL, WithingsConfigEntry from . import CONF_CLOUDHOOK_URL, WithingsConfigEntry
TO_REDACT = {
"device_id",
"hashed_device_id",
}
async def async_get_config_entry_diagnostics( async def async_get_config_entry_diagnostics(
hass: HomeAssistant, entry: WithingsConfigEntry 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_sleep_data": withings_data.sleep_coordinator.data is not None,
"received_workout_data": withings_data.workout_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, "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", "iot_class": "local_polling",
"loggers": ["holidays"], "loggers": ["holidays"],
"quality_scale": "internal", "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", "documentation": "https://www.home-assistant.io/integrations/zimi",
"iot_class": "local_push", "iot_class": "local_push",
"quality_scale": "bronze", "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.core import HomeAssistant, callback
from homeassistant.data_entry_flow import AbortFlow from homeassistant.data_entry_flow import AbortFlow
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import selector
from homeassistant.helpers.hassio import is_hassio from homeassistant.helpers.hassio import is_hassio
from homeassistant.helpers.service_info.hassio import HassioServiceInfo from homeassistant.helpers.service_info.hassio import HassioServiceInfo
from homeassistant.helpers.service_info.usb import UsbServiceInfo 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_LR_S2_AUTHENTICATED_KEY: CONF_LR_S2_AUTHENTICATED_KEY,
} }
CONF_ADDON_RF_REGION = "rf_region"
EXAMPLE_SERVER_URL = "ws://localhost:3000" EXAMPLE_SERVER_URL = "ws://localhost:3000"
ON_SUPERVISOR_SCHEMA = vol.Schema({vol.Optional(CONF_USE_ADDON, default=True): bool}) ON_SUPERVISOR_SCHEMA = vol.Schema({vol.Optional(CONF_USE_ADDON, default=True): bool})
MIN_MIGRATION_SDK_VERSION = AwesomeVersion("6.61") 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" "#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: def get_manual_schema(user_input: dict[str, Any]) -> vol.Schema:
"""Return a schema for the manual step.""" """Return a schema for the manual step."""
@@ -195,10 +211,12 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN):
self.backup_data: bytes | None = None self.backup_data: bytes | None = None
self.backup_filepath: Path | None = None self.backup_filepath: Path | None = None
self.use_addon = False self.use_addon = False
self._addon_config_updates: dict[str, Any] = {}
self._migrating = False self._migrating = False
self._reconfigure_config_entry: ZwaveJSConfigEntry | None = None self._reconfigure_config_entry: ZwaveJSConfigEntry | None = None
self._usb_discovery = False self._usb_discovery = False
self._recommended_install = False self._recommended_install = False
self._rf_region: str | None = None
async def async_step_install_addon( async def async_step_install_addon(
self, user_input: dict[str, Any] | None = None 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 self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult: ) -> ConfigFlowResult:
"""Start Z-Wave JS add-on.""" """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: if not self.start_task:
self.start_task = self.hass.async_create_task(self._async_start_addon()) 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({CONF_USE_ADDON: True})
return await self.async_step_on_supervisor() 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( async def async_step_on_supervisor(
self, user_input: dict[str, Any] | None = None self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult: ) -> ConfigFlowResult:
@@ -728,7 +788,7 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN):
CONF_ADDON_LR_S2_AUTHENTICATED_KEY: self.lr_s2_authenticated_key, 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() return await self.async_step_start_addon()
# Network already exists, go to security keys step # 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, 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() return await self.async_step_start_addon()
data_schema = vol.Schema( data_schema = vol.Schema(
@@ -1004,7 +1064,7 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN):
if user_input is not None: if user_input is not None:
if self.usb_path: if self.usb_path:
# USB discovery was used, so the device is already known. # 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() return await self.async_step_start_addon()
# Now that the old controller is gone, we can scan for serial ports again # Now that the old controller is gone, we can scan for serial ports again
return await self.async_step_choose_serial_port() 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, 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) await self._async_set_addon_config(addon_config_updates)
if addon_info.state == AddonState.RUNNING and not self.restart_addon: if addon_info.state == AddonState.RUNNING and not self.restart_addon:
@@ -1207,7 +1269,7 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN):
"""Choose a serial port.""" """Choose a serial port."""
if user_input is not None: if user_input is not None:
self.usb_path = user_input[CONF_USB_PATH] 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() return await self.async_step_start_addon()
try: try:

View File

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

View File

@@ -113,6 +113,16 @@
"description": "[%key:component::zwave_js::config::step::on_supervisor::description%]", "description": "[%key:component::zwave_js::config::step::on_supervisor::description%]",
"title": "[%key:component::zwave_js::config::step::on_supervisor::title%]" "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": { "start_addon": {
"title": "Configuring add-on" "title": "Configuring add-on"
}, },

View File

@@ -25,7 +25,7 @@ if TYPE_CHECKING:
APPLICATION_NAME: Final = "HomeAssistant" APPLICATION_NAME: Final = "HomeAssistant"
MAJOR_VERSION: Final = 2025 MAJOR_VERSION: Final = 2025
MINOR_VERSION: Final = 8 MINOR_VERSION: Final = 8
PATCH_VERSION: Final = "2" PATCH_VERSION: Final = "3"
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__: Final = f"{__short_version__}.{PATCH_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 13, 2) 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 av==13.1.0
awesomeversion==25.5.0 awesomeversion==25.5.0
bcrypt==4.3.0 bcrypt==4.3.0
bleak-retry-connector==4.0.1 bleak-retry-connector==4.0.2
bleak==1.0.1 bleak==1.0.1
bluetooth-adapters==2.0.0 bluetooth-adapters==2.0.0
bluetooth-auto-recovery==1.5.2 bluetooth-auto-recovery==1.5.2
@@ -38,7 +38,7 @@ habluetooth==4.0.2
hass-nabucasa==0.111.2 hass-nabucasa==0.111.2
hassil==2.2.3 hassil==2.2.3
home-assistant-bluetooth==1.13.1 home-assistant-bluetooth==1.13.1
home-assistant-frontend==20250811.0 home-assistant-frontend==20250811.1
home-assistant-intents==2025.7.30 home-assistant-intents==2025.7.30
httpx==0.28.1 httpx==0.28.1
ifaddr==0.2.0 ifaddr==0.2.0
@@ -221,3 +221,6 @@ num2words==0.5.14
# downgraded or upgraded by custom components # downgraded or upgraded by custom components
# This ensures all use the same version # This ensures all use the same version
pymodbus==3.9.2 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] [project]
name = "homeassistant" name = "homeassistant"
version = "2025.8.2" version = "2025.8.3"
license = "Apache-2.0" license = "Apache-2.0"
license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"] license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"]
description = "Open-source home automation platform running on Python 3." 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 aioairzone==1.0.0
# homeassistant.components.alexa_devices # homeassistant.components.alexa_devices
aioamazondevices==4.0.0 aioamazondevices==5.0.0
# homeassistant.components.ambient_network # homeassistant.components.ambient_network
# homeassistant.components.ambient_station # homeassistant.components.ambient_station
@@ -375,7 +375,7 @@ aioridwell==2024.01.0
aioruckus==0.42 aioruckus==0.42
# homeassistant.components.russound_rio # homeassistant.components.russound_rio
aiorussound==4.8.0 aiorussound==4.8.1
# homeassistant.components.ruuvi_gateway # homeassistant.components.ruuvi_gateway
aioruuvigateway==0.1.0 aioruuvigateway==0.1.0
@@ -625,7 +625,7 @@ bizkaibus==0.1.1
bleak-esphome==3.1.0 bleak-esphome==3.1.0
# homeassistant.components.bluetooth # homeassistant.components.bluetooth
bleak-retry-connector==4.0.1 bleak-retry-connector==4.0.2
# homeassistant.components.bluetooth # homeassistant.components.bluetooth
bleak==1.0.1 bleak==1.0.1
@@ -667,7 +667,7 @@ bond-async==0.2.1
bosch-alarm-mode2==0.4.6 bosch-alarm-mode2==0.4.6
# homeassistant.components.bosch_shc # homeassistant.components.bosch_shc
boschshcpy==0.2.91 boschshcpy==0.2.107
# homeassistant.components.amazon_polly # homeassistant.components.amazon_polly
# homeassistant.components.route53 # homeassistant.components.route53
@@ -1171,10 +1171,10 @@ hole==0.9.0
# homeassistant.components.holiday # homeassistant.components.holiday
# homeassistant.components.workday # homeassistant.components.workday
holidays==0.78 holidays==0.79
# homeassistant.components.frontend # homeassistant.components.frontend
home-assistant-frontend==20250811.0 home-assistant-frontend==20250811.1
# homeassistant.components.conversation # homeassistant.components.conversation
home-assistant-intents==2025.7.30 home-assistant-intents==2025.7.30
@@ -1240,7 +1240,7 @@ ihcsdk==2.8.5
imeon_inverter_api==0.3.14 imeon_inverter_api==0.3.14
# homeassistant.components.imgw_pib # homeassistant.components.imgw_pib
imgw_pib==1.5.3 imgw_pib==1.5.4
# homeassistant.components.incomfort # homeassistant.components.incomfort
incomfort-client==0.6.9 incomfort-client==0.6.9
@@ -1594,7 +1594,7 @@ ondilo==0.5.0
onedrive-personal-sdk==0.0.14 onedrive-personal-sdk==0.0.14
# homeassistant.components.onvif # homeassistant.components.onvif
onvif-zeep-async==4.0.3 onvif-zeep-async==4.0.4
# homeassistant.components.opengarage # homeassistant.components.opengarage
open-garage==0.2.0 open-garage==0.2.0
@@ -1628,7 +1628,7 @@ openwrt-luci-rpc==1.1.17
openwrt-ubus-rpc==0.0.2 openwrt-ubus-rpc==0.0.2
# homeassistant.components.opower # homeassistant.components.opower
opower==0.15.1 opower==0.15.2
# homeassistant.components.oralb # homeassistant.components.oralb
oralb-ble==0.17.6 oralb-ble==0.17.6
@@ -1852,7 +1852,7 @@ pyasuswrt==0.1.21
pyatag==0.3.5.3 pyatag==0.3.5.3
# homeassistant.components.netatmo # homeassistant.components.netatmo
pyatmo==9.2.1 pyatmo==9.2.3
# homeassistant.components.apple_tv # homeassistant.components.apple_tv
pyatv==0.16.1 pyatv==0.16.1
@@ -2352,7 +2352,7 @@ pysmappee==0.2.29
pysmarlaapi==0.9.1 pysmarlaapi==0.9.1
# homeassistant.components.smartthings # homeassistant.components.smartthings
pysmartthings==3.2.8 pysmartthings==3.2.9
# homeassistant.components.smarty # homeassistant.components.smarty
pysmarty2==0.10.2 pysmarty2==0.10.2
@@ -2478,7 +2478,7 @@ python-miio==0.5.12
python-mpd2==3.1.1 python-mpd2==3.1.1
# homeassistant.components.mystrom # homeassistant.components.mystrom
python-mystrom==2.4.0 python-mystrom==2.5.0
# homeassistant.components.open_router # homeassistant.components.open_router
python-open-router==0.3.1 python-open-router==0.3.1
@@ -2660,7 +2660,7 @@ refoss-ha==1.2.5
regenmaschine==2024.03.0 regenmaschine==2024.03.0
# homeassistant.components.renault # homeassistant.components.renault
renault-api==0.3.1 renault-api==0.4.0
# homeassistant.components.renson # homeassistant.components.renson
renson-endura-delta==1.7.2 renson-endura-delta==1.7.2
@@ -3185,7 +3185,7 @@ youless-api==2.2.0
youtubeaio==2.0.0 youtubeaio==2.0.0
# homeassistant.components.media_extractor # homeassistant.components.media_extractor
yt-dlp[default]==2025.07.21 yt-dlp[default]==2025.08.11
# homeassistant.components.zabbix # homeassistant.components.zabbix
zabbix-utils==2.0.2 zabbix-utils==2.0.2
@@ -3194,7 +3194,7 @@ zabbix-utils==2.0.2
zamg==0.3.6 zamg==0.3.6
# homeassistant.components.zimi # homeassistant.components.zimi
zcc-helper==3.5.2 zcc-helper==3.6
# homeassistant.components.zeroconf # homeassistant.components.zeroconf
zeroconf==0.147.0 zeroconf==0.147.0

View File

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

View File

@@ -247,6 +247,9 @@ num2words==0.5.14
# downgraded or upgraded by custom components # downgraded or upgraded by custom components
# This ensures all use the same version # This ensures all use the same version
pymodbus==3.9.2 pymodbus==3.9.2
# Some packages don't support gql 4.0.0 yet
gql<4.0.0
""" """
GENERATED_MESSAGE = ( 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 # - reasonX should be the name of the invalid dependency
"adax": {"adax": {"async-timeout"}, "adax-local": {"async-timeout"}}, "adax": {"adax": {"async-timeout"}, "adax-local": {"async-timeout"}},
"airthings": {"airthings-cloud": {"async-timeout"}}, "airthings": {"airthings-cloud": {"async-timeout"}},
"alexa_devices": {"marisa-trie": {"setuptools"}},
"ampio": {"asmog": {"async-timeout"}}, "ampio": {"asmog": {"async-timeout"}},
"apache_kafka": {"aiokafka": {"async-timeout"}}, "apache_kafka": {"aiokafka": {"async-timeout"}},
"apple_tv": {"pyatv": {"async-timeout"}}, "apple_tv": {"pyatv": {"async-timeout"}},

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,12 +4,14 @@ from unittest.mock import AsyncMock
from syrupy.assertion import SnapshotAssertion 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.core import HomeAssistant
from homeassistant.helpers import device_registry as dr from homeassistant.helpers import device_registry as dr
from . import setup_integration 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 from tests.common import MockConfigEntry
@@ -28,3 +30,32 @@ async def test_device_info(
) )
assert device_entry is not None assert device_entry is not None
assert device_entry == snapshot 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 from dateutil import parser
import pytest 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.components.amberelectric.coordinator import AmberUpdateCoordinator
from homeassistant.const import CONF_API_TOKEN from homeassistant.const import CONF_API_TOKEN
from homeassistant.core import HomeAssistant 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() result = await data_service._async_update_data()
current_price_api.get_current_prices.assert_called_with( 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 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() await data_service._async_update_data()
current_price_api.get_current_prices.assert_called_with( 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() result = await data_service._async_update_data()
current_price_api.get_current_prices.assert_called_with( 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 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() result = await data_service._async_update_data()
current_price_api.get_current_prices.assert_called_with( 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 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() result = await data_service._async_update_data()
current_price_api.get_current_prices.assert_called_with( 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 assert result["current"].get("general") == GENERAL_CHANNEL[0].actual_instance

View File

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

View File

@@ -133,3 +133,22 @@ async def test_valve(
command=clusters.ValveConfigurationAndControl.Commands.Open(targetLevel=100), command=clusters.ValveConfigurationAndControl.Commands.Open(targetLevel=100),
) )
matter_client.send_device_command.reset_mock() 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 import config_entries
from homeassistant.components.nanoleaf.const import DOMAIN from homeassistant.components.nanoleaf.const import DOMAIN
from homeassistant.config_entries import SOURCE_USER
from homeassistant.const import CONF_HOST, CONF_TOKEN from homeassistant.const import CONF_HOST, CONF_TOKEN
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType 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 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", "item_value",
], ],
"type": "object", "type": "object",
"additionalProperties": False,
"strict": True,
}, },
"type": "array", "type": "array",
}, },

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -232,6 +232,62 @@
'state': 'connected', '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] # name: test_sensor[ex30_2024][sensor.volvo_ex30_charging_power_status-entry]
EntityRegistryEntrySnapshot({ EntityRegistryEntrySnapshot({
'aliases': set({ 'aliases': set({
@@ -2164,7 +2220,7 @@
'last_changed': <ANY>, 'last_changed': <ANY>,
'last_reported': <ANY>, 'last_reported': <ANY>,
'last_updated': <ANY>, 'last_updated': <ANY>,
'state': '0', 'state': '1386',
}) })
# --- # ---
# name: test_sensor[xc40_electric_2024][sensor.volvo_xc40_charging_power_status-entry] # name: test_sensor[xc40_electric_2024][sensor.volvo_xc40_charging_power_status-entry]
@@ -3601,6 +3657,58 @@
'state': '30000', '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] # name: test_sensor[xc60_phev_2020][sensor.volvo_xc60_time_to_engine_service-entry]
EntityRegistryEntrySnapshot({ EntityRegistryEntrySnapshot({
'aliases': set({ 'aliases': set({

View File

@@ -68,4 +68,20 @@ async def test_skip_invalid_api_fields(
with patch("homeassistant.components.volvo.PLATFORMS", [Platform.SENSOR]): with patch("homeassistant.components.volvo.PLATFORMS", [Platform.SENSOR]):
assert await setup_integration() 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 # serializer version: 1
# name: test_diagnostics_cloudhook_instance # name: test_diagnostics_cloudhook_instance
dict({ 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_cloudhooks': True,
'has_valid_external_webhook_url': True, 'has_valid_external_webhook_url': True,
'received_activity_data': False, 'received_activity_data': False,
@@ -64,6 +76,18 @@
# --- # ---
# name: test_diagnostics_polling_instance # name: test_diagnostics_polling_instance
dict({ 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_cloudhooks': False,
'has_valid_external_webhook_url': False, 'has_valid_external_webhook_url': False,
'received_activity_data': False, 'received_activity_data': False,
@@ -127,6 +151,18 @@
# --- # ---
# name: test_diagnostics_webhook_instance # name: test_diagnostics_webhook_instance
dict({ 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_cloudhooks': False,
'has_valid_external_webhook_url': True, 'has_valid_external_webhook_url': True,
'received_activity_data': False, '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 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: async def test_manual(hass: HomeAssistant) -> None:
"""Test we create an entry with manual step.""" """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.mock_calls) == 1
assert len(mock_setup_entry.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" 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_ENTITY_MAP = {
"energy_production_power": { "energy_production_power": {
"state": 1.23, "state": 1.23,