mirror of
https://github.com/home-assistant/core.git
synced 2026-05-05 12:24:48 +02:00
2025.10.2 (#154181)
This commit is contained in:
Generated
+2
-2
@@ -760,8 +760,8 @@ build.json @home-assistant/supervisor
|
||||
/homeassistant/components/intent/ @home-assistant/core @synesthesiam @arturpragacz
|
||||
/tests/components/intent/ @home-assistant/core @synesthesiam @arturpragacz
|
||||
/homeassistant/components/intesishome/ @jnimmo
|
||||
/homeassistant/components/iometer/ @MaestroOnICe
|
||||
/tests/components/iometer/ @MaestroOnICe
|
||||
/homeassistant/components/iometer/ @jukrebs
|
||||
/tests/components/iometer/ @jukrebs
|
||||
/homeassistant/components/ios/ @robbiet480
|
||||
/tests/components/ios/ @robbiet480
|
||||
/homeassistant/components/iotawatt/ @gtdiehl @jyavenard
|
||||
|
||||
@@ -71,4 +71,4 @@ POLLEN_CATEGORY_MAP = {
|
||||
}
|
||||
UPDATE_INTERVAL_OBSERVATION = timedelta(minutes=10)
|
||||
UPDATE_INTERVAL_DAILY_FORECAST = timedelta(hours=6)
|
||||
UPDATE_INTERVAL_HOURLY_FORECAST = timedelta(hours=30)
|
||||
UPDATE_INTERVAL_HOURLY_FORECAST = timedelta(minutes=30)
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
{
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"air_quality": {
|
||||
"default": "mdi:air-filter"
|
||||
},
|
||||
"cloud_ceiling": {
|
||||
"default": "mdi:weather-fog"
|
||||
},
|
||||
@@ -34,9 +37,6 @@
|
||||
"thunderstorm_probability_night": {
|
||||
"default": "mdi:weather-lightning"
|
||||
},
|
||||
"translation_key": {
|
||||
"default": "mdi:air-filter"
|
||||
},
|
||||
"tree_pollen": {
|
||||
"default": "mdi:tree-outline"
|
||||
},
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
"""Airgradient Update platform."""
|
||||
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from airgradient import AirGradientConnectionError
|
||||
from propcache.api import cached_property
|
||||
|
||||
from homeassistant.components.update import UpdateDeviceClass, UpdateEntity
|
||||
@@ -13,6 +15,7 @@ from .entity import AirGradientEntity
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
SCAN_INTERVAL = timedelta(hours=1)
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
@@ -31,6 +34,7 @@ class AirGradientUpdate(AirGradientEntity, UpdateEntity):
|
||||
"""Representation of Airgradient Update."""
|
||||
|
||||
_attr_device_class = UpdateDeviceClass.FIRMWARE
|
||||
_server_unreachable_logged = False
|
||||
|
||||
def __init__(self, coordinator: AirGradientCoordinator) -> None:
|
||||
"""Initialize the entity."""
|
||||
@@ -47,10 +51,27 @@ class AirGradientUpdate(AirGradientEntity, UpdateEntity):
|
||||
"""Return the installed version of the entity."""
|
||||
return self.coordinator.data.measures.firmware_version
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return if entity is available."""
|
||||
return super().available and self._attr_available
|
||||
|
||||
async def async_update(self) -> None:
|
||||
"""Update the entity."""
|
||||
self._attr_latest_version = (
|
||||
await self.coordinator.client.get_latest_firmware_version(
|
||||
self.coordinator.serial_number
|
||||
try:
|
||||
self._attr_latest_version = (
|
||||
await self.coordinator.client.get_latest_firmware_version(
|
||||
self.coordinator.serial_number
|
||||
)
|
||||
)
|
||||
)
|
||||
except AirGradientConnectionError:
|
||||
self._attr_latest_version = None
|
||||
self._attr_available = False
|
||||
if not self._server_unreachable_logged:
|
||||
_LOGGER.error(
|
||||
"Unable to connect to AirGradient server to check for updates"
|
||||
)
|
||||
self._server_unreachable_logged = True
|
||||
else:
|
||||
self._server_unreachable_logged = False
|
||||
self._attr_available = True
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/airos",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["airos==0.5.4"]
|
||||
"requirements": ["airos==0.5.5"]
|
||||
}
|
||||
|
||||
@@ -18,7 +18,9 @@ from homeassistant.components.binary_sensor import (
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
import homeassistant.helpers.entity_registry as er
|
||||
|
||||
from .const import _LOGGER, DOMAIN
|
||||
from .coordinator import AmazonConfigEntry
|
||||
from .entity import AmazonEntity
|
||||
from .utils import async_update_unique_id
|
||||
@@ -51,11 +53,47 @@ BINARY_SENSORS: Final = (
|
||||
),
|
||||
is_supported=lambda device, key: device.sensors.get(key) is not None,
|
||||
is_available_fn=lambda device, key: (
|
||||
device.online and device.sensors[key].error is False
|
||||
device.online
|
||||
and (sensor := device.sensors.get(key)) is not None
|
||||
and sensor.error is False
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
DEPRECATED_BINARY_SENSORS: Final = (
|
||||
AmazonBinarySensorEntityDescription(
|
||||
key="bluetooth",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
translation_key="bluetooth",
|
||||
is_on_fn=lambda device, key: False,
|
||||
),
|
||||
AmazonBinarySensorEntityDescription(
|
||||
key="babyCryDetectionState",
|
||||
translation_key="baby_cry_detection",
|
||||
is_on_fn=lambda device, key: False,
|
||||
),
|
||||
AmazonBinarySensorEntityDescription(
|
||||
key="beepingApplianceDetectionState",
|
||||
translation_key="beeping_appliance_detection",
|
||||
is_on_fn=lambda device, key: False,
|
||||
),
|
||||
AmazonBinarySensorEntityDescription(
|
||||
key="coughDetectionState",
|
||||
translation_key="cough_detection",
|
||||
is_on_fn=lambda device, key: False,
|
||||
),
|
||||
AmazonBinarySensorEntityDescription(
|
||||
key="dogBarkDetectionState",
|
||||
translation_key="dog_bark_detection",
|
||||
is_on_fn=lambda device, key: False,
|
||||
),
|
||||
AmazonBinarySensorEntityDescription(
|
||||
key="waterSoundsDetectionState",
|
||||
translation_key="water_sounds_detection",
|
||||
is_on_fn=lambda device, key: False,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
@@ -66,6 +104,8 @@ async def async_setup_entry(
|
||||
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
entity_registry = er.async_get(hass)
|
||||
|
||||
# Replace unique id for "detectionState" binary sensor
|
||||
await async_update_unique_id(
|
||||
hass,
|
||||
@@ -75,6 +115,16 @@ async def async_setup_entry(
|
||||
"detectionState",
|
||||
)
|
||||
|
||||
# Clean up deprecated sensors
|
||||
for sensor_desc in DEPRECATED_BINARY_SENSORS:
|
||||
for serial_num in coordinator.data:
|
||||
unique_id = f"{serial_num}-{sensor_desc.key}"
|
||||
if entity_id := entity_registry.async_get_entity_id(
|
||||
BINARY_SENSOR_DOMAIN, DOMAIN, unique_id
|
||||
):
|
||||
_LOGGER.debug("Removing deprecated entity %s", entity_id)
|
||||
entity_registry.async_remove(entity_id)
|
||||
|
||||
known_devices: set[str] = set()
|
||||
|
||||
def _check_device() -> None:
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aioamazondevices"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["aioamazondevices==6.2.7"]
|
||||
"requirements": ["aioamazondevices==6.4.0"]
|
||||
}
|
||||
|
||||
@@ -32,7 +32,9 @@ class AmazonSensorEntityDescription(SensorEntityDescription):
|
||||
|
||||
native_unit_of_measurement_fn: Callable[[AmazonDevice, str], str] | None = None
|
||||
is_available_fn: Callable[[AmazonDevice, str], bool] = lambda device, key: (
|
||||
device.online and device.sensors[key].error is False
|
||||
device.online
|
||||
and (sensor := device.sensors.get(key)) is not None
|
||||
and sensor.error is False
|
||||
)
|
||||
|
||||
|
||||
@@ -40,9 +42,9 @@ SENSORS: Final = (
|
||||
AmazonSensorEntityDescription(
|
||||
key="temperature",
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
native_unit_of_measurement_fn=lambda device, _key: (
|
||||
native_unit_of_measurement_fn=lambda device, key: (
|
||||
UnitOfTemperature.CELSIUS
|
||||
if device.sensors[_key].scale == "CELSIUS"
|
||||
if key in device.sensors and device.sensors[key].scale == "CELSIUS"
|
||||
else UnitOfTemperature.FAHRENHEIT
|
||||
),
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
|
||||
@@ -18,7 +18,11 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .coordinator import AmazonConfigEntry
|
||||
from .entity import AmazonEntity
|
||||
from .utils import alexa_api_call, async_update_unique_id
|
||||
from .utils import (
|
||||
alexa_api_call,
|
||||
async_remove_dnd_from_virtual_group,
|
||||
async_update_unique_id,
|
||||
)
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
@@ -29,7 +33,9 @@ class AmazonSwitchEntityDescription(SwitchEntityDescription):
|
||||
|
||||
is_on_fn: Callable[[AmazonDevice], bool]
|
||||
is_available_fn: Callable[[AmazonDevice, str], bool] = lambda device, key: (
|
||||
device.online and device.sensors[key].error is False
|
||||
device.online
|
||||
and (sensor := device.sensors.get(key)) is not None
|
||||
and sensor.error is False
|
||||
)
|
||||
method: str
|
||||
|
||||
@@ -58,6 +64,9 @@ async def async_setup_entry(
|
||||
hass, coordinator, SWITCH_DOMAIN, "do_not_disturb", "dnd"
|
||||
)
|
||||
|
||||
# Remove DND switch from virtual groups
|
||||
await async_remove_dnd_from_virtual_group(hass, coordinator)
|
||||
|
||||
known_devices: set[str] = set()
|
||||
|
||||
def _check_device() -> None:
|
||||
|
||||
@@ -4,8 +4,10 @@ from collections.abc import Awaitable, Callable, Coroutine
|
||||
from functools import wraps
|
||||
from typing import Any, Concatenate
|
||||
|
||||
from aioamazondevices.const import SPEAKER_GROUP_FAMILY
|
||||
from aioamazondevices.exceptions import CannotConnect, CannotRetrieveData
|
||||
|
||||
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
import homeassistant.helpers.entity_registry as er
|
||||
@@ -61,3 +63,21 @@ async def async_update_unique_id(
|
||||
|
||||
# Update the registry with the new unique_id
|
||||
entity_registry.async_update_entity(entity_id, new_unique_id=new_unique_id)
|
||||
|
||||
|
||||
async def async_remove_dnd_from_virtual_group(
|
||||
hass: HomeAssistant,
|
||||
coordinator: AmazonDevicesCoordinator,
|
||||
) -> None:
|
||||
"""Remove entity DND from virtual group."""
|
||||
entity_registry = er.async_get(hass)
|
||||
|
||||
for serial_num in coordinator.data:
|
||||
unique_id = f"{serial_num}-do_not_disturb"
|
||||
entity_id = entity_registry.async_get_entity_id(
|
||||
DOMAIN, SWITCH_DOMAIN, unique_id
|
||||
)
|
||||
is_group = coordinator.data[serial_num].device_family == SPEAKER_GROUP_FAMILY
|
||||
if entity_id and is_group:
|
||||
entity_registry.async_remove(entity_id)
|
||||
_LOGGER.debug("Removed DND switch from virtual group %s", entity_id)
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["brother", "pyasn1", "pysmi", "pysnmp"],
|
||||
"requirements": ["brother==5.1.0"],
|
||||
"requirements": ["brother==5.1.1"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"type": "_printer._tcp.local.",
|
||||
|
||||
@@ -4,6 +4,7 @@ from __future__ import annotations
|
||||
|
||||
from asyncio.exceptions import TimeoutError
|
||||
from collections.abc import Mapping
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
from aiocomelit import (
|
||||
@@ -27,25 +28,20 @@ from .utils import async_client_session
|
||||
DEFAULT_HOST = "192.168.1.252"
|
||||
DEFAULT_PIN = "111111"
|
||||
|
||||
|
||||
pin_regex = r"^[0-9]{4,10}$"
|
||||
|
||||
USER_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_HOST, default=DEFAULT_HOST): cv.string,
|
||||
vol.Required(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||
vol.Optional(CONF_PIN, default=DEFAULT_PIN): cv.matches_regex(pin_regex),
|
||||
vol.Optional(CONF_PIN, default=DEFAULT_PIN): cv.string,
|
||||
vol.Required(CONF_TYPE, default=BRIDGE): vol.In(DEVICE_TYPE_LIST),
|
||||
}
|
||||
)
|
||||
STEP_REAUTH_DATA_SCHEMA = vol.Schema(
|
||||
{vol.Required(CONF_PIN): cv.matches_regex(pin_regex)}
|
||||
)
|
||||
STEP_REAUTH_DATA_SCHEMA = vol.Schema({vol.Required(CONF_PIN): cv.string})
|
||||
STEP_RECONFIGURE = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Required(CONF_PORT): cv.port,
|
||||
vol.Optional(CONF_PIN, default=DEFAULT_PIN): cv.matches_regex(pin_regex),
|
||||
vol.Optional(CONF_PIN, default=DEFAULT_PIN): cv.string,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -55,6 +51,9 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str,
|
||||
|
||||
api: ComelitCommonApi
|
||||
|
||||
if not re.fullmatch(r"[0-9]{4,10}", data[CONF_PIN]):
|
||||
raise InvalidPin
|
||||
|
||||
session = await async_client_session(hass)
|
||||
if data.get(CONF_TYPE, BRIDGE) == BRIDGE:
|
||||
api = ComeliteSerialBridgeApi(
|
||||
@@ -105,6 +104,8 @@ class ComelitConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
errors["base"] = "cannot_connect"
|
||||
except InvalidAuth:
|
||||
errors["base"] = "invalid_auth"
|
||||
except InvalidPin:
|
||||
errors["base"] = "invalid_pin"
|
||||
except Exception: # noqa: BLE001
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
@@ -146,6 +147,8 @@ class ComelitConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
errors["base"] = "cannot_connect"
|
||||
except InvalidAuth:
|
||||
errors["base"] = "invalid_auth"
|
||||
except InvalidPin:
|
||||
errors["base"] = "invalid_pin"
|
||||
except Exception: # noqa: BLE001
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
@@ -189,6 +192,8 @@ class ComelitConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
errors["base"] = "cannot_connect"
|
||||
except InvalidAuth:
|
||||
errors["base"] = "invalid_auth"
|
||||
except InvalidPin:
|
||||
errors["base"] = "invalid_pin"
|
||||
except Exception: # noqa: BLE001
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
@@ -210,3 +215,7 @@ class CannotConnect(HomeAssistantError):
|
||||
|
||||
class InvalidAuth(HomeAssistantError):
|
||||
"""Error to indicate there is invalid auth."""
|
||||
|
||||
|
||||
class InvalidPin(HomeAssistantError):
|
||||
"""Error to indicate an invalid pin."""
|
||||
|
||||
@@ -161,7 +161,7 @@ class ComelitSerialBridge(
|
||||
entry: ComelitConfigEntry,
|
||||
host: str,
|
||||
port: int,
|
||||
pin: int,
|
||||
pin: str,
|
||||
session: ClientSession,
|
||||
) -> None:
|
||||
"""Initialize the scanner."""
|
||||
@@ -195,7 +195,7 @@ class ComelitVedoSystem(ComelitBaseCoordinator[AlarmDataObject]):
|
||||
entry: ComelitConfigEntry,
|
||||
host: str,
|
||||
port: int,
|
||||
pin: int,
|
||||
pin: str,
|
||||
session: ClientSession,
|
||||
) -> None:
|
||||
"""Initialize the scanner."""
|
||||
|
||||
@@ -7,7 +7,14 @@ from typing import Any, cast
|
||||
from aiocomelit import ComelitSerialBridgeObject
|
||||
from aiocomelit.const import COVER, STATE_COVER, STATE_OFF, STATE_ON
|
||||
|
||||
from homeassistant.components.cover import CoverDeviceClass, CoverEntity
|
||||
from homeassistant.components.cover import (
|
||||
STATE_CLOSED,
|
||||
STATE_CLOSING,
|
||||
STATE_OPEN,
|
||||
STATE_OPENING,
|
||||
CoverDeviceClass,
|
||||
CoverEntity,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
@@ -62,7 +69,6 @@ class ComelitCoverEntity(ComelitBridgeBaseEntity, RestoreEntity, CoverEntity):
|
||||
super().__init__(coordinator, device, config_entry_entry_id)
|
||||
# Device doesn't provide a status so we assume UNKNOWN at first startup
|
||||
self._last_action: int | None = None
|
||||
self._last_state: str | None = None
|
||||
|
||||
def _current_action(self, action: str) -> bool:
|
||||
"""Return the current cover action."""
|
||||
@@ -98,7 +104,6 @@ class ComelitCoverEntity(ComelitBridgeBaseEntity, RestoreEntity, CoverEntity):
|
||||
@bridge_api_call
|
||||
async def _cover_set_state(self, action: int, state: int) -> None:
|
||||
"""Set desired cover state."""
|
||||
self._last_state = self.state
|
||||
await self.coordinator.api.set_device_status(COVER, self._device.index, action)
|
||||
self.coordinator.data[COVER][self._device.index].status = state
|
||||
self.async_write_ha_state()
|
||||
@@ -124,5 +129,10 @@ class ComelitCoverEntity(ComelitBridgeBaseEntity, RestoreEntity, CoverEntity):
|
||||
|
||||
await super().async_added_to_hass()
|
||||
|
||||
if last_state := await self.async_get_last_state():
|
||||
self._last_state = last_state.state
|
||||
if (state := await self.async_get_last_state()) is not None:
|
||||
if state.state == STATE_CLOSED:
|
||||
self._last_action = STATE_COVER.index(STATE_CLOSING)
|
||||
if state.state == STATE_OPEN:
|
||||
self._last_action = STATE_COVER.index(STATE_OPENING)
|
||||
|
||||
self._attr_is_closed = state.state == STATE_CLOSED
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["aiocomelit"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["aiocomelit==0.12.3"]
|
||||
"requirements": ["aiocomelit==1.1.1"]
|
||||
}
|
||||
|
||||
@@ -43,11 +43,13 @@
|
||||
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]",
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||
"invalid_pin": "The provided PIN is invalid. It must be a 4-10 digit number.",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||
"invalid_pin": "[%key:component::comelit::config::abort::invalid_pin%]",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -514,7 +514,7 @@ class ChatLog:
|
||||
"""Set the LLM system prompt."""
|
||||
llm_api: llm.APIInstance | None = None
|
||||
|
||||
if user_llm_hass_api is None:
|
||||
if not user_llm_hass_api:
|
||||
pass
|
||||
elif isinstance(user_llm_hass_api, llm.API):
|
||||
llm_api = await user_llm_hass_api.async_get_api_instance(llm_context)
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_push",
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["pycync==0.4.0"]
|
||||
"requirements": ["pycync==0.4.1"]
|
||||
}
|
||||
|
||||
@@ -6,6 +6,6 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/daikin",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pydaikin"],
|
||||
"requirements": ["pydaikin==2.16.0"],
|
||||
"requirements": ["pydaikin==2.17.1"],
|
||||
"zeroconf": ["_dkapi._tcp.local."]
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/ecovacs",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["sleekxmppfs", "sucks", "deebot_client"],
|
||||
"requirements": ["py-sucks==0.9.11", "deebot-client==15.0.0"]
|
||||
"requirements": ["py-sucks==0.9.11", "deebot-client==15.1.0"]
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/environment_canada",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["env_canada"],
|
||||
"requirements": ["env-canada==0.11.2"]
|
||||
"requirements": ["env-canada==0.11.3"]
|
||||
}
|
||||
|
||||
@@ -20,5 +20,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["home-assistant-frontend==20251001.0"]
|
||||
"requirements": ["home-assistant-frontend==20251001.2"]
|
||||
}
|
||||
|
||||
@@ -76,10 +76,6 @@ async def async_unload_entry(
|
||||
hass: HomeAssistant, entry: GoogleAssistantSDKConfigEntry
|
||||
) -> bool:
|
||||
"""Unload a config entry."""
|
||||
if not hass.config_entries.async_loaded_entries(DOMAIN):
|
||||
for service_name in hass.services.async_services_for_domain(DOMAIN):
|
||||
hass.services.async_remove(DOMAIN, service_name)
|
||||
|
||||
conversation.async_unset_agent(hass, entry)
|
||||
|
||||
return True
|
||||
|
||||
@@ -26,7 +26,7 @@ from homeassistant.components.media_player import (
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import ATTR_ENTITY_ID, CONF_ACCESS_TOKEN
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||
from homeassistant.helpers.config_entry_oauth2_flow import OAuth2Session
|
||||
from homeassistant.helpers.event import async_call_later
|
||||
|
||||
@@ -68,7 +68,13 @@ async def async_send_text_commands(
|
||||
) -> list[CommandResponse]:
|
||||
"""Send text commands to Google Assistant Service."""
|
||||
# There can only be 1 entry (config_flow has single_instance_allowed)
|
||||
entry: GoogleAssistantSDKConfigEntry = hass.config_entries.async_entries(DOMAIN)[0]
|
||||
entries = hass.config_entries.async_loaded_entries(DOMAIN)
|
||||
if not entries:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="entry_not_loaded",
|
||||
)
|
||||
entry: GoogleAssistantSDKConfigEntry = entries[0]
|
||||
|
||||
session = entry.runtime_data.session
|
||||
try:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Support for Google Assistant SDK."""
|
||||
"""Services for the Google Assistant SDK integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
@@ -65,6 +65,9 @@
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"entry_not_loaded": {
|
||||
"message": "Entry not loaded"
|
||||
},
|
||||
"grpc_error": {
|
||||
"message": "Failed to communicate with Google Assistant"
|
||||
}
|
||||
|
||||
@@ -456,6 +456,7 @@ class GoogleGenerativeAILLMBaseEntity(Entity):
|
||||
"""Initialize the agent."""
|
||||
self.entry = entry
|
||||
self.subentry = subentry
|
||||
self.default_model = default_model
|
||||
self._attr_name = subentry.title
|
||||
self._genai_client = entry.runtime_data
|
||||
self._attr_unique_id = subentry.subentry_id
|
||||
@@ -489,7 +490,7 @@ class GoogleGenerativeAILLMBaseEntity(Entity):
|
||||
tools = tools or []
|
||||
tools.append(Tool(google_search=GoogleSearch()))
|
||||
|
||||
model_name = options.get(CONF_CHAT_MODEL, RECOMMENDED_CHAT_MODEL)
|
||||
model_name = options.get(CONF_CHAT_MODEL, self.default_model)
|
||||
# Avoid INVALID_ARGUMENT Developer instruction is not enabled for <model>
|
||||
supports_system_instruction = (
|
||||
"gemma" not in model_name
|
||||
@@ -620,7 +621,7 @@ class GoogleGenerativeAILLMBaseEntity(Entity):
|
||||
def create_generate_content_config(self) -> GenerateContentConfig:
|
||||
"""Create the GenerateContentConfig for the LLM."""
|
||||
options = self.subentry.data
|
||||
model = options.get(CONF_CHAT_MODEL, RECOMMENDED_CHAT_MODEL)
|
||||
model = options.get(CONF_CHAT_MODEL, self.default_model)
|
||||
thinking_config: ThinkingConfig | None = None
|
||||
if model.startswith("models/gemini-2.5") and not model.endswith(
|
||||
("tts", "image", "image-preview")
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/holiday",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["holidays==0.81", "babel==2.15.0"]
|
||||
"requirements": ["holidays==0.82", "babel==2.15.0"]
|
||||
}
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
"""Home Assistant Hardware integration helpers."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import defaultdict
|
||||
from collections.abc import AsyncIterator, Awaitable, Callable
|
||||
from contextlib import asynccontextmanager
|
||||
import logging
|
||||
from typing import Protocol
|
||||
from typing import TYPE_CHECKING, Protocol
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback as hass_callback
|
||||
|
||||
from . import DATA_COMPONENT
|
||||
from .util import FirmwareInfo
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .util import FirmwareInfo
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -51,6 +56,7 @@ class HardwareInfoDispatcher:
|
||||
self._notification_callbacks: defaultdict[
|
||||
str, set[Callable[[FirmwareInfo], None]]
|
||||
] = defaultdict(set)
|
||||
self._active_firmware_updates: dict[str, str] = {}
|
||||
|
||||
def register_firmware_info_provider(
|
||||
self, domain: str, platform: HardwareFirmwareInfoModule
|
||||
@@ -118,6 +124,36 @@ class HardwareInfoDispatcher:
|
||||
if fw_info is not None:
|
||||
yield fw_info
|
||||
|
||||
def register_firmware_update_in_progress(
|
||||
self, device: str, source_domain: str
|
||||
) -> None:
|
||||
"""Register that a firmware update is in progress for a device."""
|
||||
if device in self._active_firmware_updates:
|
||||
current_domain = self._active_firmware_updates[device]
|
||||
raise ValueError(
|
||||
f"Firmware update already in progress for {device} by {current_domain}"
|
||||
)
|
||||
self._active_firmware_updates[device] = source_domain
|
||||
|
||||
def unregister_firmware_update_in_progress(
|
||||
self, device: str, source_domain: str
|
||||
) -> None:
|
||||
"""Unregister a firmware update for a device."""
|
||||
if device not in self._active_firmware_updates:
|
||||
raise ValueError(f"No firmware update in progress for {device}")
|
||||
|
||||
if self._active_firmware_updates[device] != source_domain:
|
||||
current_domain = self._active_firmware_updates[device]
|
||||
raise ValueError(
|
||||
f"Firmware update for {device} is owned by {current_domain}, not {source_domain}"
|
||||
)
|
||||
|
||||
del self._active_firmware_updates[device]
|
||||
|
||||
def is_firmware_update_in_progress(self, device: str) -> bool:
|
||||
"""Check if a firmware update is in progress for a device."""
|
||||
return device in self._active_firmware_updates
|
||||
|
||||
|
||||
@hass_callback
|
||||
def async_register_firmware_info_provider(
|
||||
@@ -141,3 +177,42 @@ def async_notify_firmware_info(
|
||||
) -> Awaitable[None]:
|
||||
"""Notify the dispatcher of new firmware information."""
|
||||
return hass.data[DATA_COMPONENT].notify_firmware_info(domain, firmware_info)
|
||||
|
||||
|
||||
@hass_callback
|
||||
def async_register_firmware_update_in_progress(
|
||||
hass: HomeAssistant, device: str, source_domain: str
|
||||
) -> None:
|
||||
"""Register that a firmware update is in progress for a device."""
|
||||
return hass.data[DATA_COMPONENT].register_firmware_update_in_progress(
|
||||
device, source_domain
|
||||
)
|
||||
|
||||
|
||||
@hass_callback
|
||||
def async_unregister_firmware_update_in_progress(
|
||||
hass: HomeAssistant, device: str, source_domain: str
|
||||
) -> None:
|
||||
"""Unregister a firmware update for a device."""
|
||||
return hass.data[DATA_COMPONENT].unregister_firmware_update_in_progress(
|
||||
device, source_domain
|
||||
)
|
||||
|
||||
|
||||
@hass_callback
|
||||
def async_is_firmware_update_in_progress(hass: HomeAssistant, device: str) -> bool:
|
||||
"""Check if a firmware update is in progress for a device."""
|
||||
return hass.data[DATA_COMPONENT].is_firmware_update_in_progress(device)
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def async_firmware_update_context(
|
||||
hass: HomeAssistant, device: str, source_domain: str
|
||||
) -> AsyncIterator[None]:
|
||||
"""Register a device as having its firmware being actively updated."""
|
||||
async_register_firmware_update_in_progress(hass, device, source_domain)
|
||||
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
async_unregister_firmware_update_in_progress(hass, device, source_domain)
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
}
|
||||
},
|
||||
"abort": {
|
||||
"not_hassio_thread": "The OpenThread Border Router add-on can only be installed with Home Assistant OS. If you would like to use the {model} as a Thread border router, please flash the firmware manually using the [web flasher]({docs_web_flasher_url}) and set up OpenThread Border Router to communicate with it.",
|
||||
"not_hassio_thread": "The OpenThread Border Router add-on can only be installed with Home Assistant OS. If you would like to use the {model} as a Thread border router, please manually set up OpenThread Border Router to communicate with it.",
|
||||
"otbr_addon_already_running": "The OpenThread Border Router add-on is already running, it cannot be installed again.",
|
||||
"zha_still_using_stick": "This {model} is in use by the Zigbee Home Automation integration. Please migrate your Zigbee network to another adapter or delete the integration and try again.",
|
||||
"otbr_still_using_stick": "This {model} is in use by the OpenThread Border Router add-on. If you use the Thread network, make sure you have alternative border routers. Uninstall the add-on and try again.",
|
||||
|
||||
@@ -275,6 +275,7 @@ class BaseFirmwareUpdateEntity(
|
||||
expected_installed_firmware_type=self.entity_description.expected_firmware_type,
|
||||
bootloader_reset_methods=self.bootloader_reset_methods,
|
||||
progress_callback=self._update_progress,
|
||||
domain=self._config_entry.domain,
|
||||
)
|
||||
finally:
|
||||
self._attr_in_progress = False
|
||||
|
||||
@@ -26,6 +26,7 @@ from homeassistant.helpers.singleton import singleton
|
||||
|
||||
from . import DATA_COMPONENT
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
OTBR_ADDON_MANAGER_DATA,
|
||||
OTBR_ADDON_NAME,
|
||||
OTBR_ADDON_SLUG,
|
||||
@@ -33,6 +34,7 @@ from .const import (
|
||||
ZIGBEE_FLASHER_ADDON_NAME,
|
||||
ZIGBEE_FLASHER_ADDON_SLUG,
|
||||
)
|
||||
from .helpers import async_firmware_update_context
|
||||
from .silabs_multiprotocol_addon import (
|
||||
WaitingAddonManager,
|
||||
get_multiprotocol_addon_manager,
|
||||
@@ -359,45 +361,50 @@ async def async_flash_silabs_firmware(
|
||||
expected_installed_firmware_type: ApplicationType,
|
||||
bootloader_reset_methods: Sequence[ResetTarget] = (),
|
||||
progress_callback: Callable[[int, int], None] | None = None,
|
||||
*,
|
||||
domain: str = DOMAIN,
|
||||
) -> FirmwareInfo:
|
||||
"""Flash firmware to the SiLabs device."""
|
||||
firmware_info = await guess_firmware_info(hass, device)
|
||||
_LOGGER.debug("Identified firmware info: %s", firmware_info)
|
||||
async with async_firmware_update_context(hass, device, domain):
|
||||
firmware_info = await guess_firmware_info(hass, device)
|
||||
_LOGGER.debug("Identified firmware info: %s", firmware_info)
|
||||
|
||||
fw_image = await hass.async_add_executor_job(parse_firmware_image, fw_data)
|
||||
fw_image = await hass.async_add_executor_job(parse_firmware_image, fw_data)
|
||||
|
||||
flasher = Flasher(
|
||||
device=device,
|
||||
probe_methods=(
|
||||
ApplicationType.GECKO_BOOTLOADER.as_flasher_application_type(),
|
||||
ApplicationType.EZSP.as_flasher_application_type(),
|
||||
ApplicationType.SPINEL.as_flasher_application_type(),
|
||||
ApplicationType.CPC.as_flasher_application_type(),
|
||||
),
|
||||
bootloader_reset=tuple(
|
||||
m.as_flasher_reset_target() for m in bootloader_reset_methods
|
||||
),
|
||||
)
|
||||
|
||||
async with AsyncExitStack() as stack:
|
||||
for owner in firmware_info.owners:
|
||||
await stack.enter_async_context(owner.temporarily_stop(hass))
|
||||
|
||||
try:
|
||||
# Enter the bootloader with indeterminate progress
|
||||
await flasher.enter_bootloader()
|
||||
|
||||
# Flash the firmware, with progress
|
||||
await flasher.flash_firmware(fw_image, progress_callback=progress_callback)
|
||||
except Exception as err:
|
||||
raise HomeAssistantError("Failed to flash firmware") from err
|
||||
|
||||
probed_firmware_info = await probe_silabs_firmware_info(
|
||||
device,
|
||||
probe_methods=(expected_installed_firmware_type,),
|
||||
flasher = Flasher(
|
||||
device=device,
|
||||
probe_methods=(
|
||||
ApplicationType.GECKO_BOOTLOADER.as_flasher_application_type(),
|
||||
ApplicationType.EZSP.as_flasher_application_type(),
|
||||
ApplicationType.SPINEL.as_flasher_application_type(),
|
||||
ApplicationType.CPC.as_flasher_application_type(),
|
||||
),
|
||||
bootloader_reset=tuple(
|
||||
m.as_flasher_reset_target() for m in bootloader_reset_methods
|
||||
),
|
||||
)
|
||||
|
||||
if probed_firmware_info is None:
|
||||
raise HomeAssistantError("Failed to probe the firmware after flashing")
|
||||
async with AsyncExitStack() as stack:
|
||||
for owner in firmware_info.owners:
|
||||
await stack.enter_async_context(owner.temporarily_stop(hass))
|
||||
|
||||
return probed_firmware_info
|
||||
try:
|
||||
# Enter the bootloader with indeterminate progress
|
||||
await flasher.enter_bootloader()
|
||||
|
||||
# Flash the firmware, with progress
|
||||
await flasher.flash_firmware(
|
||||
fw_image, progress_callback=progress_callback
|
||||
)
|
||||
except Exception as err:
|
||||
raise HomeAssistantError("Failed to flash firmware") from err
|
||||
|
||||
probed_firmware_info = await probe_silabs_firmware_info(
|
||||
device,
|
||||
probe_methods=(expected_installed_firmware_type,),
|
||||
)
|
||||
|
||||
if probed_firmware_info is None:
|
||||
raise HomeAssistantError("Failed to probe the firmware after flashing")
|
||||
|
||||
return probed_firmware_info
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"domain": "iometer",
|
||||
"name": "IOmeter",
|
||||
"codeowners": ["@MaestroOnICe"],
|
||||
"codeowners": ["@jukrebs"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/iometer",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["iometer==0.1.0"],
|
||||
"requirements": ["iometer==0.2.0"],
|
||||
"zeroconf": ["_iometer._tcp.local."]
|
||||
}
|
||||
|
||||
@@ -37,5 +37,5 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["pylamarzocco"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["pylamarzocco==2.1.1"]
|
||||
"requirements": ["pylamarzocco==2.1.2"]
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/melcloud",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["pymelcloud"],
|
||||
"requirements": ["python-melcloud==0.1.0"]
|
||||
"requirements": ["python-melcloud==0.1.2"]
|
||||
}
|
||||
|
||||
@@ -54,6 +54,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
DEFAULT_PLATE_COUNT = 4
|
||||
|
||||
PLATE_COUNT = {
|
||||
"KM7575": 6,
|
||||
"KM7678": 6,
|
||||
"KM7697": 6,
|
||||
"KM7878": 6,
|
||||
|
||||
@@ -253,6 +253,7 @@ class ModbusHub:
|
||||
self._client: (
|
||||
AsyncModbusSerialClient | AsyncModbusTcpClient | AsyncModbusUdpClient | None
|
||||
) = None
|
||||
self._lock = asyncio.Lock()
|
||||
self.event_connected = asyncio.Event()
|
||||
self.hass = hass
|
||||
self.name = client_config[CONF_NAME]
|
||||
@@ -415,7 +416,9 @@ class ModbusHub:
|
||||
"""Convert async to sync pymodbus call."""
|
||||
if not self._client:
|
||||
return None
|
||||
result = await self.low_level_pb_call(unit, address, value, use_call)
|
||||
if self._msg_wait:
|
||||
await asyncio.sleep(self._msg_wait)
|
||||
return result
|
||||
async with self._lock:
|
||||
result = await self.low_level_pb_call(unit, address, value, use_call)
|
||||
if self._msg_wait:
|
||||
# small delay until next request/response
|
||||
await asyncio.sleep(self._msg_wait)
|
||||
return result
|
||||
|
||||
@@ -188,7 +188,10 @@ class MqttLock(MqttEntity, LockEntity):
|
||||
return
|
||||
if payload == self._config[CONF_PAYLOAD_RESET]:
|
||||
# Reset the state to `unknown`
|
||||
self._attr_is_locked = None
|
||||
self._attr_is_locked = self._attr_is_locking = None
|
||||
self._attr_is_unlocking = None
|
||||
self._attr_is_open = self._attr_is_opening = None
|
||||
self._attr_is_jammed = None
|
||||
elif payload in self._valid_states:
|
||||
self._attr_is_locked = payload == self._config[CONF_STATE_LOCKED]
|
||||
self._attr_is_locking = payload == self._config[CONF_STATE_LOCKING]
|
||||
|
||||
@@ -157,7 +157,7 @@ def async_setup_services(hass: HomeAssistant) -> None:
|
||||
) from error
|
||||
except NordPoolEmptyResponseError:
|
||||
return {area: [] for area in areas}
|
||||
except NordPoolError as error:
|
||||
except (NordPoolError, TimeoutError) as error:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="connection_error",
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["opower"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["opower==0.15.5"]
|
||||
"requirements": ["opower==0.15.6"]
|
||||
}
|
||||
|
||||
@@ -75,6 +75,9 @@ async def _title(hass: HomeAssistant, discovery_info: HassioServiceInfo) -> str:
|
||||
if device and ("Connect_ZBT-1" in device or "SkyConnect" in device):
|
||||
return f"Home Assistant Connect ZBT-1 ({discovery_info.name})"
|
||||
|
||||
if device and "Nabu_Casa_ZBT-2" in device:
|
||||
return f"Home Assistant Connect ZBT-2 ({discovery_info.name})"
|
||||
|
||||
return discovery_info.name
|
||||
|
||||
|
||||
|
||||
@@ -39,6 +39,23 @@ from .renault_vehicle import COORDINATORS, RenaultVehicleProxy
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def _get_filtered_vehicles(account: RenaultAccount) -> list[KamereonVehiclesLink]:
|
||||
"""Filter out vehicles with missing details.
|
||||
|
||||
May be due to new purchases, or issue with the Renault servers.
|
||||
"""
|
||||
vehicles = await account.get_vehicles()
|
||||
if not vehicles.vehicleLinks:
|
||||
return []
|
||||
result: list[KamereonVehiclesLink] = []
|
||||
for link in vehicles.vehicleLinks:
|
||||
if link.vehicleDetails is None:
|
||||
LOGGER.warning("Ignoring vehicle with missing details: %s", link.vin)
|
||||
continue
|
||||
result.append(link)
|
||||
return result
|
||||
|
||||
|
||||
class RenaultHub:
|
||||
"""Handle account communication with Renault servers."""
|
||||
|
||||
@@ -84,49 +101,48 @@ class RenaultHub:
|
||||
account_id: str = config_entry.data[CONF_KAMEREON_ACCOUNT_ID]
|
||||
|
||||
self._account = await self._client.get_api_account(account_id)
|
||||
vehicles = await self._account.get_vehicles()
|
||||
if vehicles.vehicleLinks:
|
||||
if any(
|
||||
vehicle_link.vehicleDetails is None
|
||||
for vehicle_link in vehicles.vehicleLinks
|
||||
):
|
||||
raise ConfigEntryNotReady(
|
||||
"Failed to retrieve vehicle details from Renault servers"
|
||||
)
|
||||
|
||||
num_call_per_scan = len(COORDINATORS) * len(vehicles.vehicleLinks)
|
||||
scan_interval = timedelta(
|
||||
seconds=(3600 * num_call_per_scan) / MAX_CALLS_PER_HOURS
|
||||
vehicle_links = await _get_filtered_vehicles(self._account)
|
||||
if not vehicle_links:
|
||||
LOGGER.debug(
|
||||
"No valid vehicle details found for account_id: %s", account_id
|
||||
)
|
||||
raise ConfigEntryNotReady(
|
||||
"Failed to retrieve vehicle details from Renault servers"
|
||||
)
|
||||
|
||||
device_registry = dr.async_get(self._hass)
|
||||
await asyncio.gather(
|
||||
*(
|
||||
self.async_initialise_vehicle(
|
||||
vehicle_link,
|
||||
self._account,
|
||||
scan_interval,
|
||||
config_entry,
|
||||
device_registry,
|
||||
)
|
||||
for vehicle_link in vehicles.vehicleLinks
|
||||
)
|
||||
)
|
||||
num_call_per_scan = len(COORDINATORS) * len(vehicle_links)
|
||||
scan_interval = timedelta(
|
||||
seconds=(3600 * num_call_per_scan) / MAX_CALLS_PER_HOURS
|
||||
)
|
||||
|
||||
# all vehicles have been initiated with the right number of active coordinators
|
||||
num_call_per_scan = 0
|
||||
for vehicle_link in vehicles.vehicleLinks:
|
||||
device_registry = dr.async_get(self._hass)
|
||||
await asyncio.gather(
|
||||
*(
|
||||
self.async_initialise_vehicle(
|
||||
vehicle_link,
|
||||
self._account,
|
||||
scan_interval,
|
||||
config_entry,
|
||||
device_registry,
|
||||
)
|
||||
for vehicle_link in vehicle_links
|
||||
)
|
||||
)
|
||||
|
||||
# all vehicles have been initiated with the right number of active coordinators
|
||||
num_call_per_scan = 0
|
||||
for vehicle_link in vehicle_links:
|
||||
vehicle = self._vehicles[str(vehicle_link.vin)]
|
||||
num_call_per_scan += len(vehicle.coordinators)
|
||||
|
||||
new_scan_interval = timedelta(
|
||||
seconds=(3600 * num_call_per_scan) / MAX_CALLS_PER_HOURS
|
||||
)
|
||||
if new_scan_interval != scan_interval:
|
||||
# we need to change the vehicles with the right scan interval
|
||||
for vehicle_link in vehicle_links:
|
||||
vehicle = self._vehicles[str(vehicle_link.vin)]
|
||||
num_call_per_scan += len(vehicle.coordinators)
|
||||
|
||||
new_scan_interval = timedelta(
|
||||
seconds=(3600 * num_call_per_scan) / MAX_CALLS_PER_HOURS
|
||||
)
|
||||
if new_scan_interval != scan_interval:
|
||||
# we need to change the vehicles with the right scan interval
|
||||
for vehicle_link in vehicles.vehicleLinks:
|
||||
vehicle = self._vehicles[str(vehicle_link.vin)]
|
||||
vehicle.update_scan_interval(new_scan_interval)
|
||||
vehicle.update_scan_interval(new_scan_interval)
|
||||
|
||||
async def async_initialise_vehicle(
|
||||
self,
|
||||
@@ -164,10 +180,10 @@ class RenaultHub:
|
||||
"""Get Kamereon account ids."""
|
||||
accounts = []
|
||||
for account in await self._client.get_api_accounts():
|
||||
vehicles = await account.get_vehicles()
|
||||
vehicle_links = await _get_filtered_vehicles(account)
|
||||
|
||||
# Only add the account if it has linked vehicles.
|
||||
if vehicles.vehicleLinks:
|
||||
if vehicle_links:
|
||||
accounts.append(account.account_id)
|
||||
return accounts
|
||||
|
||||
|
||||
@@ -82,7 +82,7 @@ class RoborockFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
assert self._client
|
||||
errors: dict[str, str] = {}
|
||||
try:
|
||||
await self._client.request_code()
|
||||
await self._client.request_code_v4()
|
||||
except RoborockAccountDoesNotExist:
|
||||
errors["base"] = "invalid_email"
|
||||
except RoborockUrlException:
|
||||
@@ -111,7 +111,7 @@ class RoborockFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
code = user_input[CONF_ENTRY_CODE]
|
||||
_LOGGER.debug("Logging into Roborock account using email provided code")
|
||||
try:
|
||||
user_data = await self._client.code_login(code)
|
||||
user_data = await self._client.code_login_v4(code)
|
||||
except RoborockInvalidCode:
|
||||
errors["base"] = "invalid_code"
|
||||
except RoborockException:
|
||||
@@ -129,7 +129,7 @@ class RoborockFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
reauth_entry, data_updates={CONF_USER_DATA: user_data.as_dict()}
|
||||
)
|
||||
self._abort_if_unique_id_configured(error="already_configured_account")
|
||||
return self._create_entry(self._client, self._username, user_data)
|
||||
return await self._create_entry(self._client, self._username, user_data)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="code",
|
||||
@@ -176,7 +176,7 @@ class RoborockFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
return await self.async_step_code()
|
||||
return self.async_show_form(step_id="reauth_confirm", errors=errors)
|
||||
|
||||
def _create_entry(
|
||||
async def _create_entry(
|
||||
self, client: RoborockApiClient, username: str, user_data: UserData
|
||||
) -> ConfigFlowResult:
|
||||
"""Finished config flow and create entry."""
|
||||
@@ -185,7 +185,7 @@ class RoborockFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
data={
|
||||
CONF_USERNAME: username,
|
||||
CONF_USER_DATA: user_data.as_dict(),
|
||||
CONF_BASE_URL: client.base_url,
|
||||
CONF_BASE_URL: await client.base_url,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
"loggers": ["roborock"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": [
|
||||
"python-roborock==2.49.1",
|
||||
"python-roborock==2.50.2",
|
||||
"vacuum-map-parser-roborock==0.1.4"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
},
|
||||
"config_subentries": {
|
||||
"partition": {
|
||||
"entry_type": "Partition",
|
||||
"initiate_flow": {
|
||||
"user": "Add partition"
|
||||
},
|
||||
@@ -57,6 +58,7 @@
|
||||
}
|
||||
},
|
||||
"zone": {
|
||||
"entry_type": "Zone",
|
||||
"initiate_flow": {
|
||||
"user": "Add zone"
|
||||
},
|
||||
@@ -91,6 +93,7 @@
|
||||
}
|
||||
},
|
||||
"output": {
|
||||
"entry_type": "Output",
|
||||
"initiate_flow": {
|
||||
"user": "Add output"
|
||||
},
|
||||
@@ -125,6 +128,7 @@
|
||||
}
|
||||
},
|
||||
"switchable_output": {
|
||||
"entry_type": "Switchable output",
|
||||
"initiate_flow": {
|
||||
"user": "Add switchable output"
|
||||
},
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/sharkiq",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["sharkiq"],
|
||||
"requirements": ["sharkiq==1.4.0"]
|
||||
"requirements": ["sharkiq==1.4.2"]
|
||||
}
|
||||
|
||||
@@ -319,7 +319,7 @@ RPC_SENSORS: Final = {
|
||||
),
|
||||
"presencezone_state": RpcBinarySensorDescription(
|
||||
key="presencezone",
|
||||
sub_key="state",
|
||||
sub_key="value",
|
||||
name="Occupancy",
|
||||
device_class=BinarySensorDeviceClass.OCCUPANCY,
|
||||
entity_class=RpcPresenceBinarySensor,
|
||||
|
||||
@@ -226,6 +226,8 @@ class RpcShellyCover(ShellyRpcEntity, CoverEntity):
|
||||
def _update_callback(self) -> None:
|
||||
"""Handle device update. Use a task when opening/closing is in progress."""
|
||||
super()._update_callback()
|
||||
if not self.coordinator.device.initialized:
|
||||
return
|
||||
if self.is_closing or self.is_opening:
|
||||
self.launch_update_task()
|
||||
|
||||
|
||||
@@ -692,27 +692,25 @@ def async_remove_orphaned_entities(
|
||||
"""Remove orphaned entities."""
|
||||
orphaned_entities = []
|
||||
entity_reg = er.async_get(hass)
|
||||
device_reg = dr.async_get(hass)
|
||||
|
||||
if not (
|
||||
devices := device_reg.devices.get_devices_for_config_entry_id(config_entry_id)
|
||||
):
|
||||
return
|
||||
entities = er.async_entries_for_config_entry(entity_reg, config_entry_id)
|
||||
for entity in entities:
|
||||
if not entity.entity_id.startswith(platform):
|
||||
continue
|
||||
if key_suffix is not None and key_suffix not in entity.unique_id:
|
||||
continue
|
||||
# we are looking for the component ID, e.g. boolean:201, em1data:1
|
||||
if not (match := COMPONENT_ID_PATTERN.search(entity.unique_id)):
|
||||
continue
|
||||
|
||||
for device in devices:
|
||||
entities = er.async_entries_for_device(entity_reg, device.id, True)
|
||||
for entity in entities:
|
||||
if not entity.entity_id.startswith(platform):
|
||||
continue
|
||||
if key_suffix is not None and key_suffix not in entity.unique_id:
|
||||
continue
|
||||
# we are looking for the component ID, e.g. boolean:201, em1data:1
|
||||
if not (match := COMPONENT_ID_PATTERN.search(entity.unique_id)):
|
||||
continue
|
||||
|
||||
key = match.group()
|
||||
if key not in keys:
|
||||
orphaned_entities.append(entity.unique_id.split("-", 1)[1])
|
||||
key = match.group()
|
||||
if key not in keys:
|
||||
LOGGER.debug(
|
||||
"Found orphaned Shelly entity: %s, unique id: %s",
|
||||
entity.entity_id,
|
||||
entity.unique_id,
|
||||
)
|
||||
orphaned_entities.append(entity.unique_id.split("-", 1)[1])
|
||||
|
||||
if orphaned_entities:
|
||||
async_remove_shelly_rpc_entities(hass, platform, mac, orphaned_entities)
|
||||
|
||||
@@ -100,8 +100,9 @@ ATTR_PIN_VALUE = "pin"
|
||||
ATTR_TIMESTAMP = "timestamp"
|
||||
|
||||
DEFAULT_SCAN_INTERVAL = timedelta(seconds=30)
|
||||
DEFAULT_SOCKET_MIN_RETRY = 15
|
||||
|
||||
WEBSOCKET_RECONNECT_RETRIES = 3
|
||||
WEBSOCKET_RETRY_DELAY = 2
|
||||
|
||||
EVENT_SIMPLISAFE_EVENT = "SIMPLISAFE_EVENT"
|
||||
EVENT_SIMPLISAFE_NOTIFICATION = "SIMPLISAFE_NOTIFICATION"
|
||||
@@ -419,6 +420,7 @@ class SimpliSafe:
|
||||
self._api = api
|
||||
self._hass = hass
|
||||
self._system_notifications: dict[int, set[SystemNotification]] = {}
|
||||
self._websocket_reconnect_retries: int = 0
|
||||
self._websocket_reconnect_task: asyncio.Task | None = None
|
||||
self.entry = entry
|
||||
self.initial_event_to_use: dict[int, dict[str, Any]] = {}
|
||||
@@ -469,6 +471,8 @@ class SimpliSafe:
|
||||
"""Start a websocket reconnection loop."""
|
||||
assert self._api.websocket
|
||||
|
||||
self._websocket_reconnect_retries += 1
|
||||
|
||||
try:
|
||||
await self._api.websocket.async_connect()
|
||||
await self._api.websocket.async_listen()
|
||||
@@ -479,9 +483,21 @@ class SimpliSafe:
|
||||
LOGGER.error("Failed to connect to websocket: %s", err)
|
||||
except Exception as err: # noqa: BLE001
|
||||
LOGGER.error("Unknown exception while connecting to websocket: %s", err)
|
||||
else:
|
||||
self._websocket_reconnect_retries = 0
|
||||
|
||||
LOGGER.debug("Reconnecting to websocket")
|
||||
await self._async_cancel_websocket_loop()
|
||||
if self._websocket_reconnect_retries >= WEBSOCKET_RECONNECT_RETRIES:
|
||||
LOGGER.error("Max websocket connection retries exceeded")
|
||||
return
|
||||
|
||||
delay = WEBSOCKET_RETRY_DELAY * (2 ** (self._websocket_reconnect_retries - 1))
|
||||
LOGGER.info(
|
||||
"Retrying websocket connection in %s seconds (attempt %s/%s)",
|
||||
delay,
|
||||
self._websocket_reconnect_retries,
|
||||
WEBSOCKET_RECONNECT_RETRIES,
|
||||
)
|
||||
await asyncio.sleep(delay)
|
||||
self._websocket_reconnect_task = self._hass.async_create_task(
|
||||
self._async_start_websocket_loop()
|
||||
)
|
||||
|
||||
@@ -109,6 +109,8 @@ PRESET_MODE_TO_HA = {
|
||||
"quiet": "quiet",
|
||||
"longWind": "long_wind",
|
||||
"smart": "smart",
|
||||
"motionIndirect": "motion_indirect",
|
||||
"motionDirect": "motion_direct",
|
||||
}
|
||||
|
||||
HA_MODE_TO_PRESET_MODE = {v: k for k, v in PRESET_MODE_TO_HA.items()}
|
||||
|
||||
@@ -30,5 +30,5 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["pysmartthings"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["pysmartthings==3.3.0"]
|
||||
"requirements": ["pysmartthings==3.3.1"]
|
||||
}
|
||||
|
||||
@@ -87,7 +87,9 @@
|
||||
"wind_free_sleep": "WindFree sleep",
|
||||
"quiet": "Quiet",
|
||||
"long_wind": "Long wind",
|
||||
"smart": "Smart"
|
||||
"smart": "Smart",
|
||||
"motion_direct": "Motion direct",
|
||||
"motion_indirect": "Motion indirect"
|
||||
}
|
||||
},
|
||||
"fan_mode": {
|
||||
|
||||
@@ -10,6 +10,28 @@
|
||||
"zigbee_type": {
|
||||
"default": "mdi:zigbee"
|
||||
}
|
||||
},
|
||||
"switch": {
|
||||
"disable_led": {
|
||||
"default": "mdi:led-off"
|
||||
},
|
||||
"auto_zigbee_update": {
|
||||
"default": "mdi:autorenew"
|
||||
},
|
||||
"night_mode": {
|
||||
"default": "mdi:lightbulb-night"
|
||||
},
|
||||
"vpn_enabled": {
|
||||
"default": "mdi:shield-lock"
|
||||
}
|
||||
},
|
||||
"button": {
|
||||
"zigbee_flash_mode": {
|
||||
"default": "mdi:memory-arrow-down"
|
||||
},
|
||||
"reconnect_zigbee_router": {
|
||||
"default": "mdi:connection"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,7 +51,6 @@ SWITCHES: list[SmSwitchEntityDescription] = [
|
||||
SmSwitchEntityDescription(
|
||||
key="auto_zigbee_update",
|
||||
translation_key="auto_zigbee_update",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
setting=Settings.ZB_AUTOUPDATE,
|
||||
entity_registry_enabled_default=False,
|
||||
state_fn=lambda x: x.auto_zigbee,
|
||||
@@ -83,6 +82,7 @@ class SmSwitch(SmEntity, SwitchEntity):
|
||||
coordinator: SmDataUpdateCoordinator
|
||||
entity_description: SmSwitchEntityDescription
|
||||
_attr_device_class = SwitchDeviceClass.SWITCH
|
||||
_attr_entity_category = EntityCategory.CONFIG
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
|
||||
@@ -193,7 +193,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: SqueezeboxConfigEntry) -
|
||||
if player.player_id in entry.runtime_data.known_player_ids:
|
||||
await player.async_update()
|
||||
async_dispatcher_send(
|
||||
hass, SIGNAL_PLAYER_REDISCOVERED, player.player_id, player.connected
|
||||
hass,
|
||||
SIGNAL_PLAYER_REDISCOVERED + entry.entry_id,
|
||||
player.player_id,
|
||||
player.connected,
|
||||
)
|
||||
else:
|
||||
_LOGGER.debug("Adding new entity: %s", player)
|
||||
@@ -203,7 +206,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: SqueezeboxConfigEntry) -
|
||||
await player_coordinator.async_refresh()
|
||||
entry.runtime_data.known_player_ids.add(player.player_id)
|
||||
async_dispatcher_send(
|
||||
hass, SIGNAL_PLAYER_DISCOVERED, player_coordinator
|
||||
hass, SIGNAL_PLAYER_DISCOVERED + entry.entry_id, player_coordinator
|
||||
)
|
||||
|
||||
if players := await lms.async_get_players():
|
||||
|
||||
@@ -132,7 +132,9 @@ async def async_setup_entry(
|
||||
async_add_entities(entities)
|
||||
|
||||
entry.async_on_unload(
|
||||
async_dispatcher_connect(hass, SIGNAL_PLAYER_DISCOVERED, _player_discovered)
|
||||
async_dispatcher_connect(
|
||||
hass, SIGNAL_PLAYER_DISCOVERED + entry.entry_id, _player_discovered
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -117,7 +117,9 @@ class SqueezeBoxPlayerUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
|
||||
# start listening for restored players
|
||||
self._remove_dispatcher = async_dispatcher_connect(
|
||||
self.hass, SIGNAL_PLAYER_REDISCOVERED, self.rediscovered
|
||||
self.hass,
|
||||
SIGNAL_PLAYER_REDISCOVERED + self.config_entry.entry_id,
|
||||
self.rediscovered,
|
||||
)
|
||||
|
||||
alarm_dict: dict[str, Alarm] = (
|
||||
|
||||
@@ -175,7 +175,9 @@ async def async_setup_entry(
|
||||
async_add_entities([SqueezeBoxMediaPlayerEntity(coordinator)])
|
||||
|
||||
entry.async_on_unload(
|
||||
async_dispatcher_connect(hass, SIGNAL_PLAYER_DISCOVERED, _player_discovered)
|
||||
async_dispatcher_connect(
|
||||
hass, SIGNAL_PLAYER_DISCOVERED + entry.entry_id, _player_discovered
|
||||
)
|
||||
)
|
||||
|
||||
# Register entity services
|
||||
|
||||
@@ -89,7 +89,9 @@ async def async_setup_entry(
|
||||
async_add_entities([SqueezeBoxAlarmsEnabledEntity(coordinator)])
|
||||
|
||||
entry.async_on_unload(
|
||||
async_dispatcher_connect(hass, SIGNAL_PLAYER_DISCOVERED, _player_discovered)
|
||||
async_dispatcher_connect(
|
||||
hass, SIGNAL_PLAYER_DISCOVERED + entry.entry_id, _player_discovered
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -143,6 +143,7 @@ class SynologyDSMFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
self.reauth_conf: Mapping[str, Any] = {}
|
||||
self.reauth_reason: str | None = None
|
||||
self.shares: list[SynoFileSharedFolder] | None = None
|
||||
self.api: SynologyDSM | None = None
|
||||
|
||||
def _show_form(
|
||||
self,
|
||||
@@ -156,6 +157,7 @@ class SynologyDSMFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
description_placeholders = {}
|
||||
data_schema = None
|
||||
self.api = None
|
||||
|
||||
if step_id == "link":
|
||||
user_input.update(self.discovered_conf)
|
||||
@@ -194,14 +196,21 @@ class SynologyDSMFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
else:
|
||||
port = DEFAULT_PORT
|
||||
|
||||
session = async_get_clientsession(self.hass, verify_ssl)
|
||||
api = SynologyDSM(
|
||||
session, host, port, username, password, use_ssl, timeout=DEFAULT_TIMEOUT
|
||||
)
|
||||
if self.api is None:
|
||||
session = async_get_clientsession(self.hass, verify_ssl)
|
||||
self.api = SynologyDSM(
|
||||
session,
|
||||
host,
|
||||
port,
|
||||
username,
|
||||
password,
|
||||
use_ssl,
|
||||
timeout=DEFAULT_TIMEOUT,
|
||||
)
|
||||
|
||||
errors = {}
|
||||
try:
|
||||
serial = await _login_and_fetch_syno_info(api, otp_code)
|
||||
serial = await _login_and_fetch_syno_info(self.api, otp_code)
|
||||
except SynologyDSMLogin2SARequiredException:
|
||||
return await self.async_step_2sa(user_input)
|
||||
except SynologyDSMLogin2SAFailedException:
|
||||
@@ -221,10 +230,11 @@ class SynologyDSMFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
errors["base"] = "missing_data"
|
||||
|
||||
if errors:
|
||||
self.api = None
|
||||
return self._show_form(step_id, user_input, errors)
|
||||
|
||||
with suppress(*SYNOLOGY_CONNECTION_EXCEPTIONS):
|
||||
self.shares = await api.file.get_shared_folders(only_writable=True)
|
||||
self.shares = await self.api.file.get_shared_folders(only_writable=True)
|
||||
|
||||
if self.shares and not backup_path:
|
||||
return await self.async_step_backup_share(user_input)
|
||||
@@ -239,14 +249,14 @@ class SynologyDSMFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
CONF_VERIFY_SSL: verify_ssl,
|
||||
CONF_USERNAME: username,
|
||||
CONF_PASSWORD: password,
|
||||
CONF_MAC: api.network.macs,
|
||||
CONF_MAC: self.api.network.macs,
|
||||
}
|
||||
config_options = {
|
||||
CONF_BACKUP_PATH: backup_path,
|
||||
CONF_BACKUP_SHARE: backup_share,
|
||||
}
|
||||
if otp_code:
|
||||
config_data[CONF_DEVICE_TOKEN] = api.device_token
|
||||
config_data[CONF_DEVICE_TOKEN] = self.api.device_token
|
||||
if user_input.get(CONF_DISKS):
|
||||
config_data[CONF_DISKS] = user_input[CONF_DISKS]
|
||||
if user_input.get(CONF_VOLUMES):
|
||||
|
||||
@@ -336,6 +336,7 @@ BASE_SENSOR_TYPES: tuple[SystemBridgeSensorEntityDescription, ...] = (
|
||||
key="power_usage",
|
||||
translation_key="power_usage",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
suggested_display_precision=2,
|
||||
icon="mdi:power-plug",
|
||||
@@ -577,7 +578,6 @@ async def async_setup_entry(
|
||||
key=f"gpu_{gpu.id}_power_usage",
|
||||
name=f"{gpu.name} power usage",
|
||||
entity_registry_enabled_default=False,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
value=lambda data, k=index: gpu_power_usage(data, k),
|
||||
|
||||
@@ -372,6 +372,7 @@ class TriggerBinarySensorEntity(TriggerEntity, BinarySensorEntity, RestoreEntity
|
||||
def _set_state(self, state, _=None):
|
||||
"""Set up auto off."""
|
||||
self._attr_is_on = state
|
||||
self._delay_cancel = None
|
||||
self.async_write_ha_state()
|
||||
|
||||
if not state:
|
||||
|
||||
@@ -300,9 +300,10 @@ class TuyaCoverEntity(TuyaEntity, CoverEntity):
|
||||
self._current_state is not None
|
||||
and (current_state := self.device.status.get(self._current_state))
|
||||
is not None
|
||||
and current_state != "stop"
|
||||
):
|
||||
return self.entity_description.current_state_inverse is not (
|
||||
current_state in (True, "fully_close")
|
||||
current_state in (True, "close", "fully_close")
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
@@ -212,17 +212,17 @@ class VeSyncFanHA(VeSyncBaseEntity, FanEntity):
|
||||
await self.device.turn_on()
|
||||
|
||||
if preset_mode == VS_FAN_MODE_AUTO:
|
||||
success = await self.device.auto_mode()
|
||||
success = await self.device.set_auto_mode()
|
||||
elif preset_mode == VS_FAN_MODE_SLEEP:
|
||||
success = await self.device.sleep_mode()
|
||||
success = await self.device.set_sleep_mode()
|
||||
elif preset_mode == VS_FAN_MODE_ADVANCED_SLEEP:
|
||||
success = await self.device.advanced_sleep_mode()
|
||||
success = await self.device.set_advanced_sleep_mode()
|
||||
elif preset_mode == VS_FAN_MODE_PET:
|
||||
success = await self.device.pet_mode()
|
||||
success = await self.device.set_pet_mode()
|
||||
elif preset_mode == VS_FAN_MODE_TURBO:
|
||||
success = await self.device.turbo_mode()
|
||||
success = await self.device.set_turbo_mode()
|
||||
elif preset_mode == VS_FAN_MODE_NORMAL:
|
||||
success = await self.device.normal_mode()
|
||||
success = await self.device.set_normal_mode()
|
||||
if not success:
|
||||
raise HomeAssistantError(self.device.last_response.message)
|
||||
|
||||
|
||||
@@ -13,5 +13,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/vesync",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["pyvesync"],
|
||||
"requirements": ["pyvesync==3.0.0"]
|
||||
"requirements": ["pyvesync==3.1.0"]
|
||||
}
|
||||
|
||||
@@ -34,12 +34,13 @@ CONF_HEATING_TYPE = "heating_type"
|
||||
|
||||
DEFAULT_CACHE_DURATION = 60
|
||||
|
||||
VICARE_BAR = "bar"
|
||||
VICARE_CUBIC_METER = "cubicMeter"
|
||||
VICARE_KW = "kilowatt"
|
||||
VICARE_KWH = "kilowattHour"
|
||||
VICARE_PERCENT = "percent"
|
||||
VICARE_W = "watt"
|
||||
VICARE_KW = "kilowatt"
|
||||
VICARE_WH = "wattHour"
|
||||
VICARE_KWH = "kilowattHour"
|
||||
VICARE_CUBIC_METER = "cubicMeter"
|
||||
|
||||
|
||||
class HeatingType(enum.Enum):
|
||||
|
||||
@@ -41,6 +41,7 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import (
|
||||
VICARE_BAR,
|
||||
VICARE_CUBIC_METER,
|
||||
VICARE_KW,
|
||||
VICARE_KWH,
|
||||
@@ -62,20 +63,22 @@ from .utils import (
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
VICARE_UNIT_TO_DEVICE_CLASS = {
|
||||
VICARE_WH: SensorDeviceClass.ENERGY,
|
||||
VICARE_KWH: SensorDeviceClass.ENERGY,
|
||||
VICARE_W: SensorDeviceClass.POWER,
|
||||
VICARE_KW: SensorDeviceClass.POWER,
|
||||
VICARE_BAR: SensorDeviceClass.PRESSURE,
|
||||
VICARE_CUBIC_METER: SensorDeviceClass.GAS,
|
||||
VICARE_KW: SensorDeviceClass.POWER,
|
||||
VICARE_KWH: SensorDeviceClass.ENERGY,
|
||||
VICARE_WH: SensorDeviceClass.ENERGY,
|
||||
VICARE_W: SensorDeviceClass.POWER,
|
||||
}
|
||||
|
||||
VICARE_UNIT_TO_HA_UNIT = {
|
||||
VICARE_BAR: UnitOfPressure.BAR,
|
||||
VICARE_CUBIC_METER: UnitOfVolume.CUBIC_METERS,
|
||||
VICARE_KW: UnitOfPower.KILO_WATT,
|
||||
VICARE_KWH: UnitOfEnergy.KILO_WATT_HOUR,
|
||||
VICARE_PERCENT: PERCENTAGE,
|
||||
VICARE_W: UnitOfPower.WATT,
|
||||
VICARE_KW: UnitOfPower.KILO_WATT,
|
||||
VICARE_WH: UnitOfEnergy.WATT_HOUR,
|
||||
VICARE_KWH: UnitOfEnergy.KILO_WATT_HOUR,
|
||||
VICARE_CUBIC_METER: UnitOfVolume.CUBIC_METERS,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.httpx_client import get_async_client
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.selector import (
|
||||
SelectOptionDict,
|
||||
SelectSelector,
|
||||
@@ -69,7 +69,7 @@ class VictronRemoteMonitoringFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
"""
|
||||
client = VictronVRMClient(
|
||||
token=api_token,
|
||||
client_session=get_async_client(self.hass),
|
||||
client_session=async_get_clientsession(self.hass),
|
||||
)
|
||||
try:
|
||||
sites = await client.users.list_sites()
|
||||
@@ -86,7 +86,7 @@ class VictronRemoteMonitoringFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
"""Validate access to the selected site and return its data."""
|
||||
client = VictronVRMClient(
|
||||
token=api_token,
|
||||
client_session=get_async_client(self.hass),
|
||||
client_session=async_get_clientsession(self.hass),
|
||||
)
|
||||
try:
|
||||
site_data = await client.users.get_site(site_id)
|
||||
|
||||
@@ -11,7 +11,7 @@ from victron_vrm.utils import dt_now
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
from homeassistant.helpers.httpx_client import get_async_client
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import CONF_API_TOKEN, CONF_SITE_ID, DOMAIN, LOGGER
|
||||
@@ -26,8 +26,8 @@ class VRMForecastStore:
|
||||
"""Class to hold the forecast data."""
|
||||
|
||||
site_id: int
|
||||
solar: ForecastAggregations
|
||||
consumption: ForecastAggregations
|
||||
solar: ForecastAggregations | None
|
||||
consumption: ForecastAggregations | None
|
||||
|
||||
|
||||
async def get_forecast(client: VictronVRMClient, site_id: int) -> VRMForecastStore:
|
||||
@@ -75,7 +75,7 @@ class VictronRemoteMonitoringDataUpdateCoordinator(
|
||||
"""Initialize."""
|
||||
self.client = VictronVRMClient(
|
||||
token=config_entry.data[CONF_API_TOKEN],
|
||||
client_session=get_async_client(hass),
|
||||
client_session=async_get_clientsession(hass),
|
||||
)
|
||||
self.site_id = config_entry.data[CONF_SITE_ID]
|
||||
super().__init__(
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["victron-vrm==0.1.7"]
|
||||
"requirements": ["victron-vrm==0.1.8"]
|
||||
}
|
||||
|
||||
@@ -39,7 +39,9 @@ SENSORS: tuple[VRMForecastsSensorEntityDescription, ...] = (
|
||||
VRMForecastsSensorEntityDescription(
|
||||
key="energy_production_estimate_yesterday",
|
||||
translation_key="energy_production_estimate_yesterday",
|
||||
value_fn=lambda estimate: estimate.solar.yesterday_total,
|
||||
value_fn=lambda store: (
|
||||
store.solar.yesterday_total if store.solar is not None else None
|
||||
),
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
||||
@@ -49,7 +51,9 @@ SENSORS: tuple[VRMForecastsSensorEntityDescription, ...] = (
|
||||
VRMForecastsSensorEntityDescription(
|
||||
key="energy_production_estimate_today",
|
||||
translation_key="energy_production_estimate_today",
|
||||
value_fn=lambda estimate: estimate.solar.today_total,
|
||||
value_fn=lambda store: (
|
||||
store.solar.today_total if store.solar is not None else None
|
||||
),
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
||||
@@ -59,7 +63,9 @@ SENSORS: tuple[VRMForecastsSensorEntityDescription, ...] = (
|
||||
VRMForecastsSensorEntityDescription(
|
||||
key="energy_production_estimate_today_remaining",
|
||||
translation_key="energy_production_estimate_today_remaining",
|
||||
value_fn=lambda estimate: estimate.solar.today_left_total,
|
||||
value_fn=lambda store: (
|
||||
store.solar.today_left_total if store.solar is not None else None
|
||||
),
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
||||
@@ -69,7 +75,9 @@ SENSORS: tuple[VRMForecastsSensorEntityDescription, ...] = (
|
||||
VRMForecastsSensorEntityDescription(
|
||||
key="energy_production_estimate_tomorrow",
|
||||
translation_key="energy_production_estimate_tomorrow",
|
||||
value_fn=lambda estimate: estimate.solar.tomorrow_total,
|
||||
value_fn=lambda store: (
|
||||
store.solar.tomorrow_total if store.solar is not None else None
|
||||
),
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
||||
@@ -79,25 +87,33 @@ SENSORS: tuple[VRMForecastsSensorEntityDescription, ...] = (
|
||||
VRMForecastsSensorEntityDescription(
|
||||
key="power_highest_peak_time_yesterday",
|
||||
translation_key="power_highest_peak_time_yesterday",
|
||||
value_fn=lambda estimate: estimate.solar.yesterday_peak_time,
|
||||
value_fn=lambda store: (
|
||||
store.solar.yesterday_peak_time if store.solar is not None else None
|
||||
),
|
||||
device_class=SensorDeviceClass.TIMESTAMP,
|
||||
),
|
||||
VRMForecastsSensorEntityDescription(
|
||||
key="power_highest_peak_time_today",
|
||||
translation_key="power_highest_peak_time_today",
|
||||
value_fn=lambda estimate: estimate.solar.today_peak_time,
|
||||
value_fn=lambda store: (
|
||||
store.solar.today_peak_time if store.solar is not None else None
|
||||
),
|
||||
device_class=SensorDeviceClass.TIMESTAMP,
|
||||
),
|
||||
VRMForecastsSensorEntityDescription(
|
||||
key="power_highest_peak_time_tomorrow",
|
||||
translation_key="power_highest_peak_time_tomorrow",
|
||||
value_fn=lambda estimate: estimate.solar.tomorrow_peak_time,
|
||||
value_fn=lambda store: (
|
||||
store.solar.tomorrow_peak_time if store.solar is not None else None
|
||||
),
|
||||
device_class=SensorDeviceClass.TIMESTAMP,
|
||||
),
|
||||
VRMForecastsSensorEntityDescription(
|
||||
key="energy_production_current_hour",
|
||||
translation_key="energy_production_current_hour",
|
||||
value_fn=lambda estimate: estimate.solar.current_hour_total,
|
||||
value_fn=lambda store: (
|
||||
store.solar.current_hour_total if store.solar is not None else None
|
||||
),
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
||||
@@ -107,7 +123,9 @@ SENSORS: tuple[VRMForecastsSensorEntityDescription, ...] = (
|
||||
VRMForecastsSensorEntityDescription(
|
||||
key="energy_production_next_hour",
|
||||
translation_key="energy_production_next_hour",
|
||||
value_fn=lambda estimate: estimate.solar.next_hour_total,
|
||||
value_fn=lambda store: (
|
||||
store.solar.next_hour_total if store.solar is not None else None
|
||||
),
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
||||
@@ -118,7 +136,9 @@ SENSORS: tuple[VRMForecastsSensorEntityDescription, ...] = (
|
||||
VRMForecastsSensorEntityDescription(
|
||||
key="energy_consumption_estimate_yesterday",
|
||||
translation_key="energy_consumption_estimate_yesterday",
|
||||
value_fn=lambda estimate: estimate.consumption.yesterday_total,
|
||||
value_fn=lambda store: (
|
||||
store.consumption.yesterday_total if store.consumption is not None else None
|
||||
),
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
||||
@@ -128,7 +148,9 @@ SENSORS: tuple[VRMForecastsSensorEntityDescription, ...] = (
|
||||
VRMForecastsSensorEntityDescription(
|
||||
key="energy_consumption_estimate_today",
|
||||
translation_key="energy_consumption_estimate_today",
|
||||
value_fn=lambda estimate: estimate.consumption.today_total,
|
||||
value_fn=lambda store: (
|
||||
store.consumption.today_total if store.consumption is not None else None
|
||||
),
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
||||
@@ -138,7 +160,11 @@ SENSORS: tuple[VRMForecastsSensorEntityDescription, ...] = (
|
||||
VRMForecastsSensorEntityDescription(
|
||||
key="energy_consumption_estimate_today_remaining",
|
||||
translation_key="energy_consumption_estimate_today_remaining",
|
||||
value_fn=lambda estimate: estimate.consumption.today_left_total,
|
||||
value_fn=lambda store: (
|
||||
store.consumption.today_left_total
|
||||
if store.consumption is not None
|
||||
else None
|
||||
),
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
||||
@@ -148,7 +174,9 @@ SENSORS: tuple[VRMForecastsSensorEntityDescription, ...] = (
|
||||
VRMForecastsSensorEntityDescription(
|
||||
key="energy_consumption_estimate_tomorrow",
|
||||
translation_key="energy_consumption_estimate_tomorrow",
|
||||
value_fn=lambda estimate: estimate.consumption.tomorrow_total,
|
||||
value_fn=lambda store: (
|
||||
store.consumption.tomorrow_total if store.consumption is not None else None
|
||||
),
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
||||
@@ -158,25 +186,39 @@ SENSORS: tuple[VRMForecastsSensorEntityDescription, ...] = (
|
||||
VRMForecastsSensorEntityDescription(
|
||||
key="consumption_highest_peak_time_yesterday",
|
||||
translation_key="consumption_highest_peak_time_yesterday",
|
||||
value_fn=lambda estimate: estimate.consumption.yesterday_peak_time,
|
||||
value_fn=lambda store: (
|
||||
store.consumption.yesterday_peak_time
|
||||
if store.consumption is not None
|
||||
else None
|
||||
),
|
||||
device_class=SensorDeviceClass.TIMESTAMP,
|
||||
),
|
||||
VRMForecastsSensorEntityDescription(
|
||||
key="consumption_highest_peak_time_today",
|
||||
translation_key="consumption_highest_peak_time_today",
|
||||
value_fn=lambda estimate: estimate.consumption.today_peak_time,
|
||||
value_fn=lambda store: (
|
||||
store.consumption.today_peak_time if store.consumption is not None else None
|
||||
),
|
||||
device_class=SensorDeviceClass.TIMESTAMP,
|
||||
),
|
||||
VRMForecastsSensorEntityDescription(
|
||||
key="consumption_highest_peak_time_tomorrow",
|
||||
translation_key="consumption_highest_peak_time_tomorrow",
|
||||
value_fn=lambda estimate: estimate.consumption.tomorrow_peak_time,
|
||||
value_fn=lambda store: (
|
||||
store.consumption.tomorrow_peak_time
|
||||
if store.consumption is not None
|
||||
else None
|
||||
),
|
||||
device_class=SensorDeviceClass.TIMESTAMP,
|
||||
),
|
||||
VRMForecastsSensorEntityDescription(
|
||||
key="energy_consumption_current_hour",
|
||||
translation_key="energy_consumption_current_hour",
|
||||
value_fn=lambda estimate: estimate.consumption.current_hour_total,
|
||||
value_fn=lambda store: (
|
||||
store.consumption.current_hour_total
|
||||
if store.consumption is not None
|
||||
else None
|
||||
),
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
||||
@@ -186,7 +228,9 @@ SENSORS: tuple[VRMForecastsSensorEntityDescription, ...] = (
|
||||
VRMForecastsSensorEntityDescription(
|
||||
key="energy_consumption_next_hour",
|
||||
translation_key="energy_consumption_next_hour",
|
||||
value_fn=lambda estimate: estimate.consumption.next_hour_total,
|
||||
value_fn=lambda store: (
|
||||
store.consumption.next_hour_total if store.consumption is not None else None
|
||||
),
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
from enum import StrEnum
|
||||
|
||||
DOMAIN = "wallbox"
|
||||
UPDATE_INTERVAL = 60
|
||||
UPDATE_INTERVAL = 90
|
||||
|
||||
BIDIRECTIONAL_MODEL_PREFIXES = ["QS"]
|
||||
|
||||
|
||||
@@ -209,7 +209,12 @@ class WallboxCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
) from wallbox_connection_error
|
||||
|
||||
async def _async_update_data(self) -> dict[str, Any]:
|
||||
"""Get new sensor data for Wallbox component."""
|
||||
"""Get new sensor data for Wallbox component. Set update interval to be UPDATE_INTERVAL * #wallbox chargers configured, this is necessary due to rate limitations."""
|
||||
|
||||
self.update_interval = timedelta(
|
||||
seconds=UPDATE_INTERVAL
|
||||
* max(len(self.hass.config_entries.async_loaded_entries(DOMAIN)), 1)
|
||||
)
|
||||
return await self.hass.async_add_executor_job(self._get_data)
|
||||
|
||||
@_require_authentication
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["holidays"],
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["holidays==0.81"]
|
||||
"requirements": ["holidays==0.82"]
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ from zigpy.config import CONF_DATABASE, CONF_DEVICE, CONF_DEVICE_PATH
|
||||
from zigpy.exceptions import NetworkSettingsInconsistent, TransientConnectionError
|
||||
|
||||
from homeassistant.components.homeassistant_hardware.helpers import (
|
||||
async_is_firmware_update_in_progress,
|
||||
async_notify_firmware_info,
|
||||
async_register_firmware_info_provider,
|
||||
)
|
||||
@@ -119,6 +120,14 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
def _raise_if_port_in_use(hass: HomeAssistant, device_path: str) -> None:
|
||||
"""Ensure that the specified serial port is not in use by a firmware update."""
|
||||
if async_is_firmware_update_in_progress(hass, device_path):
|
||||
raise ConfigEntryNotReady(
|
||||
f"Firmware update in progress for device {device_path}"
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
|
||||
"""Set up ZHA.
|
||||
|
||||
@@ -152,6 +161,10 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
|
||||
|
||||
_LOGGER.debug("Trigger cache: %s", zha_lib_data.device_trigger_cache)
|
||||
|
||||
# Check if firmware update is in progress for this device
|
||||
device_path = config_entry.data[CONF_DEVICE][CONF_DEVICE_PATH]
|
||||
_raise_if_port_in_use(hass, device_path)
|
||||
|
||||
try:
|
||||
await zha_gateway.async_initialize()
|
||||
except NetworkSettingsInconsistent as exc:
|
||||
@@ -168,7 +181,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
|
||||
raise ConfigEntryNotReady from exc
|
||||
except Exception as exc:
|
||||
_LOGGER.debug("Failed to set up ZHA", exc_info=exc)
|
||||
device_path = config_entry.data[CONF_DEVICE][CONF_DEVICE_PATH]
|
||||
_raise_if_port_in_use(hass, device_path)
|
||||
|
||||
if (
|
||||
not device_path.startswith("socket://")
|
||||
|
||||
@@ -744,8 +744,11 @@ class ZhaConfigFlowHandler(BaseZhaFlow, ConfigFlow, domain=DOMAIN):
|
||||
|
||||
# Without confirmation, discovery can automatically progress into parts of the
|
||||
# config flow logic that interacts with hardware.
|
||||
# Ignore Zeroconf discoveries during onboarding, as they may be in use already.
|
||||
if user_input is not None or (
|
||||
not onboarding.async_is_onboarded(self.hass) and not zha_config_entries
|
||||
not onboarding.async_is_onboarded(self.hass)
|
||||
and not zha_config_entries
|
||||
and self.source != SOURCE_ZEROCONF
|
||||
):
|
||||
# Probe the radio type if we don't have one yet
|
||||
if self._radio_mgr.radio_type is None:
|
||||
|
||||
@@ -11,7 +11,13 @@ from typing import Any
|
||||
from propcache.api import cached_property
|
||||
from zha.mixins import LogMixin
|
||||
|
||||
from homeassistant.const import ATTR_MANUFACTURER, ATTR_MODEL, ATTR_NAME, EntityCategory
|
||||
from homeassistant.const import (
|
||||
ATTR_MANUFACTURER,
|
||||
ATTR_MODEL,
|
||||
ATTR_NAME,
|
||||
ATTR_VIA_DEVICE,
|
||||
EntityCategory,
|
||||
)
|
||||
from homeassistant.core import State, callback
|
||||
from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE, DeviceInfo
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
@@ -85,14 +91,19 @@ class ZHAEntity(LogMixin, RestoreEntity, Entity):
|
||||
ieee = zha_device_info["ieee"]
|
||||
zha_gateway = self.entity_data.device_proxy.gateway_proxy.gateway
|
||||
|
||||
return DeviceInfo(
|
||||
device_info = DeviceInfo(
|
||||
connections={(CONNECTION_ZIGBEE, ieee)},
|
||||
identifiers={(DOMAIN, ieee)},
|
||||
manufacturer=zha_device_info[ATTR_MANUFACTURER],
|
||||
model=zha_device_info[ATTR_MODEL],
|
||||
name=zha_device_info[ATTR_NAME],
|
||||
via_device=(DOMAIN, str(zha_gateway.state.node_info.ieee)),
|
||||
)
|
||||
if ieee != str(zha_gateway.state.node_info.ieee):
|
||||
device_info[ATTR_VIA_DEVICE] = (
|
||||
DOMAIN,
|
||||
str(zha_gateway.state.node_info.ieee),
|
||||
)
|
||||
return device_info
|
||||
|
||||
@callback
|
||||
def _handle_entity_events(self, event: Any) -> None:
|
||||
|
||||
@@ -956,6 +956,9 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
CONF_ADDON_LR_S2_AUTHENTICATED_KEY: self.lr_s2_authenticated_key,
|
||||
}
|
||||
)
|
||||
if self.restart_addon:
|
||||
manager = get_addon_manager(self.hass)
|
||||
await manager.async_stop_addon()
|
||||
|
||||
self._abort_if_unique_id_configured(
|
||||
updates={
|
||||
@@ -1501,41 +1504,51 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
if not is_hassio(self.hass):
|
||||
return self.async_abort(reason="not_hassio")
|
||||
|
||||
if (
|
||||
discovery_info.zwave_home_id
|
||||
and (
|
||||
current_config_entries := self._async_current_entries(
|
||||
include_ignore=False
|
||||
if discovery_info.zwave_home_id:
|
||||
if (
|
||||
(
|
||||
current_config_entries := self._async_current_entries(
|
||||
include_ignore=False
|
||||
)
|
||||
)
|
||||
)
|
||||
and (home_id := str(discovery_info.zwave_home_id))
|
||||
and (
|
||||
existing_entry := next(
|
||||
(
|
||||
entry
|
||||
for entry in current_config_entries
|
||||
if entry.unique_id == home_id
|
||||
),
|
||||
None,
|
||||
and (home_id := str(discovery_info.zwave_home_id))
|
||||
and (
|
||||
existing_entry := next(
|
||||
(
|
||||
entry
|
||||
for entry in current_config_entries
|
||||
if entry.unique_id == home_id
|
||||
),
|
||||
None,
|
||||
)
|
||||
)
|
||||
# Only update existing entries that are configured via sockets
|
||||
and existing_entry.data.get(CONF_SOCKET_PATH)
|
||||
# And use the add-on
|
||||
and existing_entry.data.get(CONF_USE_ADDON)
|
||||
):
|
||||
manager = get_addon_manager(self.hass)
|
||||
await self._async_set_addon_config(
|
||||
{CONF_ADDON_SOCKET: discovery_info.socket_path}
|
||||
)
|
||||
if self.restart_addon:
|
||||
await manager.async_stop_addon()
|
||||
self.hass.config_entries.async_update_entry(
|
||||
existing_entry,
|
||||
data={
|
||||
**existing_entry.data,
|
||||
CONF_SOCKET_PATH: discovery_info.socket_path,
|
||||
},
|
||||
)
|
||||
self.hass.config_entries.async_schedule_reload(existing_entry.entry_id)
|
||||
return self.async_abort(reason="already_configured")
|
||||
|
||||
# We are not aborting if home ID configured here, we just want to make sure that it's set
|
||||
# We will update a USB based config entry automatically in `async_step_finish_addon_setup_user`
|
||||
await self.async_set_unique_id(
|
||||
str(discovery_info.zwave_home_id), raise_on_progress=False
|
||||
)
|
||||
# Only update existing entries that are configured via sockets
|
||||
and existing_entry.data.get(CONF_SOCKET_PATH)
|
||||
# And use the add-on
|
||||
and existing_entry.data.get(CONF_USE_ADDON)
|
||||
):
|
||||
await self._async_set_addon_config(
|
||||
{CONF_ADDON_SOCKET: discovery_info.socket_path}
|
||||
)
|
||||
# Reloading will sync add-on options to config entry data
|
||||
self.hass.config_entries.async_schedule_reload(existing_entry.entry_id)
|
||||
return self.async_abort(reason="already_configured")
|
||||
|
||||
# We are not aborting if home ID configured here, we just want to make sure that it's set
|
||||
# We will update a USB based config entry automatically in `async_step_finish_addon_setup_user`
|
||||
await self.async_set_unique_id(
|
||||
str(discovery_info.zwave_home_id), raise_on_progress=False
|
||||
)
|
||||
self.socket_path = discovery_info.socket_path
|
||||
self.context["title_placeholders"] = {
|
||||
CONF_NAME: f"{discovery_info.name} via ESPHome"
|
||||
|
||||
@@ -26,7 +26,7 @@ if TYPE_CHECKING:
|
||||
APPLICATION_NAME: Final = "HomeAssistant"
|
||||
MAJOR_VERSION: Final = 2025
|
||||
MINOR_VERSION: Final = 10
|
||||
PATCH_VERSION: Final = "1"
|
||||
PATCH_VERSION: Final = "2"
|
||||
__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)
|
||||
|
||||
@@ -39,7 +39,7 @@ habluetooth==5.6.4
|
||||
hass-nabucasa==1.1.1
|
||||
hassil==3.2.0
|
||||
home-assistant-bluetooth==1.13.1
|
||||
home-assistant-frontend==20251001.0
|
||||
home-assistant-frontend==20251001.2
|
||||
home-assistant-intents==2025.10.1
|
||||
httpx==0.28.1
|
||||
ifaddr==0.2.0
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "homeassistant"
|
||||
version = "2025.10.1"
|
||||
version = "2025.10.2"
|
||||
license = "Apache-2.0"
|
||||
license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"]
|
||||
description = "Open-source home automation platform running on Python 3."
|
||||
|
||||
Generated
+19
-19
@@ -185,7 +185,7 @@ aioairzone-cloud==0.7.2
|
||||
aioairzone==1.0.1
|
||||
|
||||
# homeassistant.components.alexa_devices
|
||||
aioamazondevices==6.2.7
|
||||
aioamazondevices==6.4.0
|
||||
|
||||
# homeassistant.components.ambient_network
|
||||
# homeassistant.components.ambient_station
|
||||
@@ -217,7 +217,7 @@ aiobafi6==0.9.0
|
||||
aiobotocore==2.21.1
|
||||
|
||||
# homeassistant.components.comelit
|
||||
aiocomelit==0.12.3
|
||||
aiocomelit==1.1.1
|
||||
|
||||
# homeassistant.components.dhcp
|
||||
aiodhcpwatcher==1.2.1
|
||||
@@ -453,7 +453,7 @@ airgradient==0.9.2
|
||||
airly==1.1.0
|
||||
|
||||
# homeassistant.components.airos
|
||||
airos==0.5.4
|
||||
airos==0.5.5
|
||||
|
||||
# homeassistant.components.airthings_ble
|
||||
airthings-ble==0.9.2
|
||||
@@ -688,7 +688,7 @@ bring-api==1.1.0
|
||||
broadlink==0.19.0
|
||||
|
||||
# homeassistant.components.brother
|
||||
brother==5.1.0
|
||||
brother==5.1.1
|
||||
|
||||
# homeassistant.components.brottsplatskartan
|
||||
brottsplatskartan==1.0.5
|
||||
@@ -782,7 +782,7 @@ decora-wifi==1.4
|
||||
# decora==0.6
|
||||
|
||||
# homeassistant.components.ecovacs
|
||||
deebot-client==15.0.0
|
||||
deebot-client==15.1.0
|
||||
|
||||
# homeassistant.components.ihc
|
||||
# homeassistant.components.namecheapdns
|
||||
@@ -895,7 +895,7 @@ enocean==0.50
|
||||
enturclient==0.2.4
|
||||
|
||||
# homeassistant.components.environment_canada
|
||||
env-canada==0.11.2
|
||||
env-canada==0.11.3
|
||||
|
||||
# homeassistant.components.season
|
||||
ephem==4.1.6
|
||||
@@ -1183,10 +1183,10 @@ hole==0.9.0
|
||||
|
||||
# homeassistant.components.holiday
|
||||
# homeassistant.components.workday
|
||||
holidays==0.81
|
||||
holidays==0.82
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20251001.0
|
||||
home-assistant-frontend==20251001.2
|
||||
|
||||
# homeassistant.components.conversation
|
||||
home-assistant-intents==2025.10.1
|
||||
@@ -1273,7 +1273,7 @@ insteon-frontend-home-assistant==0.5.0
|
||||
intellifire4py==4.1.9
|
||||
|
||||
# homeassistant.components.iometer
|
||||
iometer==0.1.0
|
||||
iometer==0.2.0
|
||||
|
||||
# homeassistant.components.iotty
|
||||
iottycloud==0.3.0
|
||||
@@ -1649,7 +1649,7 @@ openwrt-luci-rpc==1.1.17
|
||||
openwrt-ubus-rpc==0.0.2
|
||||
|
||||
# homeassistant.components.opower
|
||||
opower==0.15.5
|
||||
opower==0.15.6
|
||||
|
||||
# homeassistant.components.oralb
|
||||
oralb-ble==0.17.6
|
||||
@@ -1933,10 +1933,10 @@ pycsspeechtts==1.0.8
|
||||
# pycups==2.0.4
|
||||
|
||||
# homeassistant.components.cync
|
||||
pycync==0.4.0
|
||||
pycync==0.4.1
|
||||
|
||||
# homeassistant.components.daikin
|
||||
pydaikin==2.16.0
|
||||
pydaikin==2.17.1
|
||||
|
||||
# homeassistant.components.danfoss_air
|
||||
pydanfossair==0.1.0
|
||||
@@ -2132,7 +2132,7 @@ pykwb==0.0.8
|
||||
pylacrosse==0.4
|
||||
|
||||
# homeassistant.components.lamarzocco
|
||||
pylamarzocco==2.1.1
|
||||
pylamarzocco==2.1.2
|
||||
|
||||
# homeassistant.components.lastfm
|
||||
pylast==5.1.0
|
||||
@@ -2384,7 +2384,7 @@ pysmappee==0.2.29
|
||||
pysmarlaapi==0.9.2
|
||||
|
||||
# homeassistant.components.smartthings
|
||||
pysmartthings==3.3.0
|
||||
pysmartthings==3.3.1
|
||||
|
||||
# homeassistant.components.smarty
|
||||
pysmarty2==0.10.3
|
||||
@@ -2501,7 +2501,7 @@ python-linkplay==0.2.12
|
||||
python-matter-server==8.1.0
|
||||
|
||||
# homeassistant.components.melcloud
|
||||
python-melcloud==0.1.0
|
||||
python-melcloud==0.1.2
|
||||
|
||||
# homeassistant.components.xiaomi_miio
|
||||
python-miio==0.5.12
|
||||
@@ -2541,7 +2541,7 @@ python-rabbitair==0.0.8
|
||||
python-ripple-api==0.0.3
|
||||
|
||||
# homeassistant.components.roborock
|
||||
python-roborock==2.49.1
|
||||
python-roborock==2.50.2
|
||||
|
||||
# homeassistant.components.smarttub
|
||||
python-smarttub==0.0.44
|
||||
@@ -2611,7 +2611,7 @@ pyvera==0.3.16
|
||||
pyversasense==0.0.6
|
||||
|
||||
# homeassistant.components.vesync
|
||||
pyvesync==3.0.0
|
||||
pyvesync==3.1.0
|
||||
|
||||
# homeassistant.components.vizio
|
||||
pyvizio==0.1.61
|
||||
@@ -2801,7 +2801,7 @@ sentry-sdk==1.45.1
|
||||
sfrbox-api==0.0.12
|
||||
|
||||
# homeassistant.components.sharkiq
|
||||
sharkiq==1.4.0
|
||||
sharkiq==1.4.2
|
||||
|
||||
# homeassistant.components.aquostv
|
||||
sharp_aquos_rc==0.3.2
|
||||
@@ -3089,7 +3089,7 @@ velbus-aio==2025.8.0
|
||||
venstarcolortouch==0.21
|
||||
|
||||
# homeassistant.components.victron_remote_monitoring
|
||||
victron-vrm==0.1.7
|
||||
victron-vrm==0.1.8
|
||||
|
||||
# homeassistant.components.vilfo
|
||||
vilfo-api-client==0.5.0
|
||||
|
||||
Generated
+19
-19
@@ -173,7 +173,7 @@ aioairzone-cloud==0.7.2
|
||||
aioairzone==1.0.1
|
||||
|
||||
# homeassistant.components.alexa_devices
|
||||
aioamazondevices==6.2.7
|
||||
aioamazondevices==6.4.0
|
||||
|
||||
# homeassistant.components.ambient_network
|
||||
# homeassistant.components.ambient_station
|
||||
@@ -205,7 +205,7 @@ aiobafi6==0.9.0
|
||||
aiobotocore==2.21.1
|
||||
|
||||
# homeassistant.components.comelit
|
||||
aiocomelit==0.12.3
|
||||
aiocomelit==1.1.1
|
||||
|
||||
# homeassistant.components.dhcp
|
||||
aiodhcpwatcher==1.2.1
|
||||
@@ -435,7 +435,7 @@ airgradient==0.9.2
|
||||
airly==1.1.0
|
||||
|
||||
# homeassistant.components.airos
|
||||
airos==0.5.4
|
||||
airos==0.5.5
|
||||
|
||||
# homeassistant.components.airthings_ble
|
||||
airthings-ble==0.9.2
|
||||
@@ -615,7 +615,7 @@ bring-api==1.1.0
|
||||
broadlink==0.19.0
|
||||
|
||||
# homeassistant.components.brother
|
||||
brother==5.1.0
|
||||
brother==5.1.1
|
||||
|
||||
# homeassistant.components.brottsplatskartan
|
||||
brottsplatskartan==1.0.5
|
||||
@@ -682,7 +682,7 @@ debugpy==1.8.16
|
||||
# decora==0.6
|
||||
|
||||
# homeassistant.components.ecovacs
|
||||
deebot-client==15.0.0
|
||||
deebot-client==15.1.0
|
||||
|
||||
# homeassistant.components.ihc
|
||||
# homeassistant.components.namecheapdns
|
||||
@@ -777,7 +777,7 @@ energyzero==2.1.1
|
||||
enocean==0.50
|
||||
|
||||
# homeassistant.components.environment_canada
|
||||
env-canada==0.11.2
|
||||
env-canada==0.11.3
|
||||
|
||||
# homeassistant.components.season
|
||||
ephem==4.1.6
|
||||
@@ -1032,10 +1032,10 @@ hole==0.9.0
|
||||
|
||||
# homeassistant.components.holiday
|
||||
# homeassistant.components.workday
|
||||
holidays==0.81
|
||||
holidays==0.82
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20251001.0
|
||||
home-assistant-frontend==20251001.2
|
||||
|
||||
# homeassistant.components.conversation
|
||||
home-assistant-intents==2025.10.1
|
||||
@@ -1107,7 +1107,7 @@ insteon-frontend-home-assistant==0.5.0
|
||||
intellifire4py==4.1.9
|
||||
|
||||
# homeassistant.components.iometer
|
||||
iometer==0.1.0
|
||||
iometer==0.2.0
|
||||
|
||||
# homeassistant.components.iotty
|
||||
iottycloud==0.3.0
|
||||
@@ -1405,7 +1405,7 @@ openhomedevice==2.2.0
|
||||
openwebifpy==4.3.1
|
||||
|
||||
# homeassistant.components.opower
|
||||
opower==0.15.5
|
||||
opower==0.15.6
|
||||
|
||||
# homeassistant.components.oralb
|
||||
oralb-ble==0.17.6
|
||||
@@ -1623,10 +1623,10 @@ pycsspeechtts==1.0.8
|
||||
# pycups==2.0.4
|
||||
|
||||
# homeassistant.components.cync
|
||||
pycync==0.4.0
|
||||
pycync==0.4.1
|
||||
|
||||
# homeassistant.components.daikin
|
||||
pydaikin==2.16.0
|
||||
pydaikin==2.17.1
|
||||
|
||||
# homeassistant.components.deako
|
||||
pydeako==0.6.0
|
||||
@@ -1777,7 +1777,7 @@ pykrakenapi==0.1.8
|
||||
pykulersky==0.5.8
|
||||
|
||||
# homeassistant.components.lamarzocco
|
||||
pylamarzocco==2.1.1
|
||||
pylamarzocco==2.1.2
|
||||
|
||||
# homeassistant.components.lastfm
|
||||
pylast==5.1.0
|
||||
@@ -1987,7 +1987,7 @@ pysmappee==0.2.29
|
||||
pysmarlaapi==0.9.2
|
||||
|
||||
# homeassistant.components.smartthings
|
||||
pysmartthings==3.3.0
|
||||
pysmartthings==3.3.1
|
||||
|
||||
# homeassistant.components.smarty
|
||||
pysmarty2==0.10.3
|
||||
@@ -2074,7 +2074,7 @@ python-linkplay==0.2.12
|
||||
python-matter-server==8.1.0
|
||||
|
||||
# homeassistant.components.melcloud
|
||||
python-melcloud==0.1.0
|
||||
python-melcloud==0.1.2
|
||||
|
||||
# homeassistant.components.xiaomi_miio
|
||||
python-miio==0.5.12
|
||||
@@ -2111,7 +2111,7 @@ python-pooldose==0.5.0
|
||||
python-rabbitair==0.0.8
|
||||
|
||||
# homeassistant.components.roborock
|
||||
python-roborock==2.49.1
|
||||
python-roborock==2.50.2
|
||||
|
||||
# homeassistant.components.smarttub
|
||||
python-smarttub==0.0.44
|
||||
@@ -2169,7 +2169,7 @@ pyuptimerobot==22.2.0
|
||||
pyvera==0.3.16
|
||||
|
||||
# homeassistant.components.vesync
|
||||
pyvesync==3.0.0
|
||||
pyvesync==3.1.0
|
||||
|
||||
# homeassistant.components.vizio
|
||||
pyvizio==0.1.61
|
||||
@@ -2326,7 +2326,7 @@ sentry-sdk==1.45.1
|
||||
sfrbox-api==0.0.12
|
||||
|
||||
# homeassistant.components.sharkiq
|
||||
sharkiq==1.4.0
|
||||
sharkiq==1.4.2
|
||||
|
||||
# homeassistant.components.simplefin
|
||||
simplefin4py==0.0.18
|
||||
@@ -2560,7 +2560,7 @@ velbus-aio==2025.8.0
|
||||
venstarcolortouch==0.21
|
||||
|
||||
# homeassistant.components.victron_remote_monitoring
|
||||
victron-vrm==0.1.7
|
||||
victron-vrm==0.1.8
|
||||
|
||||
# homeassistant.components.vilfo
|
||||
vilfo-api-client==0.5.0
|
||||
|
||||
@@ -3,10 +3,12 @@
|
||||
from datetime import timedelta
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from airgradient import AirGradientConnectionError
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.const import STATE_OFF, STATE_ON, Platform
|
||||
from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
@@ -67,3 +69,64 @@ async def test_update_mechanism(
|
||||
assert state.state == STATE_ON
|
||||
assert state.attributes["installed_version"] == "3.1.4"
|
||||
assert state.attributes["latest_version"] == "3.1.5"
|
||||
|
||||
|
||||
async def test_update_errors(
|
||||
hass: HomeAssistant,
|
||||
mock_airgradient_client: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test update entity errors."""
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
state = hass.states.get("update.airgradient_firmware")
|
||||
assert state.state == STATE_ON
|
||||
mock_airgradient_client.get_latest_firmware_version.side_effect = (
|
||||
AirGradientConnectionError("Boom")
|
||||
)
|
||||
|
||||
freezer.tick(timedelta(hours=1))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("update.airgradient_firmware")
|
||||
assert state.state == STATE_UNAVAILABLE
|
||||
|
||||
assert "Unable to connect to AirGradient server to check for updates" in caplog.text
|
||||
|
||||
caplog.clear()
|
||||
|
||||
freezer.tick(timedelta(hours=1))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("update.airgradient_firmware")
|
||||
assert state.state == STATE_UNAVAILABLE
|
||||
|
||||
assert (
|
||||
"Unable to connect to AirGradient server to check for updates"
|
||||
not in caplog.text
|
||||
)
|
||||
|
||||
mock_airgradient_client.get_latest_firmware_version.side_effect = None
|
||||
|
||||
freezer.tick(timedelta(hours=1))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("update.airgradient_firmware")
|
||||
assert state.state == STATE_ON
|
||||
mock_airgradient_client.get_latest_firmware_version.side_effect = (
|
||||
AirGradientConnectionError("Boom")
|
||||
)
|
||||
|
||||
freezer.tick(timedelta(hours=1))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("update.airgradient_firmware")
|
||||
assert state.state == STATE_UNAVAILABLE
|
||||
|
||||
assert "Unable to connect to AirGradient server to check for updates" in caplog.text
|
||||
|
||||
@@ -22,9 +22,21 @@ TEST_DEVICE_1 = AmazonDevice(
|
||||
entity_id="11111111-2222-3333-4444-555555555555",
|
||||
endpoint_id="G1234567890123456789012345678A",
|
||||
sensors={
|
||||
"dnd": AmazonDeviceSensor(name="dnd", value=False, error=False, scale=None),
|
||||
"dnd": AmazonDeviceSensor(
|
||||
name="dnd",
|
||||
value=False,
|
||||
error=False,
|
||||
error_msg=None,
|
||||
error_type=None,
|
||||
scale=None,
|
||||
),
|
||||
"temperature": AmazonDeviceSensor(
|
||||
name="temperature", value="22.5", error=False, scale="CELSIUS"
|
||||
name="temperature",
|
||||
value="22.5",
|
||||
error=False,
|
||||
error_msg=None,
|
||||
error_type=None,
|
||||
scale="CELSIUS",
|
||||
),
|
||||
},
|
||||
)
|
||||
@@ -46,7 +58,12 @@ TEST_DEVICE_2 = AmazonDevice(
|
||||
endpoint_id="G1234567890123456789012345678A",
|
||||
sensors={
|
||||
"temperature": AmazonDeviceSensor(
|
||||
name="temperature", value="22.5", error=False, scale="CELSIUS"
|
||||
name="temperature",
|
||||
value="22.5",
|
||||
error=False,
|
||||
error_msg=None,
|
||||
error_type=None,
|
||||
scale="CELSIUS",
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
@@ -15,11 +15,11 @@
|
||||
'sensors': dict({
|
||||
'dnd': dict({
|
||||
'__type': "<class 'aioamazondevices.api.AmazonDeviceSensor'>",
|
||||
'repr': "AmazonDeviceSensor(name='dnd', value=False, error=False, scale=None)",
|
||||
'repr': "AmazonDeviceSensor(name='dnd', value=False, error=False, error_type=None, error_msg=None, scale=None)",
|
||||
}),
|
||||
'temperature': dict({
|
||||
'__type': "<class 'aioamazondevices.api.AmazonDeviceSensor'>",
|
||||
'repr': "AmazonDeviceSensor(name='temperature', value='22.5', error=False, scale='CELSIUS')",
|
||||
'repr': "AmazonDeviceSensor(name='temperature', value='22.5', error=False, error_type=None, error_msg=None, scale='CELSIUS')",
|
||||
}),
|
||||
}),
|
||||
'serial number': 'echo_test_serial_number',
|
||||
@@ -45,11 +45,11 @@
|
||||
'sensors': dict({
|
||||
'dnd': dict({
|
||||
'__type': "<class 'aioamazondevices.api.AmazonDeviceSensor'>",
|
||||
'repr': "AmazonDeviceSensor(name='dnd', value=False, error=False, scale=None)",
|
||||
'repr': "AmazonDeviceSensor(name='dnd', value=False, error=False, error_type=None, error_msg=None, scale=None)",
|
||||
}),
|
||||
'temperature': dict({
|
||||
'__type': "<class 'aioamazondevices.api.AmazonDeviceSensor'>",
|
||||
'repr': "AmazonDeviceSensor(name='temperature', value='22.5', error=False, scale='CELSIUS')",
|
||||
'repr': "AmazonDeviceSensor(name='temperature', value='22.5', error=False, error_type=None, error_msg=None, scale='CELSIUS')",
|
||||
}),
|
||||
}),
|
||||
'serial number': 'echo_test_serial_number',
|
||||
|
||||
@@ -21,12 +21,16 @@
|
||||
'sensors': dict({
|
||||
'dnd': dict({
|
||||
'error': False,
|
||||
'error_msg': None,
|
||||
'error_type': None,
|
||||
'name': 'dnd',
|
||||
'scale': None,
|
||||
'value': False,
|
||||
}),
|
||||
'temperature': dict({
|
||||
'error': False,
|
||||
'error_msg': None,
|
||||
'error_type': None,
|
||||
'name': 'temperature',
|
||||
'scale': 'CELSIUS',
|
||||
'value': '22.5',
|
||||
@@ -63,12 +67,16 @@
|
||||
'sensors': dict({
|
||||
'dnd': dict({
|
||||
'error': False,
|
||||
'error_msg': None,
|
||||
'error_type': None,
|
||||
'name': 'dnd',
|
||||
'scale': None,
|
||||
'value': False,
|
||||
}),
|
||||
'temperature': dict({
|
||||
'error': False,
|
||||
'error_msg': None,
|
||||
'error_type': None,
|
||||
'name': 'temperature',
|
||||
'scale': 'CELSIUS',
|
||||
'value': '22.5',
|
||||
|
||||
@@ -11,10 +11,12 @@ from freezegun.api import FrozenDateTimeFactory
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.alexa_devices.const import DOMAIN
|
||||
from homeassistant.components.alexa_devices.coordinator import SCAN_INTERVAL
|
||||
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
|
||||
from homeassistant.const import STATE_ON, STATE_UNAVAILABLE, 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 . import setup_integration
|
||||
from .const import TEST_DEVICE_1, TEST_DEVICE_1_SN, TEST_DEVICE_2, TEST_DEVICE_2_SN
|
||||
@@ -137,3 +139,51 @@ async def test_dynamic_device(
|
||||
|
||||
assert (state := hass.states.get(entity_id_2))
|
||||
assert state.state == STATE_ON
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"key",
|
||||
[
|
||||
"bluetooth",
|
||||
"babyCryDetectionState",
|
||||
"beepingApplianceDetectionState",
|
||||
"coughDetectionState",
|
||||
"dogBarkDetectionState",
|
||||
"waterSoundsDetectionState",
|
||||
],
|
||||
)
|
||||
async def test_deprecated_sensor_removal(
|
||||
hass: HomeAssistant,
|
||||
mock_amazon_devices_client: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
key: str,
|
||||
) -> None:
|
||||
"""Test deprecated sensors are removed."""
|
||||
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
device = device_registry.async_get_or_create(
|
||||
config_entry_id=mock_config_entry.entry_id,
|
||||
identifiers={(DOMAIN, mock_config_entry.entry_id)},
|
||||
name=mock_config_entry.title,
|
||||
manufacturer="Amazon",
|
||||
model="Echo Dot",
|
||||
entry_type=dr.DeviceEntryType.SERVICE,
|
||||
)
|
||||
|
||||
entity = entity_registry.async_get_or_create(
|
||||
BINARY_SENSOR_DOMAIN,
|
||||
DOMAIN,
|
||||
unique_id=f"{TEST_DEVICE_1_SN}-{key}",
|
||||
device_id=device.id,
|
||||
config_entry=mock_config_entry,
|
||||
has_entity_name=True,
|
||||
)
|
||||
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
entity2 = entity_registry.async_get(entity.entity_id)
|
||||
assert entity2 is None
|
||||
|
||||
@@ -136,7 +136,12 @@ async def test_unit_of_measurement(
|
||||
TEST_DEVICE_1_SN
|
||||
].sensors = {
|
||||
sensor: AmazonDeviceSensor(
|
||||
name=sensor, value=api_value, error=False, scale=scale
|
||||
name=sensor,
|
||||
value=api_value,
|
||||
error=False,
|
||||
error_msg=None,
|
||||
error_type=None,
|
||||
scale=scale,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -161,7 +166,12 @@ async def test_sensor_unavailable(
|
||||
TEST_DEVICE_1_SN
|
||||
].sensors = {
|
||||
"illuminance": AmazonDeviceSensor(
|
||||
name="illuminance", value="800", error=True, scale=None
|
||||
name="illuminance",
|
||||
value="800",
|
||||
error=True,
|
||||
error_msg=None,
|
||||
error_type=None,
|
||||
scale=None,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -70,9 +70,21 @@ async def test_switch_dnd(
|
||||
|
||||
device_data = deepcopy(TEST_DEVICE_1)
|
||||
device_data.sensors = {
|
||||
"dnd": AmazonDeviceSensor(name="dnd", value=True, error=False, scale=None),
|
||||
"dnd": AmazonDeviceSensor(
|
||||
name="dnd",
|
||||
value=True,
|
||||
error=False,
|
||||
error_msg=None,
|
||||
error_type=None,
|
||||
scale=None,
|
||||
),
|
||||
"temperature": AmazonDeviceSensor(
|
||||
name="temperature", value="22.5", error=False, scale="CELSIUS"
|
||||
name="temperature",
|
||||
value="22.5",
|
||||
error=False,
|
||||
error_msg=None,
|
||||
error_type=None,
|
||||
scale="CELSIUS",
|
||||
),
|
||||
}
|
||||
mock_amazon_devices_client.get_devices_data.return_value = {
|
||||
@@ -94,9 +106,21 @@ async def test_switch_dnd(
|
||||
)
|
||||
|
||||
device_data.sensors = {
|
||||
"dnd": AmazonDeviceSensor(name="dnd", value=False, error=False, scale=None),
|
||||
"dnd": AmazonDeviceSensor(
|
||||
name="dnd",
|
||||
value=False,
|
||||
error=False,
|
||||
error_msg=None,
|
||||
error_type=None,
|
||||
scale=None,
|
||||
),
|
||||
"temperature": AmazonDeviceSensor(
|
||||
name="temperature", value="22.5", error=False, scale="CELSIUS"
|
||||
name="temperature",
|
||||
value="22.5",
|
||||
error=False,
|
||||
error_msg=None,
|
||||
error_type=None,
|
||||
scale="CELSIUS",
|
||||
),
|
||||
}
|
||||
mock_amazon_devices_client.get_devices_data.return_value = {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from aioamazondevices.const import SPEAKER_GROUP_FAMILY, SPEAKER_GROUP_MODEL
|
||||
from aioamazondevices.exceptions import CannotConnect, CannotRetrieveData
|
||||
import pytest
|
||||
|
||||
@@ -94,3 +95,42 @@ async def test_alexa_unique_id_migration(
|
||||
assert migrated_entity is not None
|
||||
assert migrated_entity.config_entry_id == mock_config_entry.entry_id
|
||||
assert migrated_entity.unique_id == f"{TEST_DEVICE_1_SN}-dnd"
|
||||
|
||||
|
||||
async def test_alexa_dnd_group_removal(
|
||||
hass: HomeAssistant,
|
||||
mock_amazon_devices_client: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test dnd switch is removed for Speaker Groups."""
|
||||
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
device = device_registry.async_get_or_create(
|
||||
config_entry_id=mock_config_entry.entry_id,
|
||||
identifiers={(DOMAIN, mock_config_entry.entry_id)},
|
||||
name=mock_config_entry.title,
|
||||
manufacturer="Amazon",
|
||||
model=SPEAKER_GROUP_MODEL,
|
||||
entry_type=dr.DeviceEntryType.SERVICE,
|
||||
)
|
||||
|
||||
entity = entity_registry.async_get_or_create(
|
||||
DOMAIN,
|
||||
SWITCH_DOMAIN,
|
||||
unique_id=f"{TEST_DEVICE_1_SN}-do_not_disturb",
|
||||
device_id=device.id,
|
||||
config_entry=mock_config_entry,
|
||||
has_entity_name=True,
|
||||
)
|
||||
|
||||
mock_amazon_devices_client.get_devices_data.return_value[
|
||||
TEST_DEVICE_1_SN
|
||||
].device_family = SPEAKER_GROUP_FAMILY
|
||||
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert not hass.states.get(entity.entity_id)
|
||||
|
||||
@@ -6,11 +6,12 @@ from aiocomelit import CannotAuthenticate, CannotConnect
|
||||
from aiocomelit.const import BRIDGE, VEDO
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.comelit.config_flow import InvalidPin
|
||||
from homeassistant.components.comelit.const import DOMAIN
|
||||
from homeassistant.config_entries import SOURCE_USER
|
||||
from homeassistant.const import CONF_HOST, CONF_PIN, CONF_PORT, CONF_TYPE
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType, InvalidData
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
|
||||
from .const import (
|
||||
BAD_PIN,
|
||||
@@ -97,6 +98,7 @@ async def test_flow_vedo(
|
||||
(CannotConnect, "cannot_connect"),
|
||||
(CannotAuthenticate, "invalid_auth"),
|
||||
(ConnectionResetError, "unknown"),
|
||||
(InvalidPin, "invalid_pin"),
|
||||
],
|
||||
)
|
||||
async def test_exception_connection(
|
||||
@@ -181,6 +183,7 @@ async def test_reauth_successful(
|
||||
(CannotConnect, "cannot_connect"),
|
||||
(CannotAuthenticate, "invalid_auth"),
|
||||
(ConnectionResetError, "unknown"),
|
||||
(InvalidPin, "invalid_pin"),
|
||||
],
|
||||
)
|
||||
async def test_reauth_not_successful(
|
||||
@@ -261,6 +264,7 @@ async def test_reconfigure_successful(
|
||||
(CannotConnect, "cannot_connect"),
|
||||
(CannotAuthenticate, "invalid_auth"),
|
||||
(ConnectionResetError, "unknown"),
|
||||
(InvalidPin, "invalid_pin"),
|
||||
],
|
||||
)
|
||||
async def test_reconfigure_fails(
|
||||
@@ -326,16 +330,17 @@ async def test_pin_format_serial_bridge(
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
with pytest.raises(InvalidData):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
CONF_HOST: BRIDGE_HOST,
|
||||
CONF_PORT: BRIDGE_PORT,
|
||||
CONF_PIN: BAD_PIN,
|
||||
},
|
||||
)
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
CONF_HOST: BRIDGE_HOST,
|
||||
CONF_PORT: BRIDGE_PORT,
|
||||
CONF_PIN: BAD_PIN,
|
||||
},
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
assert result["errors"] == {"base": "invalid_pin"}
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
|
||||
@@ -5,6 +5,7 @@ from unittest.mock import AsyncMock, patch
|
||||
from aiocomelit.api import ComelitSerialBridgeObject
|
||||
from aiocomelit.const import COVER, WATT
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.comelit.const import SCAN_INTERVAL
|
||||
@@ -17,14 +18,20 @@ from homeassistant.components.cover import (
|
||||
STATE_CLOSING,
|
||||
STATE_OPEN,
|
||||
STATE_OPENING,
|
||||
CoverState,
|
||||
)
|
||||
from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.core import HomeAssistant, State
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from . import setup_integration
|
||||
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform
|
||||
from tests.common import (
|
||||
MockConfigEntry,
|
||||
async_fire_time_changed,
|
||||
mock_restore_cache,
|
||||
snapshot_platform,
|
||||
)
|
||||
|
||||
ENTITY_ID = "cover.cover0"
|
||||
|
||||
@@ -162,37 +169,26 @@ async def test_cover_stop_if_stopped(
|
||||
assert state.state == STATE_UNKNOWN
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"cover_state",
|
||||
[
|
||||
CoverState.OPEN,
|
||||
CoverState.CLOSED,
|
||||
],
|
||||
)
|
||||
async def test_cover_restore_state(
|
||||
hass: HomeAssistant,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
mock_serial_bridge: AsyncMock,
|
||||
mock_serial_bridge_config_entry: MockConfigEntry,
|
||||
cover_state: CoverState,
|
||||
) -> None:
|
||||
"""Test cover restore state on reload."""
|
||||
|
||||
mock_serial_bridge.reset_mock()
|
||||
mock_restore_cache(hass, [State(ENTITY_ID, cover_state)])
|
||||
await setup_integration(hass, mock_serial_bridge_config_entry)
|
||||
|
||||
assert (state := hass.states.get(ENTITY_ID))
|
||||
assert state.state == STATE_UNKNOWN
|
||||
|
||||
# Open cover
|
||||
await hass.services.async_call(
|
||||
COVER_DOMAIN,
|
||||
SERVICE_OPEN_COVER,
|
||||
{ATTR_ENTITY_ID: ENTITY_ID},
|
||||
blocking=True,
|
||||
)
|
||||
mock_serial_bridge.set_device_status.assert_called()
|
||||
|
||||
assert (state := hass.states.get(ENTITY_ID))
|
||||
assert state.state == STATE_OPENING
|
||||
|
||||
await hass.config_entries.async_reload(mock_serial_bridge_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert (state := hass.states.get(ENTITY_ID))
|
||||
assert state.state == STATE_OPENING
|
||||
assert state.state == cover_state
|
||||
|
||||
|
||||
async def test_cover_dynamic(
|
||||
|
||||
@@ -14,7 +14,7 @@ from homeassistant.components.google_assistant_sdk import DOMAIN
|
||||
from homeassistant.components.google_assistant_sdk.const import SUPPORTED_LANGUAGE_CODES
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.core import Context, HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||
from homeassistant.setup import async_setup_component
|
||||
from homeassistant.util.dt import utcnow
|
||||
|
||||
@@ -37,19 +37,29 @@ async def test_setup_success(
|
||||
hass: HomeAssistant,
|
||||
setup_integration: ComponentSetup,
|
||||
) -> None:
|
||||
"""Test successful setup and unload."""
|
||||
"""Test successful setup, unload, and re-setup."""
|
||||
# Initial setup
|
||||
await setup_integration()
|
||||
|
||||
entries = hass.config_entries.async_entries(DOMAIN)
|
||||
assert len(entries) == 1
|
||||
assert entries[0].state is ConfigEntryState.LOADED
|
||||
assert hass.services.has_service(DOMAIN, "send_text_command")
|
||||
|
||||
await hass.config_entries.async_unload(entries[0].entry_id)
|
||||
# Unload the entry
|
||||
entry_id = entries[0].entry_id
|
||||
await hass.config_entries.async_unload(entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert not hass.data.get(DOMAIN)
|
||||
assert entries[0].state is ConfigEntryState.NOT_LOADED
|
||||
assert not hass.services.async_services().get(DOMAIN, {})
|
||||
assert hass.services.has_service(DOMAIN, "send_text_command")
|
||||
|
||||
# Re-setup the entry
|
||||
assert await hass.config_entries.async_setup(entry_id)
|
||||
await hass.async_block_till_done()
|
||||
entries = hass.config_entries.async_entries(DOMAIN)
|
||||
assert len(entries) == 1
|
||||
assert entries[0].state is ConfigEntryState.LOADED
|
||||
assert hass.services.has_service(DOMAIN, "send_text_command")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expires_at", [time.time() - 3600], ids=["expired"])
|
||||
@@ -134,6 +144,12 @@ async def test_setup_client_error(
|
||||
assert len(entries) == 1
|
||||
assert entries[0].state is ConfigEntryState.SETUP_RETRY
|
||||
|
||||
with pytest.raises(ServiceValidationError) as exc:
|
||||
await hass.services.async_call(
|
||||
DOMAIN, "send_text_command", {"command": "some command"}, blocking=True
|
||||
)
|
||||
assert exc.value.translation_key == "entry_not_loaded"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("configured_language_code", "expected_language_code"),
|
||||
|
||||
@@ -22,6 +22,9 @@ from homeassistant.components.homeassistant_hardware.firmware_config_flow import
|
||||
BaseFirmwareConfigFlow,
|
||||
BaseFirmwareOptionsFlow,
|
||||
)
|
||||
from homeassistant.components.homeassistant_hardware.helpers import (
|
||||
async_firmware_update_context,
|
||||
)
|
||||
from homeassistant.components.homeassistant_hardware.util import (
|
||||
ApplicationType,
|
||||
FirmwareInfo,
|
||||
@@ -302,18 +305,21 @@ def mock_firmware_info(
|
||||
expected_installed_firmware_type: ApplicationType,
|
||||
bootloader_reset_methods: Sequence[ResetTarget] = (),
|
||||
progress_callback: Callable[[int, int], None] | None = None,
|
||||
*,
|
||||
domain: str = "homeassistant_hardware",
|
||||
) -> FirmwareInfo:
|
||||
await asyncio.sleep(0)
|
||||
progress_callback(0, 100)
|
||||
await asyncio.sleep(0)
|
||||
progress_callback(50, 100)
|
||||
await asyncio.sleep(0)
|
||||
progress_callback(100, 100)
|
||||
async with async_firmware_update_context(hass, device, domain):
|
||||
await asyncio.sleep(0)
|
||||
progress_callback(0, 100)
|
||||
await asyncio.sleep(0)
|
||||
progress_callback(50, 100)
|
||||
await asyncio.sleep(0)
|
||||
progress_callback(100, 100)
|
||||
|
||||
if flashed_firmware_info is None:
|
||||
raise HomeAssistantError("Failed to probe the firmware after flashing")
|
||||
if flashed_firmware_info is None:
|
||||
raise HomeAssistantError("Failed to probe the firmware after flashing")
|
||||
|
||||
return flashed_firmware_info
|
||||
return flashed_firmware_info
|
||||
|
||||
with (
|
||||
patch(
|
||||
|
||||
@@ -7,9 +7,13 @@ import pytest
|
||||
|
||||
from homeassistant.components.homeassistant_hardware.const import DATA_COMPONENT
|
||||
from homeassistant.components.homeassistant_hardware.helpers import (
|
||||
async_firmware_update_context,
|
||||
async_is_firmware_update_in_progress,
|
||||
async_notify_firmware_info,
|
||||
async_register_firmware_info_callback,
|
||||
async_register_firmware_info_provider,
|
||||
async_register_firmware_update_in_progress,
|
||||
async_unregister_firmware_update_in_progress,
|
||||
)
|
||||
from homeassistant.components.homeassistant_hardware.util import (
|
||||
ApplicationType,
|
||||
@@ -183,3 +187,73 @@ async def test_dispatcher_callback_error_handling(
|
||||
|
||||
assert callback1.mock_calls == [call(FIRMWARE_INFO_EZSP)]
|
||||
assert callback2.mock_calls == [call(FIRMWARE_INFO_EZSP)]
|
||||
|
||||
|
||||
async def test_firmware_update_tracking(hass: HomeAssistant) -> None:
|
||||
"""Test firmware update tracking API."""
|
||||
await async_setup_component(hass, "homeassistant_hardware", {})
|
||||
|
||||
device_path = "/dev/ttyUSB0"
|
||||
|
||||
assert not async_is_firmware_update_in_progress(hass, device_path)
|
||||
|
||||
# Register an update in progress
|
||||
async_register_firmware_update_in_progress(hass, device_path, "zha")
|
||||
assert async_is_firmware_update_in_progress(hass, device_path)
|
||||
|
||||
with pytest.raises(ValueError, match="Firmware update already in progress"):
|
||||
async_register_firmware_update_in_progress(hass, device_path, "skyconnect")
|
||||
|
||||
assert async_is_firmware_update_in_progress(hass, device_path)
|
||||
|
||||
# Unregister the update with correct domain
|
||||
async_unregister_firmware_update_in_progress(hass, device_path, "zha")
|
||||
assert not async_is_firmware_update_in_progress(hass, device_path)
|
||||
|
||||
# Test unregistering with wrong domain should raise an error
|
||||
async_register_firmware_update_in_progress(hass, device_path, "zha")
|
||||
with pytest.raises(ValueError, match="is owned by zha, not skyconnect"):
|
||||
async_unregister_firmware_update_in_progress(hass, device_path, "skyconnect")
|
||||
|
||||
# Still registered to zha
|
||||
assert async_is_firmware_update_in_progress(hass, device_path)
|
||||
async_unregister_firmware_update_in_progress(hass, device_path, "zha")
|
||||
assert not async_is_firmware_update_in_progress(hass, device_path)
|
||||
|
||||
|
||||
async def test_firmware_update_context_manager(hass: HomeAssistant) -> None:
|
||||
"""Test firmware update progress context manager."""
|
||||
await async_setup_component(hass, "homeassistant_hardware", {})
|
||||
|
||||
device_path = "/dev/ttyUSB0"
|
||||
|
||||
# Initially no updates in progress
|
||||
assert not async_is_firmware_update_in_progress(hass, device_path)
|
||||
|
||||
# Test successful completion
|
||||
async with async_firmware_update_context(hass, device_path, "zha"):
|
||||
assert async_is_firmware_update_in_progress(hass, device_path)
|
||||
|
||||
# Should be cleaned up after context
|
||||
assert not async_is_firmware_update_in_progress(hass, device_path)
|
||||
|
||||
# Test exception handling
|
||||
with pytest.raises(ValueError, match="test error"): # noqa: PT012
|
||||
async with async_firmware_update_context(hass, device_path, "zha"):
|
||||
assert async_is_firmware_update_in_progress(hass, device_path)
|
||||
raise ValueError("test error")
|
||||
|
||||
# Should still be cleaned up after exception
|
||||
assert not async_is_firmware_update_in_progress(hass, device_path)
|
||||
|
||||
# Test concurrent context manager attempts should fail
|
||||
async with async_firmware_update_context(hass, device_path, "zha"):
|
||||
assert async_is_firmware_update_in_progress(hass, device_path)
|
||||
|
||||
# Second context manager should fail to register
|
||||
with pytest.raises(ValueError, match="Firmware update already in progress"):
|
||||
async with async_firmware_update_context(hass, device_path, "skyconnect"):
|
||||
pytest.fail("We should not enter this context manager")
|
||||
|
||||
# Should be cleaned up after first context
|
||||
assert not async_is_firmware_update_in_progress(hass, device_path)
|
||||
|
||||
@@ -364,6 +364,8 @@ async def test_update_entity_installation(
|
||||
expected_installed_firmware_type: ApplicationType,
|
||||
bootloader_reset_methods: Sequence[ResetTarget] = (),
|
||||
progress_callback: Callable[[int, int], None] | None = None,
|
||||
*,
|
||||
domain: str = "homeassistant_hardware",
|
||||
) -> FirmwareInfo:
|
||||
await asyncio.sleep(0)
|
||||
progress_callback(0, 100)
|
||||
|
||||
@@ -537,6 +537,8 @@ async def test_probe_silabs_firmware_type(
|
||||
|
||||
async def test_async_flash_silabs_firmware(hass: HomeAssistant) -> None:
|
||||
"""Test async_flash_silabs_firmware."""
|
||||
await async_setup_component(hass, "homeassistant_hardware", {})
|
||||
|
||||
owner1 = create_mock_owner()
|
||||
owner2 = create_mock_owner()
|
||||
|
||||
@@ -625,6 +627,8 @@ async def test_async_flash_silabs_firmware(hass: HomeAssistant) -> None:
|
||||
|
||||
async def test_async_flash_silabs_firmware_flash_failure(hass: HomeAssistant) -> None:
|
||||
"""Test async_flash_silabs_firmware flash failure."""
|
||||
await async_setup_component(hass, "homeassistant_hardware", {})
|
||||
|
||||
owner1 = create_mock_owner()
|
||||
owner2 = create_mock_owner()
|
||||
|
||||
@@ -679,6 +683,8 @@ async def test_async_flash_silabs_firmware_flash_failure(hass: HomeAssistant) ->
|
||||
|
||||
async def test_async_flash_silabs_firmware_probe_failure(hass: HomeAssistant) -> None:
|
||||
"""Test async_flash_silabs_firmware probe failure."""
|
||||
await async_setup_component(hass, "homeassistant_hardware", {})
|
||||
|
||||
owner1 = create_mock_owner()
|
||||
owner2 = create_mock_owner()
|
||||
|
||||
|
||||
@@ -75,6 +75,7 @@ CONFIG_WITH_STATES = {
|
||||
"state_opening": "opening",
|
||||
"state_unlocked": "unlocked",
|
||||
"state_unlocking": "unlocking",
|
||||
"state_jammed": "jammed",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -89,6 +90,7 @@ CONFIG_WITH_STATES = {
|
||||
(CONFIG_WITH_STATES, "opening", LockState.OPENING),
|
||||
(CONFIG_WITH_STATES, "unlocked", LockState.UNLOCKED),
|
||||
(CONFIG_WITH_STATES, "unlocking", LockState.UNLOCKING),
|
||||
(CONFIG_WITH_STATES, "jammed", LockState.JAMMED),
|
||||
],
|
||||
)
|
||||
async def test_controlling_state_via_topic(
|
||||
@@ -111,6 +113,12 @@ async def test_controlling_state_via_topic(
|
||||
state = hass.states.get("lock.test")
|
||||
assert state.state == lock_state
|
||||
|
||||
async_fire_mqtt_message(hass, "state-topic", "None")
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("lock.test")
|
||||
assert state.state == STATE_UNKNOWN
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("hass_config", "payload", "lock_state"),
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user