mirror of
https://github.com/home-assistant/core.git
synced 2026-09-05 02:25:07 -05:00
2025.10.4 (#155109)
This commit is contained in:
@@ -37,7 +37,7 @@ on:
|
||||
type: boolean
|
||||
|
||||
env:
|
||||
CACHE_VERSION: 8
|
||||
CACHE_VERSION: 1
|
||||
UV_CACHE_VERSION: 1
|
||||
MYPY_CACHE_VERSION: 1
|
||||
HA_SHORT_VERSION: "2025.10"
|
||||
|
||||
@@ -34,6 +34,9 @@ INPUT_FIELD_CODE = "code"
|
||||
|
||||
DUMMY_SECRET = "FPPTH34D4E3MI2HG"
|
||||
|
||||
GOOGLE_AUTHENTICATOR_URL = "https://support.google.com/accounts/answer/1066447"
|
||||
AUTHY_URL = "https://authy.com/"
|
||||
|
||||
|
||||
def _generate_qr_code(data: str) -> str:
|
||||
"""Generate a base64 PNG string represent QR Code image of data."""
|
||||
@@ -229,6 +232,8 @@ class TotpSetupFlow(SetupFlow[TotpAuthModule]):
|
||||
"code": self._ota_secret,
|
||||
"url": self._url,
|
||||
"qr_code": self._image,
|
||||
"google_authenticator_url": GOOGLE_AUTHENTICATOR_URL,
|
||||
"authy_url": AUTHY_URL,
|
||||
},
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
@@ -26,6 +26,10 @@ from .const import DOMAIN
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# Documentation URL for API key generation
|
||||
_API_KEY_URL = "https://docs.airnowapi.org/account/request/"
|
||||
|
||||
|
||||
async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> bool:
|
||||
"""Validate the user input allows us to connect.
|
||||
|
||||
@@ -114,6 +118,7 @@ class AirNowConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
),
|
||||
}
|
||||
),
|
||||
description_placeholders={"api_key_url": _API_KEY_URL},
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"description": "To generate API key go to https://docs.airnowapi.org/account/request/",
|
||||
"description": "To generate API key go to {api_key_url}",
|
||||
"data": {
|
||||
"api_key": "[%key:common::config_flow::data::api_key%]",
|
||||
"latitude": "[%key:common::config_flow::data::latitude%]",
|
||||
|
||||
@@ -11,5 +11,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/airzone",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["aioairzone"],
|
||||
"requirements": ["aioairzone==1.0.1"]
|
||||
"requirements": ["aioairzone==1.0.2"]
|
||||
}
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aioamazondevices"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["aioamazondevices==6.4.4"]
|
||||
"requirements": ["aioamazondevices==6.4.6"]
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"step": {
|
||||
"init": {
|
||||
"title": "Set up two-factor authentication using TOTP",
|
||||
"description": "To activate two-factor authentication using time-based one-time passwords, scan the QR code with your authentication app. If you don't have one, we recommend either [Google Authenticator](https://support.google.com/accounts/answer/1066447) or [Authy](https://authy.com/).\n\n{qr_code}\n\nAfter scanning the code, enter the six-digit code from your app to verify the setup. If you have problems scanning the QR code, do a manual setup with code **`{code}`**."
|
||||
"description": "To activate two-factor authentication using time-based one-time passwords, scan the QR code with your authentication app. If you don't have one, we recommend either [Google Authenticator]({google_authenticator_url}) or [Authy]({authy_url}).\n\n{qr_code}\n\nAfter scanning the code, enter the six-digit code from your app to verify the setup. If you have problems scanning the QR code, do a manual setup with code **`{code}`**."
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["bring_api"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["bring-api==1.1.0"]
|
||||
"requirements": ["bring-api==1.1.1"]
|
||||
}
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_push",
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["pycync==0.4.1"]
|
||||
"requirements": ["pycync==0.4.2"]
|
||||
}
|
||||
|
||||
@@ -6,6 +6,6 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/droplet",
|
||||
"iot_class": "local_push",
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["pydroplet==2.3.3"],
|
||||
"requirements": ["pydroplet==2.3.4"],
|
||||
"zeroconf": ["_droplet._tcp.local."]
|
||||
}
|
||||
|
||||
@@ -111,7 +111,12 @@ class FlumeConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
errors[CONF_PASSWORD] = "invalid_auth"
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user", data_schema=DATA_SCHEMA, errors=errors
|
||||
step_id="user",
|
||||
data_schema=DATA_SCHEMA,
|
||||
errors=errors,
|
||||
description_placeholders={
|
||||
"api_url": "https://portal.flumetech.com/settings#token"
|
||||
},
|
||||
)
|
||||
|
||||
async def async_step_reauth(
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"description": "In order to access the Flume Personal API, you will need to request a 'Client ID' and 'Client Secret' at https://portal.flumetech.com/settings#token",
|
||||
"description": "In order to access the Flume Personal API, you will need to request a 'Client ID' and 'Client Secret' at {api_url}",
|
||||
"title": "Connect to your Flume account",
|
||||
"data": {
|
||||
"username": "[%key:common::config_flow::data::username%]",
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/holiday",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["holidays==0.82", "babel==2.15.0"]
|
||||
"requirements": ["holidays==0.83", "babel==2.15.0"]
|
||||
}
|
||||
|
||||
@@ -182,14 +182,6 @@ class AutomowerDataUpdateCoordinator(DataUpdateCoordinator[MowerDictionary]):
|
||||
"Failed to listen to websocket. Trying to reconnect: %s",
|
||||
err,
|
||||
)
|
||||
if not hass.is_stopping:
|
||||
await asyncio.sleep(self.reconnect_time)
|
||||
self.reconnect_time = min(self.reconnect_time * 2, MAX_WS_RECONNECT_TIME)
|
||||
entry.async_create_background_task(
|
||||
hass,
|
||||
self.client_listen(hass, entry, automower_client),
|
||||
"reconnect_task",
|
||||
)
|
||||
|
||||
def _should_poll(self) -> bool:
|
||||
"""Return True if at least one mower is connected and at least one is not OFF."""
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["aioautomower"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["aioautomower==2.2.1"]
|
||||
"requirements": ["aioautomower==2.3.1"]
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import CONFIG_DEFAULT_MAX_TEMP, CONFIG_DEFAULT_MIN_TEMP
|
||||
from .coordinator import HuumConfigEntry, HuumDataUpdateCoordinator
|
||||
from .entity import HuumBaseEntity
|
||||
|
||||
@@ -55,12 +56,12 @@ class HuumDevice(HuumBaseEntity, ClimateEntity):
|
||||
@property
|
||||
def min_temp(self) -> int:
|
||||
"""Return configured minimal temperature."""
|
||||
return self.coordinator.data.sauna_config.min_temp
|
||||
return self.coordinator.data.sauna_config.min_temp or CONFIG_DEFAULT_MIN_TEMP
|
||||
|
||||
@property
|
||||
def max_temp(self) -> int:
|
||||
"""Return configured maximum temperature."""
|
||||
return self.coordinator.data.sauna_config.max_temp
|
||||
return self.coordinator.data.sauna_config.max_temp or CONFIG_DEFAULT_MAX_TEMP
|
||||
|
||||
@property
|
||||
def hvac_mode(self) -> HVACMode:
|
||||
|
||||
@@ -9,3 +9,6 @@ PLATFORMS = [Platform.BINARY_SENSOR, Platform.CLIMATE, Platform.LIGHT, Platform.
|
||||
CONFIG_STEAMER = 1
|
||||
CONFIG_LIGHT = 2
|
||||
CONFIG_STEAMER_AND_LIGHT = 3
|
||||
|
||||
CONFIG_DEFAULT_MIN_TEMP = 40
|
||||
CONFIG_DEFAULT_MAX_TEMP = 110
|
||||
|
||||
@@ -48,6 +48,8 @@ from homeassistant.util.network import is_link_local
|
||||
|
||||
from .const import DOMAIN, LOGGER
|
||||
|
||||
DEVICES_URL = "https://developer.lametric.com/user/devices"
|
||||
|
||||
|
||||
class LaMetricFlowHandler(AbstractOAuth2FlowHandler, domain=DOMAIN):
|
||||
"""Handle a LaMetric config flow."""
|
||||
@@ -164,6 +166,9 @@ class LaMetricFlowHandler(AbstractOAuth2FlowHandler, domain=DOMAIN):
|
||||
return self.async_show_form(
|
||||
step_id="manual_entry",
|
||||
data_schema=vol.Schema(schema),
|
||||
description_placeholders={
|
||||
"devices_url": DEVICES_URL,
|
||||
},
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The IP address or hostname of your LaMetric TIME on your network.",
|
||||
"api_key": "You can find this API key in the [devices page in your LaMetric developer account](https://developer.lametric.com/user/devices)."
|
||||
"api_key": "You can find this API key in the [devices page in your LaMetric developer account]({devices_url})."
|
||||
}
|
||||
},
|
||||
"cloud_select_device": {
|
||||
|
||||
@@ -18,6 +18,7 @@ from homeassistant.const import (
|
||||
CONF_SOURCE,
|
||||
CONF_UNIT_OF_MEASUREMENT,
|
||||
LIGHT_LUX,
|
||||
PERCENTAGE,
|
||||
UnitOfElectricCurrent,
|
||||
UnitOfElectricPotential,
|
||||
UnitOfSpeed,
|
||||
@@ -50,6 +51,7 @@ DEVICE_CLASS_MAPPING = {
|
||||
pypck.lcn_defs.VarUnit.VOLT: SensorDeviceClass.VOLTAGE,
|
||||
pypck.lcn_defs.VarUnit.AMPERE: SensorDeviceClass.CURRENT,
|
||||
pypck.lcn_defs.VarUnit.PPM: SensorDeviceClass.CO2,
|
||||
pypck.lcn_defs.VarUnit.PERCENT: SensorDeviceClass.HUMIDITY,
|
||||
}
|
||||
|
||||
UNIT_OF_MEASUREMENT_MAPPING = {
|
||||
@@ -62,6 +64,7 @@ UNIT_OF_MEASUREMENT_MAPPING = {
|
||||
pypck.lcn_defs.VarUnit.VOLT: UnitOfElectricPotential.VOLT,
|
||||
pypck.lcn_defs.VarUnit.AMPERE: UnitOfElectricCurrent.AMPERE,
|
||||
pypck.lcn_defs.VarUnit.PPM: CONCENTRATION_PARTS_PER_MILLION,
|
||||
pypck.lcn_defs.VarUnit.PERCENT: PERCENTAGE,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -202,5 +202,10 @@ class MotionBlindsFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="connect", data_schema=self._config_settings, errors=errors
|
||||
step_id="connect",
|
||||
data_schema=self._config_settings,
|
||||
errors=errors,
|
||||
description_placeholders={
|
||||
"documentation_url": "https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-api-key",
|
||||
},
|
||||
)
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
}
|
||||
},
|
||||
"connect": {
|
||||
"description": "You will need the 16 character API key, see https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-api-key for instructions",
|
||||
"description": "You will need the 16 character API key, see {documentation_url} for instructions",
|
||||
"data": {
|
||||
"api_key": "[%key:common::config_flow::data::api_key%]"
|
||||
}
|
||||
|
||||
@@ -63,7 +63,12 @@ class NightscoutConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
return self.async_create_entry(title=info["title"], data=user_input)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user", data_schema=DATA_SCHEMA, errors=errors
|
||||
step_id="user",
|
||||
data_schema=DATA_SCHEMA,
|
||||
errors=errors,
|
||||
description_placeholders={
|
||||
"example_url": "https://myhomeassistant.duckdns.org:5423",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -3,10 +3,13 @@
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Enter your Nightscout server information.",
|
||||
"description": "- URL: the address of your nightscout instance. I.e.: https://myhomeassistant.duckdns.org:5423\n- API Key (optional): Only use if your instance is protected (auth_default_roles != readable).",
|
||||
"data": {
|
||||
"url": "[%key:common::config_flow::data::url%]",
|
||||
"api_key": "[%key:common::config_flow::data::api_key%]"
|
||||
},
|
||||
"data_description": {
|
||||
"url": "The address of your Nightscout instance. For example: {example_url}.",
|
||||
"api_key": "Optional; only use it if your instance is protected (auth_default_roles != readable)."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -110,6 +110,8 @@ async def _create_webhook(
|
||||
translation_placeholders={
|
||||
"base_url": hass_url,
|
||||
"network_link": "https://my.home-assistant.io/redirect/network/",
|
||||
"sample_ip": "192.168.1.10",
|
||||
"sample_url": "http://192.168.1.10:8123",
|
||||
},
|
||||
)
|
||||
else:
|
||||
|
||||
@@ -177,4 +177,5 @@ class NukiConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
step_id="user",
|
||||
data_schema=self.add_suggested_values_to_schema(data_schema, user_input),
|
||||
errors=errors,
|
||||
description_placeholders={"sample_ip": "192.168.1.25"},
|
||||
)
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"encrypt_token": "Use an encrypted token for authentication."
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The hostname or IP address of your Nuki bridge. For example: 192.168.1.25."
|
||||
"host": "The hostname or IP address of your Nuki bridge. For example: {sample_ip}."
|
||||
}
|
||||
},
|
||||
"reauth_confirm": {
|
||||
@@ -34,7 +34,7 @@
|
||||
"issues": {
|
||||
"https_webhook": {
|
||||
"title": "Nuki webhook URL uses HTTPS (SSL)",
|
||||
"description": "The Nuki bridge can not push events to an HTTPS address (SSL), please configure a (local) HTTP address under \"Home Assistant URL\" in the [network settings]({network_link}). The current (local) address is: `{base_url}`, a valid address could, for example, be `http://192.168.1.10:8123` where `192.168.1.10` is the IP of the Home Assistant device"
|
||||
"description": "The Nuki bridge cannot push events to an HTTPS address (SSL), please configure a (local) HTTP address under \"Home Assistant URL\" in the [network settings]({network_link}). The current (local) address is: `{base_url}`, a valid address could, for example, be `{sample_url}` where `{sample_ip}` is the IP of the Home Assistant device"
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["opower"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["opower==0.15.7"]
|
||||
"requirements": ["opower==0.15.8"]
|
||||
}
|
||||
|
||||
@@ -16,6 +16,9 @@ from .const import DOMAIN
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
_SCHEMA_STEP_USER = vol.Schema({vol.Required(CONF_API_KEY): str})
|
||||
|
||||
CONF_PORTAL_URL = "portal_url"
|
||||
OSOENERGY_PORTAL_URL = "https://portal.osoenergy.no/"
|
||||
|
||||
|
||||
class OSOEnergyFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a OSO Energy config flow."""
|
||||
@@ -45,6 +48,7 @@ class OSOEnergyFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
step_id="user",
|
||||
data_schema=_SCHEMA_STEP_USER,
|
||||
errors=errors,
|
||||
description_placeholders={CONF_PORTAL_URL: OSOENERGY_PORTAL_URL},
|
||||
)
|
||||
|
||||
async def get_user_email(self, subscription_key: str) -> str | None:
|
||||
@@ -66,4 +70,5 @@ class OSOEnergyFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
data_schema=self.add_suggested_values_to_schema(
|
||||
_SCHEMA_STEP_USER, self._get_reauth_entry().data
|
||||
),
|
||||
description_placeholders={CONF_PORTAL_URL: OSOENERGY_PORTAL_URL},
|
||||
)
|
||||
|
||||
@@ -3,14 +3,14 @@
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "OSO Energy auth",
|
||||
"description": "Enter the 'Subscription key' for your account generated at 'https://portal.osoenergy.no/'",
|
||||
"description": "Enter the 'Subscription key' for your account generated at {portal_url}",
|
||||
"data": {
|
||||
"api_key": "[%key:common::config_flow::data::api_key%]"
|
||||
}
|
||||
},
|
||||
"reauth": {
|
||||
"title": "OSO Energy auth",
|
||||
"description": "Enter a new 'Subscription key' for your account generated at 'https://portal.osoenergy.no/'.",
|
||||
"description": "Enter a new 'Subscription key' for your account generated at {portal_url}",
|
||||
"data": {
|
||||
"api_key": "[%key:common::config_flow::data::api_key%]"
|
||||
}
|
||||
|
||||
@@ -171,9 +171,27 @@ class OverkizConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
except TooManyAttemptsBannedException:
|
||||
errors["base"] = "too_many_attempts"
|
||||
except UnknownUserException:
|
||||
# If the user has no supported CozyTouch devices on
|
||||
# the Overkiz API server. Login will return unknown user.
|
||||
if user_input[CONF_HUB] in {
|
||||
Server.ATLANTIC_COZYTOUCH,
|
||||
Server.SAUTER_COZYTOUCH,
|
||||
Server.THERMOR_COZYTOUCH,
|
||||
}:
|
||||
description_placeholders["unsupported_device"] = "CozyTouch"
|
||||
# Somfy Protect accounts are not supported since they don't use
|
||||
# the Overkiz API server. Login will return unknown user.
|
||||
description_placeholders["unsupported_device"] = "Somfy Protect"
|
||||
elif user_input[CONF_HUB] in {
|
||||
Server.SOMFY_AMERICA,
|
||||
Server.SOMFY_DEVELOPER_MODE,
|
||||
Server.SOMFY_EUROPE,
|
||||
Server.SOMFY_OCEANIA,
|
||||
}:
|
||||
description_placeholders["unsupported_device"] = "Somfy Protect"
|
||||
# Fallback for other unknown devices
|
||||
else:
|
||||
description_placeholders["unsupported_device"] = "Unknown"
|
||||
|
||||
errors["base"] = "unsupported_hardware"
|
||||
except Exception: # noqa: BLE001
|
||||
errors["base"] = "unknown"
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
"too_many_attempts": "Too many attempts with an invalid token, temporarily banned",
|
||||
"too_many_requests": "Too many requests, try again later",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]",
|
||||
"unsupported_hardware": "Your {unsupported_device} hardware is not supported by this integration."
|
||||
"unsupported_hardware": "Your {unsupported_device} hardware is not using the Overkiz platform and can't be supported by this integration."
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
|
||||
|
||||
@@ -8,11 +8,15 @@ import logging
|
||||
from pyprobeplus import ProbePlusDevice
|
||||
from pyprobeplus.exceptions import ProbePlusDeviceNotFound, ProbePlusError
|
||||
|
||||
from homeassistant.components import bluetooth
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_ADDRESS
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
type ProbePlusConfigEntry = ConfigEntry[ProbePlusDataUpdateCoordinator]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -39,8 +43,17 @@ class ProbePlusDataUpdateCoordinator(DataUpdateCoordinator[None]):
|
||||
config_entry=entry,
|
||||
)
|
||||
|
||||
available_scanners = bluetooth.async_scanner_count(hass, connectable=True)
|
||||
|
||||
if available_scanners == 0:
|
||||
raise ConfigEntryNotReady(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="no_bleak_scanner",
|
||||
)
|
||||
|
||||
self.device: ProbePlusDevice = ProbePlusDevice(
|
||||
address_or_ble_device=entry.data[CONF_ADDRESS],
|
||||
scanner=bluetooth.async_get_scanner(hass),
|
||||
name=entry.title,
|
||||
notify_callback=self.async_update_listeners,
|
||||
)
|
||||
|
||||
@@ -15,5 +15,5 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["pyprobeplus==1.1.0"]
|
||||
"requirements": ["pyprobeplus==1.1.2"]
|
||||
}
|
||||
|
||||
@@ -45,5 +45,10 @@
|
||||
"name": "Relay voltage"
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"no_bleak_scanner": {
|
||||
"message": "No compatible Bluetooth scanner found."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""Base entity for the Pterodactyl integration."""
|
||||
|
||||
from yarl import URL
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_URL
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
@@ -33,7 +35,9 @@ class PterodactylEntity(CoordinatorEntity[PterodactylCoordinator]):
|
||||
name=self.game_server_data.name,
|
||||
model=self.game_server_data.name,
|
||||
model_id=self.game_server_data.uuid,
|
||||
configuration_url=f"{config_entry.data[CONF_URL]}/server/{identifier}",
|
||||
configuration_url=str(
|
||||
URL(config_entry.data[CONF_URL]) / "server" / identifier
|
||||
),
|
||||
)
|
||||
|
||||
@property
|
||||
|
||||
@@ -91,7 +91,12 @@ class RachioConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
errors["base"] = "unknown"
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user", data_schema=DATA_SCHEMA, errors=errors
|
||||
step_id="user",
|
||||
data_schema=DATA_SCHEMA,
|
||||
errors=errors,
|
||||
description_placeholders={
|
||||
"api_key_url": "https://app.rach.io/",
|
||||
},
|
||||
)
|
||||
|
||||
async def async_step_homekit(
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Connect to your Rachio device",
|
||||
"description": "You will need the API key from https://app.rach.io/. Go to Settings, then select 'GET API KEY'.",
|
||||
"description": "You will need the API key from {api_key_url}. Go to Settings, then select 'GET API KEY'.",
|
||||
"data": {
|
||||
"api_key": "[%key:common::config_flow::data::api_key%]"
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ from .const import (
|
||||
|
||||
DEFAULT_OFF_DELAY = 2.0
|
||||
|
||||
CONNECT_TIMEOUT = 30.0
|
||||
CONNECT_TIMEOUT = 60.0
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -61,4 +61,7 @@ class SensorPushCloudConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
}
|
||||
),
|
||||
errors=errors,
|
||||
description_placeholders={
|
||||
"dashboard_url": "https://dashboard.sensorpush.com/",
|
||||
},
|
||||
)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"description": "To activate API access, log in to the [Gateway Cloud Dashboard](https://dashboard.sensorpush.com/) and agree to the terms of service. Devices are not available until activated with the SensorPush app on iOS or Android.",
|
||||
"description": "To activate API access, log in to the [Gateway Cloud Dashboard]({dashboard_url}) and agree to the terms of service. Devices are not available until activated with the SensorPush app on iOS or Android.",
|
||||
"data": {
|
||||
"email": "[%key:common::config_flow::data::email%]",
|
||||
"password": "[%key:common::config_flow::data::password%]"
|
||||
|
||||
@@ -1554,8 +1554,7 @@ RPC_SENSORS: Final = {
|
||||
"number_energy_charge": RpcSensorDescription(
|
||||
key="number",
|
||||
sub_key="value",
|
||||
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
||||
suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
suggested_display_precision=2,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
@@ -1599,8 +1598,7 @@ RPC_SENSORS: Final = {
|
||||
key="object",
|
||||
sub_key="value",
|
||||
value=lambda status, _: float(status["counter"]["total"]),
|
||||
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
||||
suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
suggested_display_precision=2,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
@@ -1611,8 +1609,7 @@ RPC_SENSORS: Final = {
|
||||
sub_key="value",
|
||||
name="Total Active Energy",
|
||||
value=lambda status, _: float(status["total_act_energy"]),
|
||||
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
||||
suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
suggested_display_precision=2,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
@@ -1623,8 +1620,7 @@ RPC_SENSORS: Final = {
|
||||
sub_key="value",
|
||||
name="Total Power",
|
||||
value=lambda status, _: float(status["total_power"]),
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
suggested_unit_of_measurement=UnitOfPower.KILO_WATT,
|
||||
native_unit_of_measurement=UnitOfPower.KILO_WATT,
|
||||
suggested_display_precision=2,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
|
||||
@@ -27,6 +27,10 @@ from homeassistant.helpers import aiohttp_client, config_validation as cv
|
||||
from .const import DOMAIN, LOGGER
|
||||
|
||||
CONF_AUTH_CODE = "auth_code"
|
||||
CONF_DOCUMENTATION_URL = "documentation_url"
|
||||
DOCUMENTATION_URL = (
|
||||
"https://home-assistant.io/integrations/simplisafe#getting-an-authorization-code"
|
||||
)
|
||||
|
||||
STEP_USER_SCHEMA = vol.Schema(
|
||||
{
|
||||
@@ -84,7 +88,10 @@ class SimpliSafeFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=STEP_USER_SCHEMA,
|
||||
description_placeholders={CONF_URL: self._oauth_values.auth_url},
|
||||
description_placeholders={
|
||||
CONF_URL: self._oauth_values.auth_url,
|
||||
CONF_DOCUMENTATION_URL: DOCUMENTATION_URL,
|
||||
},
|
||||
)
|
||||
|
||||
auth_code = user_input[CONF_AUTH_CODE]
|
||||
@@ -102,7 +109,10 @@ class SimpliSafeFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
step_id="user",
|
||||
data_schema=STEP_USER_SCHEMA,
|
||||
errors={CONF_AUTH_CODE: "invalid_auth_code_length"},
|
||||
description_placeholders={CONF_URL: self._oauth_values.auth_url},
|
||||
description_placeholders={
|
||||
CONF_URL: self._oauth_values.auth_url,
|
||||
CONF_DOCUMENTATION_URL: DOCUMENTATION_URL,
|
||||
},
|
||||
)
|
||||
|
||||
errors = {}
|
||||
@@ -124,7 +134,10 @@ class SimpliSafeFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
step_id="user",
|
||||
data_schema=STEP_USER_SCHEMA,
|
||||
errors=errors,
|
||||
description_placeholders={CONF_URL: self._oauth_values.auth_url},
|
||||
description_placeholders={
|
||||
CONF_URL: self._oauth_values.auth_url,
|
||||
CONF_DOCUMENTATION_URL: DOCUMENTATION_URL,
|
||||
},
|
||||
)
|
||||
|
||||
simplisafe_user_id = str(simplisafe.user_id)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"description": "SimpliSafe authenticates users via its web app. Due to technical limitations, there is a manual step at the end of this process; please ensure that you read the [documentation](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) before starting.\n\nWhen you are ready, click [here]({url}) to open the SimpliSafe web app and input your credentials. If you've already logged into SimpliSafe in your browser, you may want to open a new tab, then copy/paste the above URL into that tab.\n\nWhen the process is complete, return here and input the authorization code from the `com.simplisafe.mobile` URL.",
|
||||
"description": "SimpliSafe authenticates users via its web app. Due to technical limitations, there is a manual step at the end of this process; please ensure that you read the [documentation]({documentation_url}) before starting.\n\nWhen you are ready, click [here]({url}) to open the SimpliSafe web app and input your credentials. If you've already logged into SimpliSafe in your browser, you may want to open a new tab, then copy/paste the above URL into that tab.\n\nWhen the process is complete, return here and input the authorization code from the `com.simplisafe.mobile` URL.",
|
||||
"data": {
|
||||
"auth_code": "Authorization Code"
|
||||
}
|
||||
|
||||
@@ -117,6 +117,9 @@ class StarlineFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
}
|
||||
),
|
||||
errors=errors,
|
||||
description_placeholders={
|
||||
"developer_account_url": "https://my.starline.ru/developer",
|
||||
},
|
||||
)
|
||||
|
||||
@callback
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"step": {
|
||||
"auth_app": {
|
||||
"title": "Application credentials",
|
||||
"description": "Application ID and secret code from [StarLine developer account](https://my.starline.ru/developer)",
|
||||
"description": "Application ID and secret code from [StarLine developer account]({developer_account_url})",
|
||||
"data": {
|
||||
"app_id": "App ID",
|
||||
"app_secret": "Secret"
|
||||
|
||||
@@ -24,7 +24,7 @@ from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
|
||||
|
||||
from .const import DATA_WAIT_TIMEOUT, DOMAIN
|
||||
from .const import DATA_WAIT_TIMEOUT, DOMAIN, SYNTAX_KEYS_DOCUMENTATION_URL
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -132,7 +132,11 @@ class SystemBridgeConfigFlow(
|
||||
"""Handle the initial step."""
|
||||
if user_input is None:
|
||||
return self.async_show_form(
|
||||
step_id="user", data_schema=STEP_USER_DATA_SCHEMA
|
||||
step_id="user",
|
||||
data_schema=STEP_USER_DATA_SCHEMA,
|
||||
description_placeholders={
|
||||
"syntax_keys_documentation_url": SYNTAX_KEYS_DOCUMENTATION_URL
|
||||
},
|
||||
)
|
||||
|
||||
errors, info = await _async_get_info(self.hass, user_input)
|
||||
@@ -144,7 +148,12 @@ class SystemBridgeConfigFlow(
|
||||
return self.async_create_entry(title=info["hostname"], data=user_input)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
|
||||
step_id="user",
|
||||
data_schema=STEP_USER_DATA_SCHEMA,
|
||||
errors=errors,
|
||||
description_placeholders={
|
||||
"syntax_keys_documentation_url": SYNTAX_KEYS_DOCUMENTATION_URL
|
||||
},
|
||||
)
|
||||
|
||||
async def async_step_authenticate(
|
||||
@@ -174,7 +183,10 @@ class SystemBridgeConfigFlow(
|
||||
return self.async_show_form(
|
||||
step_id="authenticate",
|
||||
data_schema=STEP_AUTHENTICATE_DATA_SCHEMA,
|
||||
description_placeholders={"name": self._name},
|
||||
description_placeholders={
|
||||
"name": self._name,
|
||||
"syntax_keys_documentation_url": SYNTAX_KEYS_DOCUMENTATION_URL,
|
||||
},
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@ from typing import Final
|
||||
|
||||
from systembridgemodels.modules import Module
|
||||
|
||||
SYNTAX_KEYS_DOCUMENTATION_URL = "http://robotjs.io/docs/syntax#keys"
|
||||
|
||||
DOMAIN = "system_bridge"
|
||||
|
||||
MODULES: Final[list[Module]] = [
|
||||
|
||||
@@ -194,7 +194,7 @@
|
||||
},
|
||||
"key": {
|
||||
"name": "Key",
|
||||
"description": "Key to press. List available here: http://robotjs.io/docs/syntax#keys."
|
||||
"description": "Key to press. List available here: {syntax_keys_documentation_url}."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -134,7 +134,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: TeslaFleetConfigEntry) -
|
||||
api = tesla.vehicles.createSigned(vin)
|
||||
else:
|
||||
api = tesla.vehicles.createFleet(vin)
|
||||
coordinator = TeslaFleetVehicleDataCoordinator(hass, entry, api, product)
|
||||
coordinator = TeslaFleetVehicleDataCoordinator(
|
||||
hass, entry, api, product, Scope.VEHICLE_LOCATION in scopes
|
||||
)
|
||||
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
|
||||
@@ -39,9 +39,9 @@ ENDPOINTS = [
|
||||
VehicleDataEndpoint.CHARGE_STATE,
|
||||
VehicleDataEndpoint.CLIMATE_STATE,
|
||||
VehicleDataEndpoint.DRIVE_STATE,
|
||||
VehicleDataEndpoint.LOCATION_DATA,
|
||||
VehicleDataEndpoint.VEHICLE_STATE,
|
||||
VehicleDataEndpoint.VEHICLE_CONFIG,
|
||||
VehicleDataEndpoint.LOCATION_DATA,
|
||||
]
|
||||
|
||||
|
||||
@@ -65,6 +65,7 @@ class TeslaFleetVehicleDataCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
updated_once: bool
|
||||
pre2021: bool
|
||||
last_active: datetime
|
||||
endpoints: list[VehicleDataEndpoint]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -72,6 +73,7 @@ class TeslaFleetVehicleDataCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
config_entry: TeslaFleetConfigEntry,
|
||||
api: VehicleFleet,
|
||||
product: dict,
|
||||
location: bool,
|
||||
) -> None:
|
||||
"""Initialize TeslaFleet Vehicle Update Coordinator."""
|
||||
super().__init__(
|
||||
@@ -85,6 +87,11 @@ class TeslaFleetVehicleDataCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
self.data = flatten(product)
|
||||
self.updated_once = False
|
||||
self.last_active = datetime.now()
|
||||
self.endpoints = (
|
||||
ENDPOINTS
|
||||
if location
|
||||
else [ep for ep in ENDPOINTS if ep != VehicleDataEndpoint.LOCATION_DATA]
|
||||
)
|
||||
|
||||
async def _async_update_data(self) -> dict[str, Any]:
|
||||
"""Update vehicle data using TeslaFleet API."""
|
||||
@@ -97,7 +104,7 @@ class TeslaFleetVehicleDataCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
if self.data["state"] != TeslaFleetState.ONLINE:
|
||||
return self.data
|
||||
|
||||
response = await self.api.vehicle_data(endpoints=ENDPOINTS)
|
||||
response = await self.api.vehicle_data(endpoints=self.endpoints)
|
||||
data = response["response"]
|
||||
|
||||
except VehicleOffline:
|
||||
@@ -251,11 +258,14 @@ class TeslaFleetEnergySiteHistoryCoordinator(DataUpdateCoordinator[dict[str, Any
|
||||
raise UpdateFailed("Received invalid data")
|
||||
|
||||
# Add all time periods together
|
||||
output = dict.fromkeys(ENERGY_HISTORY_FIELDS, 0)
|
||||
output = dict.fromkeys(ENERGY_HISTORY_FIELDS, None)
|
||||
for period in data.get("time_series", []):
|
||||
for key in ENERGY_HISTORY_FIELDS:
|
||||
if key in period:
|
||||
output[key] += period[key]
|
||||
if output[key] is None:
|
||||
output[key] = period[key]
|
||||
else:
|
||||
output[key] += period[key]
|
||||
|
||||
return output
|
||||
|
||||
|
||||
@@ -199,10 +199,13 @@ class TeslemetryEnergyHistoryCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
raise UpdateFailed("Received invalid data")
|
||||
|
||||
# Add all time periods together
|
||||
output = dict.fromkeys(ENERGY_HISTORY_FIELDS, 0)
|
||||
for period in data["time_series"]:
|
||||
output = dict.fromkeys(ENERGY_HISTORY_FIELDS, None)
|
||||
for period in data.get("time_series", []):
|
||||
for key in ENERGY_HISTORY_FIELDS:
|
||||
if key in period:
|
||||
output[key] += period[key]
|
||||
if output[key] is None:
|
||||
output[key] = period[key]
|
||||
else:
|
||||
output[key] += period[key]
|
||||
|
||||
return output
|
||||
|
||||
@@ -82,7 +82,14 @@ class TTNFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
),
|
||||
user_input,
|
||||
)
|
||||
return self.async_show_form(step_id="user", data_schema=schema, errors=errors)
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=schema,
|
||||
errors=errors,
|
||||
description_placeholders={
|
||||
"instructions_url": "https://www.thethingsindustries.com/docs/integrations/adding-applications/"
|
||||
},
|
||||
)
|
||||
|
||||
async def async_step_reauth(
|
||||
self, entry_data: Mapping[str, Any]
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Connect to The Things Network v3",
|
||||
"description": "Enter the API hostname, application ID and API key to use with Home Assistant.\n\n[Read the instructions](https://www.thethingsindustries.com/docs/integrations/adding-applications/) on how to register your application and create an API key.",
|
||||
"description": "Enter the API hostname, application ID and API key to use with Home Assistant.\n\n[Read the instructions]({instructions_url}) on how to register your application and create an API key.",
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]",
|
||||
"app_id": "Application ID",
|
||||
|
||||
@@ -172,4 +172,7 @@ class TomorrowioConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
step_id="user",
|
||||
data_schema=_get_config_schema(self.hass, self.source, user_input),
|
||||
errors=errors,
|
||||
description_placeholders={
|
||||
"signup_link": "[Tomorrow.io](https://app.tomorrow.io/signup)"
|
||||
},
|
||||
)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"description": "To get an API key, sign up at [Tomorrow.io](https://app.tomorrow.io/signup).",
|
||||
"description": "To get an API key, sign up at {signup_link}.",
|
||||
"data": {
|
||||
"name": "[%key:common::config_flow::data::name%]",
|
||||
"api_key": "[%key:common::config_flow::data::api_key%]",
|
||||
|
||||
@@ -19,7 +19,7 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_API_KEY, CONF_URL, CONF_VERIFY_SSL
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
@@ -89,7 +89,8 @@ def async_migrate_entities_unique_ids(
|
||||
"""Migrate unique_ids in the entity registry after updating Uptime Kuma."""
|
||||
|
||||
if (
|
||||
coordinator.version is coordinator.api.version
|
||||
coordinator.version is None
|
||||
or coordinator.version.version == coordinator.api.version.version
|
||||
or int(coordinator.api.version.major) < 2
|
||||
):
|
||||
return
|
||||
@@ -116,6 +117,32 @@ def async_migrate_entities_unique_ids(
|
||||
new_unique_id=f"{registry_entry.config_entry_id}_{monitor.monitor_id!s}_{registry_entry.translation_key}",
|
||||
)
|
||||
|
||||
# migrate device identifiers and update version
|
||||
device_reg = dr.async_get(hass)
|
||||
for monitor in metrics.values():
|
||||
if device := device_reg.async_get_device(
|
||||
{(DOMAIN, f"{coordinator.config_entry.entry_id}_{monitor.monitor_name!s}")}
|
||||
):
|
||||
new_identifier = {
|
||||
(DOMAIN, f"{coordinator.config_entry.entry_id}_{monitor.monitor_id!s}")
|
||||
}
|
||||
device_reg.async_update_device(
|
||||
device.id,
|
||||
new_identifiers=new_identifier,
|
||||
sw_version=coordinator.api.version.version,
|
||||
)
|
||||
if device := device_reg.async_get_device(
|
||||
{(DOMAIN, f"{coordinator.config_entry.entry_id}_update")}
|
||||
):
|
||||
device_reg.async_update_device(
|
||||
device.id,
|
||||
sw_version=coordinator.api.version.version,
|
||||
)
|
||||
|
||||
hass.async_create_task(
|
||||
hass.config_entries.async_reload(coordinator.config_entry.entry_id)
|
||||
)
|
||||
|
||||
|
||||
class UptimeKumaSoftwareUpdateCoordinator(DataUpdateCoordinator[LatestRelease]):
|
||||
"""Uptime Kuma coordinator for retrieving update information."""
|
||||
|
||||
@@ -100,7 +100,10 @@ class VeSyncFanHA(VeSyncBaseEntity, FanEntity):
|
||||
"""Return the currently set speed."""
|
||||
|
||||
current_level = self.device.state.fan_level
|
||||
if self.device.state.mode == VS_FAN_MODE_MANUAL and current_level is not None:
|
||||
if (
|
||||
self.device.state.mode in (VS_FAN_MODE_MANUAL, VS_FAN_MODE_NORMAL)
|
||||
and current_level is not None
|
||||
):
|
||||
if current_level == 0:
|
||||
return 0
|
||||
return ordered_list_item_to_percentage(
|
||||
@@ -182,7 +185,7 @@ class VeSyncFanHA(VeSyncBaseEntity, FanEntity):
|
||||
)
|
||||
|
||||
# Switch to manual mode if not already set
|
||||
if self.device.state.mode != VS_FAN_MODE_MANUAL:
|
||||
if self.device.state.mode not in (VS_FAN_MODE_MANUAL, VS_FAN_MODE_NORMAL):
|
||||
if not await self.device.set_manual_mode():
|
||||
raise HomeAssistantError(
|
||||
"An error occurred while setting manual mode."
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["holidays"],
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["holidays==0.82"]
|
||||
"requirements": ["holidays==0.83"]
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ from aiomusiccast.features import ZoneFeature
|
||||
|
||||
from homeassistant.components import media_source
|
||||
from homeassistant.components.media_player import (
|
||||
BrowseError,
|
||||
BrowseMedia,
|
||||
MediaClass,
|
||||
MediaPlayerEntity,
|
||||
@@ -372,7 +373,7 @@ class MusicCastMediaPlayer(MusicCastDeviceEntity, MediaPlayerEntity):
|
||||
]
|
||||
|
||||
if add_media_source:
|
||||
with contextlib.suppress(media_source.BrowseError):
|
||||
with contextlib.suppress(BrowseError):
|
||||
item = await media_source.async_browse_media(
|
||||
self.hass,
|
||||
None,
|
||||
|
||||
@@ -26,7 +26,7 @@ from homeassistant.helpers import (
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from . import api
|
||||
from .const import ATTR_LORA_INFO, DOMAIN, YOLINK_EVENT
|
||||
from .const import ATTR_LORA_INFO, DOMAIN, SUPPORTED_REMOTERS, YOLINK_EVENT
|
||||
from .coordinator import YoLinkCoordinator
|
||||
from .device_trigger import CONF_LONG_PRESS, CONF_SHORT_PRESS
|
||||
from .services import async_setup_services
|
||||
@@ -151,6 +151,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
device_pairing_mapping[parent_id] = device.device_id
|
||||
|
||||
for device in yolink_home.get_devices():
|
||||
if (
|
||||
device.device_type == ATTR_DEVICE_SMART_REMOTER
|
||||
and device.device_model_name not in SUPPORTED_REMOTERS
|
||||
):
|
||||
continue
|
||||
paried_device: YoLinkDevice | None = None
|
||||
if (
|
||||
paried_device_id := device_pairing_mapping.get(device.device_id)
|
||||
|
||||
@@ -44,3 +44,10 @@ DEV_MODEL_LEAK_STOP_YS5009 = "YS5009"
|
||||
DEV_MODEL_LEAK_STOP_YS5029 = "YS5029"
|
||||
DEV_MODEL_WATER_METER_YS5018_EC = "YS5018-EC"
|
||||
DEV_MODEL_WATER_METER_YS5018_UC = "YS5018-UC"
|
||||
|
||||
SUPPORTED_REMOTERS = [
|
||||
DEV_MODEL_FLEX_FOB_YS3604_EC,
|
||||
DEV_MODEL_FLEX_FOB_YS3604_UC,
|
||||
DEV_MODEL_FLEX_FOB_YS3614_EC,
|
||||
DEV_MODEL_FLEX_FOB_YS3614_UC,
|
||||
]
|
||||
|
||||
@@ -26,7 +26,7 @@ if TYPE_CHECKING:
|
||||
APPLICATION_NAME: Final = "HomeAssistant"
|
||||
MAJOR_VERSION: Final = 2025
|
||||
MINOR_VERSION: Final = 10
|
||||
PATCH_VERSION: Final = "3"
|
||||
PATCH_VERSION: Final = "4"
|
||||
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
||||
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
|
||||
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 13, 2)
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "homeassistant"
|
||||
version = "2025.10.3"
|
||||
version = "2025.10.4"
|
||||
license = "Apache-2.0"
|
||||
license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"]
|
||||
description = "Open-source home automation platform running on Python 3."
|
||||
|
||||
Generated
+9
-9
@@ -182,10 +182,10 @@ aioairq==0.4.7
|
||||
aioairzone-cloud==0.7.2
|
||||
|
||||
# homeassistant.components.airzone
|
||||
aioairzone==1.0.1
|
||||
aioairzone==1.0.2
|
||||
|
||||
# homeassistant.components.alexa_devices
|
||||
aioamazondevices==6.4.4
|
||||
aioamazondevices==6.4.6
|
||||
|
||||
# homeassistant.components.ambient_network
|
||||
# homeassistant.components.ambient_station
|
||||
@@ -204,7 +204,7 @@ aioaseko==1.0.0
|
||||
aioasuswrt==1.5.1
|
||||
|
||||
# homeassistant.components.husqvarna_automower
|
||||
aioautomower==2.2.1
|
||||
aioautomower==2.3.1
|
||||
|
||||
# homeassistant.components.azure_devops
|
||||
aioazuredevops==2.2.2
|
||||
@@ -682,7 +682,7 @@ boto3==1.37.1
|
||||
botocore==1.37.1
|
||||
|
||||
# homeassistant.components.bring
|
||||
bring-api==1.1.0
|
||||
bring-api==1.1.1
|
||||
|
||||
# homeassistant.components.broadlink
|
||||
broadlink==0.19.0
|
||||
@@ -1183,7 +1183,7 @@ hole==0.9.0
|
||||
|
||||
# homeassistant.components.holiday
|
||||
# homeassistant.components.workday
|
||||
holidays==0.82
|
||||
holidays==0.83
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20251001.4
|
||||
@@ -1649,7 +1649,7 @@ openwrt-luci-rpc==1.1.17
|
||||
openwrt-ubus-rpc==0.0.2
|
||||
|
||||
# homeassistant.components.opower
|
||||
opower==0.15.7
|
||||
opower==0.15.8
|
||||
|
||||
# homeassistant.components.oralb
|
||||
oralb-ble==0.17.6
|
||||
@@ -1933,7 +1933,7 @@ pycsspeechtts==1.0.8
|
||||
# pycups==2.0.4
|
||||
|
||||
# homeassistant.components.cync
|
||||
pycync==0.4.1
|
||||
pycync==0.4.2
|
||||
|
||||
# homeassistant.components.daikin
|
||||
pydaikin==2.17.1
|
||||
@@ -1966,7 +1966,7 @@ pydrawise==2025.9.0
|
||||
pydroid-ipcam==3.0.0
|
||||
|
||||
# homeassistant.components.droplet
|
||||
pydroplet==2.3.3
|
||||
pydroplet==2.3.4
|
||||
|
||||
# homeassistant.components.ebox
|
||||
pyebox==1.1.4
|
||||
@@ -2290,7 +2290,7 @@ pypoint==3.0.0
|
||||
pyportainer==1.0.3
|
||||
|
||||
# homeassistant.components.probe_plus
|
||||
pyprobeplus==1.1.0
|
||||
pyprobeplus==1.1.2
|
||||
|
||||
# homeassistant.components.profiler
|
||||
pyprof2calltree==1.4.5
|
||||
|
||||
Generated
+9
-9
@@ -170,10 +170,10 @@ aioairq==0.4.7
|
||||
aioairzone-cloud==0.7.2
|
||||
|
||||
# homeassistant.components.airzone
|
||||
aioairzone==1.0.1
|
||||
aioairzone==1.0.2
|
||||
|
||||
# homeassistant.components.alexa_devices
|
||||
aioamazondevices==6.4.4
|
||||
aioamazondevices==6.4.6
|
||||
|
||||
# homeassistant.components.ambient_network
|
||||
# homeassistant.components.ambient_station
|
||||
@@ -192,7 +192,7 @@ aioaseko==1.0.0
|
||||
aioasuswrt==1.5.1
|
||||
|
||||
# homeassistant.components.husqvarna_automower
|
||||
aioautomower==2.2.1
|
||||
aioautomower==2.3.1
|
||||
|
||||
# homeassistant.components.azure_devops
|
||||
aioazuredevops==2.2.2
|
||||
@@ -609,7 +609,7 @@ boschshcpy==0.2.107
|
||||
botocore==1.37.1
|
||||
|
||||
# homeassistant.components.bring
|
||||
bring-api==1.1.0
|
||||
bring-api==1.1.1
|
||||
|
||||
# homeassistant.components.broadlink
|
||||
broadlink==0.19.0
|
||||
@@ -1032,7 +1032,7 @@ hole==0.9.0
|
||||
|
||||
# homeassistant.components.holiday
|
||||
# homeassistant.components.workday
|
||||
holidays==0.82
|
||||
holidays==0.83
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20251001.4
|
||||
@@ -1405,7 +1405,7 @@ openhomedevice==2.2.0
|
||||
openwebifpy==4.3.1
|
||||
|
||||
# homeassistant.components.opower
|
||||
opower==0.15.7
|
||||
opower==0.15.8
|
||||
|
||||
# homeassistant.components.oralb
|
||||
oralb-ble==0.17.6
|
||||
@@ -1623,7 +1623,7 @@ pycsspeechtts==1.0.8
|
||||
# pycups==2.0.4
|
||||
|
||||
# homeassistant.components.cync
|
||||
pycync==0.4.1
|
||||
pycync==0.4.2
|
||||
|
||||
# homeassistant.components.daikin
|
||||
pydaikin==2.17.1
|
||||
@@ -1647,7 +1647,7 @@ pydrawise==2025.9.0
|
||||
pydroid-ipcam==3.0.0
|
||||
|
||||
# homeassistant.components.droplet
|
||||
pydroplet==2.3.3
|
||||
pydroplet==2.3.4
|
||||
|
||||
# homeassistant.components.ecoforest
|
||||
pyecoforest==0.4.0
|
||||
@@ -1914,7 +1914,7 @@ pypoint==3.0.0
|
||||
pyportainer==1.0.3
|
||||
|
||||
# homeassistant.components.probe_plus
|
||||
pyprobeplus==1.1.0
|
||||
pyprobeplus==1.1.2
|
||||
|
||||
# homeassistant.components.profiler
|
||||
pyprof2calltree==1.4.5
|
||||
|
||||
@@ -237,11 +237,6 @@ FORBIDDEN_PACKAGE_EXCEPTIONS: dict[str, dict[str, set[str]]] = {
|
||||
# pyopnsense > pbr > setuptools
|
||||
"pbr": {"setuptools"}
|
||||
},
|
||||
"opower": {
|
||||
# https://github.com/arrow-py/arrow/issues/1169 (fixed not yet released)
|
||||
# opower > arrow > types-python-dateutil
|
||||
"arrow": {"types-python-dateutil"}
|
||||
},
|
||||
"pvpc_hourly_pricing": {"aiopvpc": {"async-timeout"}},
|
||||
"remote_rpi_gpio": {
|
||||
# https://github.com/waveform80/colorzero/issues/9
|
||||
@@ -322,8 +317,6 @@ FORBIDDEN_PACKAGE_FILES_EXCEPTIONS = {
|
||||
"lyric": {"homeassistant": {"aiolyric"}},
|
||||
# https://github.com/microBeesTech/pythonSDK/
|
||||
"microbees": {"homeassistant": {"microbeespy"}},
|
||||
# https://github.com/tiagocoutinho/async_modbus
|
||||
"nibe_heatpump": {"nibe": {"async-modbus"}},
|
||||
# https://github.com/ejpenney/pyobihai
|
||||
"obihai": {"homeassistant": {"pyobihai"}},
|
||||
# https://github.com/iamkubi/pydactyl
|
||||
|
||||
@@ -25,6 +25,9 @@ async def test_form(
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {}
|
||||
assert result["description_placeholders"] == {
|
||||
"api_key_url": "https://docs.airnowapi.org/account/request/"
|
||||
}
|
||||
|
||||
result2 = await hass.config_entries.flow.async_configure(result["flow_id"], config)
|
||||
assert result2["type"] is FlowResultType.CREATE_ENTRY
|
||||
|
||||
@@ -328,65 +328,64 @@
|
||||
'firmware': '3.31',
|
||||
'full-name': 'Airzone [1] System',
|
||||
'id': 1,
|
||||
'master-system-zone': '1:1',
|
||||
'master-zone': 1,
|
||||
'mode': 3,
|
||||
'model': 'C6',
|
||||
'modes': list([
|
||||
'masters': list([
|
||||
1,
|
||||
4,
|
||||
2,
|
||||
3,
|
||||
5,
|
||||
]),
|
||||
'masters-slaves': dict({
|
||||
'1': list([
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
]),
|
||||
}),
|
||||
'model': 'C6',
|
||||
'problems': False,
|
||||
'q-adapt': 0,
|
||||
'slaves': list([
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
]),
|
||||
}),
|
||||
'2': dict({
|
||||
'available': True,
|
||||
'full-name': 'Airzone [2] System',
|
||||
'id': 2,
|
||||
'master-system-zone': '2:1',
|
||||
'master-zone': 1,
|
||||
'mode': 7,
|
||||
'modes': list([
|
||||
7,
|
||||
'masters': list([
|
||||
1,
|
||||
]),
|
||||
'masters-slaves': dict({
|
||||
'1': list([
|
||||
]),
|
||||
}),
|
||||
'problems': False,
|
||||
}),
|
||||
'3': dict({
|
||||
'available': True,
|
||||
'full-name': 'Airzone [3] System',
|
||||
'id': 3,
|
||||
'master-system-zone': '3:1',
|
||||
'master-zone': 1,
|
||||
'mode': 7,
|
||||
'modes': list([
|
||||
4,
|
||||
2,
|
||||
3,
|
||||
5,
|
||||
7,
|
||||
'masters': list([
|
||||
1,
|
||||
]),
|
||||
'masters-slaves': dict({
|
||||
'1': list([
|
||||
]),
|
||||
}),
|
||||
'problems': False,
|
||||
}),
|
||||
'4': dict({
|
||||
'available': True,
|
||||
'full-name': 'Airzone [4] System',
|
||||
'id': 4,
|
||||
'master-system-zone': '4:1',
|
||||
'master-zone': 1,
|
||||
'mode': 6,
|
||||
'modes': list([
|
||||
'masters': list([
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
6,
|
||||
]),
|
||||
'masters-slaves': dict({
|
||||
'1': list([
|
||||
]),
|
||||
}),
|
||||
'problems': False,
|
||||
}),
|
||||
}),
|
||||
|
||||
@@ -192,17 +192,11 @@ async def test_websocket_not_available(
|
||||
await hass.async_block_till_done()
|
||||
assert f"{error_msg} Trying to reconnect: Boom" in caplog.text
|
||||
|
||||
# Simulate a successful connection
|
||||
caplog.clear()
|
||||
await mock_called.wait()
|
||||
mock_called.clear()
|
||||
await hass.async_block_till_done()
|
||||
assert mock.call_count == 2
|
||||
assert "Trying to reconnect: Boom" not in caplog.text
|
||||
|
||||
# Simulate hass shutting down
|
||||
await hass.async_stop()
|
||||
assert mock.call_count == 2
|
||||
assert mock.call_count == 1
|
||||
|
||||
|
||||
async def test_device_info(
|
||||
|
||||
@@ -11,6 +11,10 @@ from homeassistant.components.climate import (
|
||||
SERVICE_SET_TEMPERATURE,
|
||||
HVACMode,
|
||||
)
|
||||
from homeassistant.components.huum.const import (
|
||||
CONFIG_DEFAULT_MAX_TEMP,
|
||||
CONFIG_DEFAULT_MIN_TEMP,
|
||||
)
|
||||
from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
@@ -76,3 +80,39 @@ async def test_set_temperature(
|
||||
)
|
||||
|
||||
mock_huum.turn_on.assert_called_once_with(60)
|
||||
|
||||
|
||||
async def test_temperature_range(
|
||||
hass: HomeAssistant,
|
||||
mock_huum: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test the temperature range."""
|
||||
await setup_with_selected_platforms(hass, mock_config_entry, [Platform.CLIMATE])
|
||||
|
||||
# API response.
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state.attributes["min_temp"] == 40
|
||||
assert state.attributes["max_temp"] == 110
|
||||
|
||||
# Empty/unconfigured API response should return default values.
|
||||
mock_huum.sauna_config.min_temp = 0
|
||||
mock_huum.sauna_config.max_temp = 0
|
||||
|
||||
await mock_config_entry.runtime_data.async_refresh()
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state.attributes["min_temp"] == CONFIG_DEFAULT_MIN_TEMP
|
||||
assert state.attributes["max_temp"] == CONFIG_DEFAULT_MAX_TEMP
|
||||
|
||||
# Custom configured API response.
|
||||
mock_huum.sauna_config.min_temp = 50
|
||||
mock_huum.sauna_config.max_temp = 80
|
||||
|
||||
await mock_config_entry.runtime_data.async_refresh()
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state.attributes["min_temp"] == 50
|
||||
assert state.attributes["max_temp"] == 80
|
||||
|
||||
@@ -255,6 +255,96 @@ async def test_form_invalid_auth_cloud(
|
||||
assert result["errors"] == {"base": error}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("side_effect", "description_placeholder", "server"),
|
||||
[
|
||||
(UnknownUserException, "CozyTouch", TEST_SERVER_COZYTOUCH),
|
||||
(UnknownUserException, "Unknown", TEST_SERVER2),
|
||||
],
|
||||
)
|
||||
async def test_form_invalid_hardware_cloud(
|
||||
hass: HomeAssistant,
|
||||
side_effect: Exception,
|
||||
description_placeholder: str,
|
||||
server: str,
|
||||
) -> None:
|
||||
"""Test we handle unsupported hardware (cloud)."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{"hub": server},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "cloud"
|
||||
|
||||
with patch("pyoverkiz.client.OverkizClient.login", side_effect=side_effect):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{"username": TEST_EMAIL, "password": TEST_PASSWORD},
|
||||
)
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {"base": "unsupported_hardware"}
|
||||
assert result["description_placeholders"] == {
|
||||
"unsupported_device": description_placeholder
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("side_effect", "description_placeholder", "server"),
|
||||
[
|
||||
(UnknownUserException, "Somfy Protect", TEST_SERVER),
|
||||
],
|
||||
)
|
||||
async def test_form_invalid_hardware_cloud_local(
|
||||
hass: HomeAssistant,
|
||||
side_effect: Exception,
|
||||
description_placeholder: str,
|
||||
server: str,
|
||||
) -> None:
|
||||
"""Test we handle unsupported hardware (cloud and local)."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{"hub": server},
|
||||
)
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{"api_type": "cloud"},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "cloud"
|
||||
|
||||
with patch("pyoverkiz.client.OverkizClient.login", side_effect=side_effect):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{"username": TEST_EMAIL, "password": TEST_PASSWORD},
|
||||
)
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {"base": "unsupported_hardware"}
|
||||
assert result["description_placeholders"] == {
|
||||
"unsupported_device": description_placeholder
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("side_effect", "error"),
|
||||
[
|
||||
|
||||
@@ -307,9 +307,6 @@
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 2,
|
||||
}),
|
||||
'sensor.private': dict({
|
||||
'suggested_unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.ENERGY: 'energy'>,
|
||||
'original_icon': None,
|
||||
|
||||
@@ -1714,7 +1714,7 @@ async def test_rpc_shelly_ev_sensors(
|
||||
}
|
||||
config["number:201"] = {
|
||||
"name": "Session energy",
|
||||
"meta": {"ui": {"unit": "Wh", "view": "label"}},
|
||||
"meta": {"ui": {"unit": "kWh", "view": "label"}},
|
||||
"role": "energy_charge",
|
||||
}
|
||||
config["number:202"] = {
|
||||
@@ -1726,7 +1726,7 @@ async def test_rpc_shelly_ev_sensors(
|
||||
|
||||
status = deepcopy(mock_rpc_device.status)
|
||||
status["number:200"] = {"value": "charger_charging"}
|
||||
status["number:201"] = {"value": 5000}
|
||||
status["number:201"] = {"value": 5.0}
|
||||
status["number:202"] = {"value": 60}
|
||||
monkeypatch.setattr(mock_rpc_device, "status", status)
|
||||
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '0.0',
|
||||
'state': 'unknown',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[sensor.energy_site_battery_discharged-entry]
|
||||
@@ -146,7 +146,7 @@
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '0.0',
|
||||
'state': 'unknown',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[sensor.energy_site_battery_exported-entry]
|
||||
@@ -1121,7 +1121,7 @@
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '0.0',
|
||||
'state': 'unknown',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[sensor.energy_site_grid_exported_from_battery-entry]
|
||||
@@ -1881,7 +1881,7 @@
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '0.0',
|
||||
'state': 'unknown',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[sensor.energy_site_load_power-entry]
|
||||
@@ -2178,7 +2178,7 @@
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '0.0',
|
||||
'state': 'unknown',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[sensor.energy_site_solar_power-entry]
|
||||
|
||||
@@ -9,6 +9,7 @@ from aiohttp.client_exceptions import ClientResponseError
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
from tesla_fleet_api.const import Scope, VehicleDataEndpoint
|
||||
from tesla_fleet_api.exceptions import (
|
||||
InvalidRegion,
|
||||
InvalidToken,
|
||||
@@ -36,6 +37,7 @@ from homeassistant.data_entry_flow import FlowResultType
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
|
||||
from . import setup_platform
|
||||
from .conftest import create_config_entry
|
||||
from .const import VEHICLE_ASLEEP, VEHICLE_DATA_ALT
|
||||
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed
|
||||
@@ -497,3 +499,65 @@ async def test_bad_implementation(
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "reauth_confirm"
|
||||
assert not result["errors"]
|
||||
|
||||
|
||||
async def test_vehicle_without_location_scope(
|
||||
hass: HomeAssistant,
|
||||
expires_at: int,
|
||||
mock_vehicle_data: AsyncMock,
|
||||
) -> None:
|
||||
"""Test vehicle setup without VEHICLE_LOCATION scope excludes location endpoint."""
|
||||
|
||||
# Create config entry without VEHICLE_LOCATION scope
|
||||
config_entry = create_config_entry(
|
||||
expires_at,
|
||||
[
|
||||
Scope.OPENID,
|
||||
Scope.OFFLINE_ACCESS,
|
||||
Scope.VEHICLE_DEVICE_DATA,
|
||||
# Deliberately exclude Scope.VEHICLE_LOCATION
|
||||
],
|
||||
)
|
||||
|
||||
await setup_platform(hass, config_entry)
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
# Verify that vehicle_data was called without LOCATION_DATA endpoint
|
||||
mock_vehicle_data.assert_called()
|
||||
call_args = mock_vehicle_data.call_args
|
||||
endpoints = call_args.kwargs.get("endpoints", [])
|
||||
|
||||
# Should not include LOCATION_DATA endpoint
|
||||
assert VehicleDataEndpoint.LOCATION_DATA not in endpoints
|
||||
|
||||
# Should include other endpoints
|
||||
assert VehicleDataEndpoint.CHARGE_STATE in endpoints
|
||||
assert VehicleDataEndpoint.CLIMATE_STATE in endpoints
|
||||
assert VehicleDataEndpoint.DRIVE_STATE in endpoints
|
||||
assert VehicleDataEndpoint.VEHICLE_STATE in endpoints
|
||||
assert VehicleDataEndpoint.VEHICLE_CONFIG in endpoints
|
||||
|
||||
|
||||
async def test_vehicle_with_location_scope(
|
||||
hass: HomeAssistant,
|
||||
normal_config_entry: MockConfigEntry,
|
||||
mock_vehicle_data: AsyncMock,
|
||||
) -> None:
|
||||
"""Test vehicle setup with VEHICLE_LOCATION scope includes location endpoint."""
|
||||
await setup_platform(hass, normal_config_entry)
|
||||
assert normal_config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
# Verify that vehicle_data was called with LOCATION_DATA endpoint
|
||||
mock_vehicle_data.assert_called()
|
||||
call_args = mock_vehicle_data.call_args
|
||||
endpoints = call_args.kwargs.get("endpoints", [])
|
||||
|
||||
# Should include LOCATION_DATA endpoint when scope is present
|
||||
assert VehicleDataEndpoint.LOCATION_DATA in endpoints
|
||||
|
||||
# Should include all other endpoints
|
||||
assert VehicleDataEndpoint.CHARGE_STATE in endpoints
|
||||
assert VehicleDataEndpoint.CLIMATE_STATE in endpoints
|
||||
assert VehicleDataEndpoint.DRIVE_STATE in endpoints
|
||||
assert VehicleDataEndpoint.VEHICLE_STATE in endpoints
|
||||
assert VehicleDataEndpoint.VEHICLE_CONFIG in endpoints
|
||||
|
||||
@@ -9,10 +9,11 @@ import pytest
|
||||
from pythonkuma import MonitorStatus, UptimeKumaMonitor, UptimeKumaVersion
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.uptime_kuma.const import DOMAIN
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform
|
||||
|
||||
@@ -53,6 +54,7 @@ async def test_migrate_unique_id(
|
||||
snapshot: SnapshotAssertion,
|
||||
entity_registry: er.EntityRegistry,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
) -> None:
|
||||
"""Snapshot test states of sensor platform."""
|
||||
mock_pythonkuma.metrics.return_value = {
|
||||
@@ -87,7 +89,7 @@ async def test_migrate_unique_id(
|
||||
)
|
||||
}
|
||||
mock_pythonkuma.version = UptimeKumaVersion(
|
||||
version="2.0.0-beta.3", major="2", minor="0", patch="0-beta.3"
|
||||
version="2.0.2", major="2", minor="0", patch="2"
|
||||
)
|
||||
freezer.tick(timedelta(seconds=30))
|
||||
async_fire_time_changed(hass)
|
||||
@@ -95,3 +97,10 @@ async def test_migrate_unique_id(
|
||||
|
||||
assert (entity := entity_registry.async_get("sensor.monitor_status"))
|
||||
assert entity.unique_id == "123456789_1_status"
|
||||
|
||||
assert (
|
||||
device := device_registry.async_get_device(
|
||||
identifiers={(DOMAIN, f"{entity.config_entry_id}_1")}
|
||||
)
|
||||
)
|
||||
assert device.sw_version == "2.0.2"
|
||||
|
||||
@@ -670,7 +670,7 @@
|
||||
'display_status': 'off',
|
||||
'friendly_name': 'SmartTowerFan',
|
||||
'mode': 'normal',
|
||||
'percentage': None,
|
||||
'percentage': 0,
|
||||
'percentage_step': 8.333333333333334,
|
||||
'preset_mode': 'normal',
|
||||
'preset_modes': list([
|
||||
|
||||
Reference in New Issue
Block a user