Compare commits

..

24 Commits

Author SHA1 Message Date
abmantis
b0d69533eb Update TriggerActionType 2025-10-08 12:04:59 +01:00
abmantis
bfa2f8098e Clarify comment 2025-10-08 11:33:36 +01:00
abmantis
0ebd210252 Use type for new action; move description arg 2025-10-07 16:46:10 +01:00
abmantis
6027b697c9 Fix return type 2025-10-06 22:13:22 +01:00
abmantis
7a86007083 Add tests for different action type 2025-10-06 22:08:46 +01:00
abmantis
a6e21a54a3 Wrap action so it is always a coroutine function 2025-10-06 22:08:01 +01:00
abmantis
8b3cb69b79 Merge branch 'dev' of github.com:home-assistant/core into trigger_action_ux 2025-10-06 18:49:51 +01:00
abmantis
f296a215e7 Use hass from init 2025-10-02 17:50:34 +01:00
abmantis
4b5fd38849 Merge branch 'dev' of github.com:home-assistant/core into trigger_action_ux 2025-10-02 17:25:00 +01:00
abmantis
308f6eb5a8 Improve doc 2025-10-02 16:22:17 +01:00
abmantis
82f1ae3519 Rename class; add doc 2025-10-02 16:19:41 +01:00
abmantis
b8660b4248 Rename callback type 2025-10-01 22:12:51 +01:00
abmantis
fba50af1c3 Replace wrapper with builder method 2025-10-01 22:04:34 +01:00
abmantis
bdd448fbe0 Allow overriding trigger runner helper 2025-10-01 16:24:50 +01:00
abmantis
c3f45d594b Return future from runner 2025-09-28 12:59:16 +01:00
Abílio Costa
a95af1a40e Merge branch 'dev' into trigger_action_ux 2025-09-27 15:08:54 +01:00
abmantis
fa863649fa Merge branch 'trigger_action_ux' of github.com:home-assistant/core into trigger_action_ux 2025-09-25 15:41:20 +01:00
abmantis
b7c6e21707 Merge branch 'dev' of github.com:home-assistant/core into trigger_action_ux 2025-09-25 15:13:55 +01:00
Abílio Costa
e7da1250ba Merge branch 'dev' into trigger_action_ux 2025-09-24 22:01:19 +01:00
abmantis
e71140e09b Fix typing 2025-09-24 17:11:45 +01:00
abmantis
53875f7188 Fix zwavejs device_trigger 2025-09-24 14:39:46 +01:00
abmantis
526541f666 Merge branch 'dev' of github.com:home-assistant/core into trigger_action_ux 2025-09-24 14:37:52 +01:00
abmantis
01d81f8980 Move attach_trigger out of Trigger class 2025-09-23 16:30:22 +01:00
abmantis
7d96a814f9 Simplify firing of trigger actions 2025-09-22 19:16:30 +01:00
76 changed files with 818 additions and 1843 deletions

View File

@@ -1,9 +1,6 @@
{
"entity": {
"sensor": {
"air_quality": {
"default": "mdi:air-filter"
},
"cloud_ceiling": {
"default": "mdi:weather-fog"
},
@@ -37,6 +34,9 @@
"thunderstorm_probability_night": {
"default": "mdi:weather-lightning"
},
"translation_key": {
"default": "mdi:air-filter"
},
"tree_pollen": {
"default": "mdi:tree-outline"
},

View File

@@ -1,9 +1,7 @@
"""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
@@ -15,7 +13,6 @@ from .entity import AirGradientEntity
PARALLEL_UPDATES = 1
SCAN_INTERVAL = timedelta(hours=1)
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(
@@ -34,7 +31,6 @@ 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."""
@@ -51,27 +47,10 @@ 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."""
try:
self._attr_latest_version = (
await self.coordinator.client.get_latest_firmware_version(
self.coordinator.serial_number
)
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
)

View File

@@ -15,7 +15,6 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .coordinator import ComelitConfigEntry, ComelitVedoSystem
from .utils import DeviceType, new_device_listener
# Coordinator is used to centralize the data updates
PARALLEL_UPDATES = 0
@@ -30,19 +29,23 @@ async def async_setup_entry(
coordinator = cast(ComelitVedoSystem, config_entry.runtime_data)
def _add_new_entities(new_devices: list[DeviceType], dev_type: str) -> None:
"""Add entities for new monitors."""
entities = [
ComelitVedoBinarySensorEntity(coordinator, device, config_entry.entry_id)
for device in coordinator.data["alarm_zones"].values()
if device in new_devices
]
if entities:
async_add_entities(entities)
known_devices: set[int] = set()
config_entry.async_on_unload(
new_device_listener(coordinator, _add_new_entities, "alarm_zones")
)
def _check_device() -> None:
current_devices = set(coordinator.data["alarm_zones"])
new_devices = current_devices - known_devices
if new_devices:
known_devices.update(new_devices)
async_add_entities(
ComelitVedoBinarySensorEntity(
coordinator, device, config_entry.entry_id
)
for device in coordinator.data["alarm_zones"].values()
if device.index in new_devices
)
_check_device()
config_entry.async_on_unload(coordinator.async_add_listener(_check_device))
class ComelitVedoBinarySensorEntity(

View File

@@ -7,21 +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 (
STATE_CLOSED,
STATE_CLOSING,
STATE_OPEN,
STATE_OPENING,
CoverDeviceClass,
CoverEntity,
)
from homeassistant.components.cover import CoverDeviceClass, CoverEntity
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.restore_state import RestoreEntity
from .coordinator import ComelitConfigEntry, ComelitSerialBridge
from .entity import ComelitBridgeBaseEntity
from .utils import DeviceType, bridge_api_call, new_device_listener
from .utils import bridge_api_call
# Coordinator is used to centralize the data updates
PARALLEL_UPDATES = 0
@@ -36,19 +29,21 @@ async def async_setup_entry(
coordinator = cast(ComelitSerialBridge, config_entry.runtime_data)
def _add_new_entities(new_devices: list[DeviceType], dev_type: str) -> None:
"""Add entities for new monitors."""
entities = [
ComelitCoverEntity(coordinator, device, config_entry.entry_id)
for device in coordinator.data[dev_type].values()
if device in new_devices
]
if entities:
async_add_entities(entities)
known_devices: set[int] = set()
config_entry.async_on_unload(
new_device_listener(coordinator, _add_new_entities, COVER)
)
def _check_device() -> None:
current_devices = set(coordinator.data[COVER])
new_devices = current_devices - known_devices
if new_devices:
known_devices.update(new_devices)
async_add_entities(
ComelitCoverEntity(coordinator, device, config_entry.entry_id)
for device in coordinator.data[COVER].values()
if device.index in new_devices
)
_check_device()
config_entry.async_on_unload(coordinator.async_add_listener(_check_device))
class ComelitCoverEntity(ComelitBridgeBaseEntity, RestoreEntity, CoverEntity):
@@ -67,6 +62,7 @@ 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."""
@@ -102,6 +98,7 @@ 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()
@@ -127,10 +124,5 @@ class ComelitCoverEntity(ComelitBridgeBaseEntity, RestoreEntity, CoverEntity):
await super().async_added_to_hass()
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
if last_state := await self.async_get_last_state():
self._last_state = last_state.state

View File

@@ -12,7 +12,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import ComelitConfigEntry, ComelitSerialBridge
from .entity import ComelitBridgeBaseEntity
from .utils import DeviceType, bridge_api_call, new_device_listener
from .utils import bridge_api_call
# Coordinator is used to centralize the data updates
PARALLEL_UPDATES = 0
@@ -27,19 +27,21 @@ async def async_setup_entry(
coordinator = cast(ComelitSerialBridge, config_entry.runtime_data)
def _add_new_entities(new_devices: list[DeviceType], dev_type: str) -> None:
"""Add entities for new monitors."""
entities = [
ComelitLightEntity(coordinator, device, config_entry.entry_id)
for device in coordinator.data[dev_type].values()
if device in new_devices
]
if entities:
async_add_entities(entities)
known_devices: set[int] = set()
config_entry.async_on_unload(
new_device_listener(coordinator, _add_new_entities, LIGHT)
)
def _check_device() -> None:
current_devices = set(coordinator.data[LIGHT])
new_devices = current_devices - known_devices
if new_devices:
known_devices.update(new_devices)
async_add_entities(
ComelitLightEntity(coordinator, device, config_entry.entry_id)
for device in coordinator.data[LIGHT].values()
if device.index in new_devices
)
_check_device()
config_entry.async_on_unload(coordinator.async_add_listener(_check_device))
class ComelitLightEntity(ComelitBridgeBaseEntity, LightEntity):

View File

@@ -20,7 +20,6 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .coordinator import ComelitConfigEntry, ComelitSerialBridge, ComelitVedoSystem
from .entity import ComelitBridgeBaseEntity
from .utils import DeviceType, new_device_listener
# Coordinator is used to centralize the data updates
PARALLEL_UPDATES = 0
@@ -66,22 +65,24 @@ async def async_setup_bridge_entry(
coordinator = cast(ComelitSerialBridge, config_entry.runtime_data)
def _add_new_entities(new_devices: list[DeviceType], dev_type: str) -> None:
"""Add entities for new monitors."""
entities = [
ComelitBridgeSensorEntity(
coordinator, device, config_entry.entry_id, sensor_desc
)
for sensor_desc in SENSOR_BRIDGE_TYPES
for device in coordinator.data[dev_type].values()
if device in new_devices
]
if entities:
async_add_entities(entities)
known_devices: set[int] = set()
config_entry.async_on_unload(
new_device_listener(coordinator, _add_new_entities, OTHER)
)
def _check_device() -> None:
current_devices = set(coordinator.data[OTHER])
new_devices = current_devices - known_devices
if new_devices:
known_devices.update(new_devices)
async_add_entities(
ComelitBridgeSensorEntity(
coordinator, device, config_entry.entry_id, sensor_desc
)
for sensor_desc in SENSOR_BRIDGE_TYPES
for device in coordinator.data[OTHER].values()
if device.index in new_devices
)
_check_device()
config_entry.async_on_unload(coordinator.async_add_listener(_check_device))
async def async_setup_vedo_entry(
@@ -93,22 +94,24 @@ async def async_setup_vedo_entry(
coordinator = cast(ComelitVedoSystem, config_entry.runtime_data)
def _add_new_entities(new_devices: list[DeviceType], dev_type: str) -> None:
"""Add entities for new monitors."""
entities = [
ComelitVedoSensorEntity(
coordinator, device, config_entry.entry_id, sensor_desc
)
for sensor_desc in SENSOR_VEDO_TYPES
for device in coordinator.data["alarm_zones"].values()
if device in new_devices
]
if entities:
async_add_entities(entities)
known_devices: set[int] = set()
config_entry.async_on_unload(
new_device_listener(coordinator, _add_new_entities, "alarm_zones")
)
def _check_device() -> None:
current_devices = set(coordinator.data["alarm_zones"])
new_devices = current_devices - known_devices
if new_devices:
known_devices.update(new_devices)
async_add_entities(
ComelitVedoSensorEntity(
coordinator, device, config_entry.entry_id, sensor_desc
)
for sensor_desc in SENSOR_VEDO_TYPES
for device in coordinator.data["alarm_zones"].values()
if device.index in new_devices
)
_check_device()
config_entry.async_on_unload(coordinator.async_add_listener(_check_device))
class ComelitBridgeSensorEntity(ComelitBridgeBaseEntity, SensorEntity):

View File

@@ -13,7 +13,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import ComelitConfigEntry, ComelitSerialBridge
from .entity import ComelitBridgeBaseEntity
from .utils import DeviceType, bridge_api_call, new_device_listener
from .utils import bridge_api_call
# Coordinator is used to centralize the data updates
PARALLEL_UPDATES = 0
@@ -28,20 +28,35 @@ async def async_setup_entry(
coordinator = cast(ComelitSerialBridge, config_entry.runtime_data)
def _add_new_entities(new_devices: list[DeviceType], dev_type: str) -> None:
"""Add entities for new monitors."""
entities = [
ComelitSwitchEntity(coordinator, device, config_entry.entry_id)
for device in coordinator.data[dev_type].values()
if device in new_devices
]
if entities:
async_add_entities(entities)
entities: list[ComelitSwitchEntity] = []
entities.extend(
ComelitSwitchEntity(coordinator, device, config_entry.entry_id)
for device in coordinator.data[IRRIGATION].values()
)
entities.extend(
ComelitSwitchEntity(coordinator, device, config_entry.entry_id)
for device in coordinator.data[OTHER].values()
)
async_add_entities(entities)
for dev_type in (IRRIGATION, OTHER):
config_entry.async_on_unload(
new_device_listener(coordinator, _add_new_entities, dev_type)
)
known_devices: dict[str, set[int]] = {
dev_type: set() for dev_type in (IRRIGATION, OTHER)
}
def _check_device() -> None:
for dev_type in (IRRIGATION, OTHER):
current_devices = set(coordinator.data[dev_type])
new_devices = current_devices - known_devices[dev_type]
if new_devices:
known_devices[dev_type].update(new_devices)
async_add_entities(
ComelitSwitchEntity(coordinator, device, config_entry.entry_id)
for device in coordinator.data[dev_type].values()
if device.index in new_devices
)
_check_device()
config_entry.async_on_unload(coordinator.async_add_listener(_check_device))
class ComelitSwitchEntity(ComelitBridgeBaseEntity, SwitchEntity):

View File

@@ -4,11 +4,7 @@ from collections.abc import Awaitable, Callable, Coroutine
from functools import wraps
from typing import Any, Concatenate
from aiocomelit.api import (
ComelitSerialBridgeObject,
ComelitVedoAreaObject,
ComelitVedoZoneObject,
)
from aiocomelit import ComelitSerialBridgeObject
from aiocomelit.exceptions import CannotAuthenticate, CannotConnect, CannotRetrieveData
from aiohttp import ClientSession, CookieJar
@@ -23,11 +19,8 @@ from homeassistant.helpers import (
)
from .const import _LOGGER, DOMAIN
from .coordinator import ComelitBaseCoordinator
from .entity import ComelitBridgeBaseEntity
DeviceType = ComelitSerialBridgeObject | ComelitVedoAreaObject | ComelitVedoZoneObject
async def async_client_session(hass: HomeAssistant) -> ClientSession:
"""Return a new aiohttp session."""
@@ -120,41 +113,3 @@ def bridge_api_call[_T: ComelitBridgeBaseEntity, **_P](
self.coordinator.config_entry.async_start_reauth(self.hass)
return cmd_wrapper
def new_device_listener(
coordinator: ComelitBaseCoordinator,
new_devices_callback: Callable[
[
list[
ComelitSerialBridgeObject
| ComelitVedoAreaObject
| ComelitVedoZoneObject
],
str,
],
None,
],
data_type: str,
) -> Callable[[], None]:
"""Subscribe to coordinator updates to check for new devices."""
known_devices: set[int] = set()
def _check_devices() -> None:
"""Check for new devices and call callback with any new monitors."""
if not coordinator.data:
return
new_devices: list[DeviceType] = []
for _id in coordinator.data[data_type]:
if _id not in known_devices:
known_devices.add(_id)
new_devices.append(coordinator.data[data_type][_id])
if new_devices:
new_devices_callback(new_devices, data_type)
# Check for devices immediately
_check_devices()
return coordinator.async_add_listener(_check_devices)

View File

@@ -45,18 +45,13 @@ from home_assistant_intents import (
)
import yaml
from homeassistant import core
from homeassistant.components.homeassistant.exposed_entities import (
async_listen_entity_updates,
async_should_expose,
)
from homeassistant.const import EVENT_STATE_CHANGED, MATCH_ALL
from homeassistant.core import (
Event,
EventStateChangedData,
HomeAssistant,
State,
callback,
)
from homeassistant.core import Event, callback
from homeassistant.helpers import (
area_registry as ar,
device_registry as dr,
@@ -197,7 +192,7 @@ class IntentCache:
async def async_setup_default_agent(
hass: HomeAssistant,
hass: core.HomeAssistant,
entity_component: EntityComponent[ConversationEntity],
config_intents: dict[str, Any],
) -> None:
@@ -206,13 +201,15 @@ async def async_setup_default_agent(
await entity_component.async_add_entities([agent])
await get_agent_manager(hass).async_setup_default_agent(agent)
@callback
def async_entity_state_listener(event: Event[EventStateChangedData]) -> None:
@core.callback
def async_entity_state_listener(
event: core.Event[core.EventStateChangedData],
) -> None:
"""Set expose flag on new entities."""
async_should_expose(hass, DOMAIN, event.data["entity_id"])
@callback
def async_hass_started(hass: HomeAssistant) -> None:
@core.callback
def async_hass_started(hass: core.HomeAssistant) -> None:
"""Set expose flag on all entities."""
for state in hass.states.async_all():
async_should_expose(hass, DOMAIN, state.entity_id)
@@ -227,7 +224,9 @@ class DefaultAgent(ConversationEntity):
_attr_name = "Home Assistant"
_attr_supported_features = ConversationEntityFeature.CONTROL
def __init__(self, hass: HomeAssistant, config_intents: dict[str, Any]) -> None:
def __init__(
self, hass: core.HomeAssistant, config_intents: dict[str, Any]
) -> None:
"""Initialize the default agent."""
self.hass = hass
self._lang_intents: dict[str, LanguageIntents | object] = {}
@@ -260,7 +259,7 @@ class DefaultAgent(ConversationEntity):
"""Return a list of supported languages."""
return get_languages()
@callback
@core.callback
def _filter_entity_registry_changes(
self, event_data: er.EventEntityRegistryUpdatedData
) -> bool:
@@ -269,12 +268,12 @@ class DefaultAgent(ConversationEntity):
field in event_data["changes"] for field in _ENTITY_REGISTRY_UPDATE_FIELDS
)
@callback
def _filter_state_changes(self, event_data: EventStateChangedData) -> bool:
@core.callback
def _filter_state_changes(self, event_data: core.EventStateChangedData) -> bool:
"""Filter state changed events."""
return not event_data["old_state"] or not event_data["new_state"]
@callback
@core.callback
def _listen_clear_slot_list(self) -> None:
"""Listen for changes that can invalidate slot list."""
assert self._unsub_clear_slot_list is None
@@ -891,7 +890,7 @@ class DefaultAgent(ConversationEntity):
) -> str:
# Get first matched or unmatched state.
# This is available in the response template as "state".
state1: State | None = None
state1: core.State | None = None
if intent_response.matched_states:
state1 = intent_response.matched_states[0]
elif intent_response.unmatched_states:
@@ -1590,7 +1589,7 @@ def _get_unmatched_response(result: RecognizeResult) -> tuple[ErrorKey, dict[str
def _get_match_error_response(
hass: HomeAssistant,
hass: core.HomeAssistant,
match_error: intent.MatchFailedError,
) -> tuple[ErrorKey, dict[str, Any]]:
"""Return key and template arguments for error when target matching fails."""

View File

@@ -116,10 +116,6 @@ class WaterSourceType(TypedDict):
# an EnergyCostSensor will be automatically created
stat_cost: str | None
# An optional statistic_id identifying a device
# that includes this device's consumption in its total
included_in_stat: str | None
# Used to generate costs if stat_cost is set to None
entity_energy_price: str | None # entity_id of an entity providing price ($/m³)
number_energy_price: float | None # Price for energy ($/m³)

View File

@@ -41,12 +41,16 @@ class IdasenDeskCoordinator(DataUpdateCoordinator[int | None]):
self._expected_connected = False
self._height: int | None = None
@callback
def async_update_data() -> None:
self.async_set_updated_data(self._height)
self._debouncer = Debouncer(
hass=self.hass,
logger=_LOGGER,
cooldown=UPDATE_DEBOUNCE_TIME,
immediate=True,
function=callback(lambda: self.async_set_updated_data(self._height)),
function=async_update_data,
)
async def async_connect(self) -> bool:

View File

@@ -3,7 +3,6 @@
from __future__ import annotations
from collections.abc import Mapping
from dataclasses import dataclass
import logging
from typing import Any, cast
@@ -24,13 +23,7 @@ from homeassistant.helpers.config_entry_oauth2_flow import (
from . import async_get_config_entry_implementation
from .application_credentials import authorization_server_context
from .const import (
CONF_ACCESS_TOKEN,
CONF_AUTHORIZATION_URL,
CONF_SCOPE,
CONF_TOKEN_URL,
DOMAIN,
)
from .const import CONF_ACCESS_TOKEN, CONF_AUTHORIZATION_URL, CONF_TOKEN_URL, DOMAIN
from .coordinator import TokenManager, mcp_client
_LOGGER = logging.getLogger(__name__)
@@ -48,17 +41,9 @@ MCP_DISCOVERY_HEADERS = {
}
@dataclass
class OAuthConfig:
"""Class to hold OAuth configuration."""
authorization_server: AuthorizationServer
scopes: list[str] | None = None
async def async_discover_oauth_config(
hass: HomeAssistant, mcp_server_url: str
) -> OAuthConfig:
) -> AuthorizationServer:
"""Discover the OAuth configuration for the MCP server.
This implements the functionality in the MCP spec for discovery. If the MCP server URL
@@ -80,11 +65,9 @@ async def async_discover_oauth_config(
except httpx.HTTPStatusError as error:
if error.response.status_code == 404:
_LOGGER.info("Authorization Server Metadata not found, using default paths")
return OAuthConfig(
authorization_server=AuthorizationServer(
authorize_url=str(parsed_url.with_path("/authorize")),
token_url=str(parsed_url.with_path("/token")),
)
return AuthorizationServer(
authorize_url=str(parsed_url.with_path("/authorize")),
token_url=str(parsed_url.with_path("/token")),
)
raise CannotConnect from error
except httpx.HTTPError as error:
@@ -98,15 +81,9 @@ async def async_discover_oauth_config(
authorize_url = str(parsed_url.with_path(authorize_url))
if token_url.startswith("/"):
token_url = str(parsed_url.with_path(token_url))
# We have no way to know the minimum set of scopes needed, so request
# all of them and let the user limit during the authorization step.
scopes = data.get("scopes_supported")
return OAuthConfig(
authorization_server=AuthorizationServer(
authorize_url=authorize_url,
token_url=token_url,
),
scopes=scopes,
return AuthorizationServer(
authorize_url=authorize_url,
token_url=token_url,
)
@@ -153,7 +130,6 @@ class ModelContextProtocolConfigFlow(AbstractOAuth2FlowHandler, domain=DOMAIN):
"""Initialize the config flow."""
super().__init__()
self.data: dict[str, Any] = {}
self.oauth_config: OAuthConfig | None = None
async def async_step_user(
self, user_input: dict[str, Any] | None = None
@@ -194,7 +170,7 @@ class ModelContextProtocolConfigFlow(AbstractOAuth2FlowHandler, domain=DOMAIN):
to find the OAuth medata then run the OAuth authentication flow.
"""
try:
oauth_config = await async_discover_oauth_config(
authorization_server = await async_discover_oauth_config(
self.hass, self.data[CONF_URL]
)
except TimeoutConnectError:
@@ -205,13 +181,11 @@ class ModelContextProtocolConfigFlow(AbstractOAuth2FlowHandler, domain=DOMAIN):
_LOGGER.exception("Unexpected exception")
return self.async_abort(reason="unknown")
else:
_LOGGER.info("OAuth configuration: %s", oauth_config)
self.oauth_config = oauth_config
_LOGGER.info("OAuth configuration: %s", authorization_server)
self.data.update(
{
CONF_AUTHORIZATION_URL: oauth_config.authorization_server.authorize_url,
CONF_TOKEN_URL: oauth_config.authorization_server.token_url,
CONF_SCOPE: oauth_config.scopes,
CONF_AUTHORIZATION_URL: authorization_server.authorize_url,
CONF_TOKEN_URL: authorization_server.token_url,
}
)
return await self.async_step_credentials_choice()
@@ -223,15 +197,6 @@ class ModelContextProtocolConfigFlow(AbstractOAuth2FlowHandler, domain=DOMAIN):
self.data[CONF_TOKEN_URL],
)
@property
def extra_authorize_data(self) -> dict:
"""Extra data that needs to be appended to the authorize url."""
data = {}
if self.data and (scopes := self.data[CONF_SCOPE]) is not None:
data[CONF_SCOPE] = " ".join(scopes)
data.update(super().extra_authorize_data)
return data
async def async_step_credentials_choice(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:

View File

@@ -5,4 +5,3 @@ DOMAIN = "mcp"
CONF_ACCESS_TOKEN = "access_token"
CONF_AUTHORIZATION_URL = "authorization_url"
CONF_TOKEN_URL = "token_url"
CONF_SCOPE = "scope"

View File

@@ -54,7 +54,6 @@ _LOGGER = logging.getLogger(__name__)
DEFAULT_PLATE_COUNT = 4
PLATE_COUNT = {
"KM7575": 6,
"KM7678": 6,
"KM7697": 6,
"KM7878": 6,

View File

@@ -157,7 +157,7 @@ def async_setup_services(hass: HomeAssistant) -> None:
) from error
except NordPoolEmptyResponseError:
return {area: [] for area in areas}
except (NordPoolError, TimeoutError) as error:
except NordPoolError as error:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="connection_error",

View File

@@ -31,39 +31,38 @@ async def async_setup_entry(
events = device.events.get_platform("binary_sensor")
entity_names = build_event_entity_names(events)
uids = set()
entities = []
for event in events:
uids.add(event.uid)
entities.append(
ONVIFBinarySensor(event.uid, device, name=entity_names[event.uid])
)
entities = {
event.uid: ONVIFBinarySensor(event.uid, device, name=entity_names[event.uid])
for event in events
}
ent_reg = er.async_get(hass)
for entry in er.async_entries_for_config_entry(ent_reg, config_entry.entry_id):
if entry.domain == "binary_sensor" and entry.unique_id not in uids:
uids.add(entry.unique_id)
entities.append(ONVIFBinarySensor(entry.unique_id, device, entry=entry))
if entry.domain == "binary_sensor" and entry.unique_id not in entities:
entities[entry.unique_id] = ONVIFBinarySensor(
entry.unique_id, device, entry=entry
)
async_add_entities(entities)
async_add_entities(entities.values())
uids_by_platform = device.events.get_uids_by_platform("binary_sensor")
@callback
def async_check_entities() -> None:
"""Check if we have added an entity for the event."""
nonlocal uids_by_platform
if not (missing := uids_by_platform.difference(uids)):
if not (missing := uids_by_platform.difference(entities)):
return
events = device.events.get_platform("binary_sensor")
entity_names = build_event_entity_names(events)
new_entities = [
ONVIFBinarySensor(uid, device, name=entity_names[uid]) for uid in missing
]
new_entities: dict[str, ONVIFBinarySensor] = {
uid: ONVIFBinarySensor(uid, device, name=entity_names[uid])
for uid in missing
}
if new_entities:
uids.update(missing)
async_add_entities(new_entities)
entities.update(new_entities)
async_add_entities(new_entities.values())
device.events.async_add_listener(async_check_entities)

View File

@@ -30,37 +30,37 @@ async def async_setup_entry(
events = device.events.get_platform("sensor")
entity_names = build_event_entity_names(events)
uids = set()
entities = []
for event in events:
uids.add(event.uid)
entities.append(ONVIFSensor(event.uid, device, name=entity_names[event.uid]))
entities = {
event.uid: ONVIFSensor(event.uid, device, name=entity_names[event.uid])
for event in events
}
ent_reg = er.async_get(hass)
for entry in er.async_entries_for_config_entry(ent_reg, config_entry.entry_id):
if entry.domain == "sensor" and entry.unique_id not in uids:
uids.add(entry.unique_id)
entities.append(ONVIFSensor(entry.unique_id, device, entry=entry))
if entry.domain == "sensor" and entry.unique_id not in entities:
entities[entry.unique_id] = ONVIFSensor(
entry.unique_id, device, entry=entry
)
async_add_entities(entities)
async_add_entities(entities.values())
uids_by_platform = device.events.get_uids_by_platform("sensor")
@callback
def async_check_entities() -> None:
"""Check if we have added an entity for the event."""
nonlocal uids_by_platform
if not (missing := uids_by_platform.difference(uids)):
if not (missing := uids_by_platform.difference(entities)):
return
events = device.events.get_platform("sensor")
entity_names = build_event_entity_names(events)
new_entities = [
ONVIFSensor(uid, device, name=entity_names[uid]) for uid in missing
]
new_entities: dict[str, ONVIFSensor] = {
uid: ONVIFSensor(uid, device, name=entity_names[uid]) for uid in missing
}
if new_entities:
uids.update(missing)
async_add_entities(new_entities)
entities.update(new_entities)
async_add_entities(new_entities.values())
device.events.async_add_listener(async_check_entities)

View File

@@ -8,7 +8,7 @@ import logging
from pyopenweathermap import create_owm_client
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_API_KEY, CONF_LANGUAGE, CONF_MODE
from homeassistant.const import CONF_API_KEY, CONF_LANGUAGE, CONF_MODE, CONF_NAME
from homeassistant.core import HomeAssistant
from .const import CONFIG_FLOW_VERSION, DEFAULT_OWM_MODE, OWM_MODES, PLATFORMS
@@ -25,6 +25,7 @@ type OpenweathermapConfigEntry = ConfigEntry[OpenweathermapData]
class OpenweathermapData:
"""Runtime data definition."""
name: str
mode: str
coordinator: OWMUpdateCoordinator
@@ -33,6 +34,7 @@ async def async_setup_entry(
hass: HomeAssistant, entry: OpenweathermapConfigEntry
) -> bool:
"""Set up OpenWeatherMap as config entry."""
name = entry.data[CONF_NAME]
api_key = entry.data[CONF_API_KEY]
language = entry.options[CONF_LANGUAGE]
mode = entry.options[CONF_MODE]
@@ -49,7 +51,7 @@ async def async_setup_entry(
entry.async_on_unload(entry.add_update_listener(async_update_options))
entry.runtime_data = OpenweathermapData(mode, owm_coordinator)
entry.runtime_data = OpenweathermapData(name, mode, owm_coordinator)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)

View File

@@ -14,17 +14,12 @@ from homeassistant.const import (
CONF_API_KEY,
CONF_LANGUAGE,
CONF_LATITUDE,
CONF_LOCATION,
CONF_LONGITUDE,
CONF_MODE,
CONF_NAME,
)
from homeassistant.core import callback
from homeassistant.helpers.selector import (
LanguageSelector,
LanguageSelectorConfig,
LocationSelector,
LocationSelectorConfig,
)
from homeassistant.helpers import config_validation as cv
from .const import (
CONFIG_FLOW_VERSION,
@@ -39,12 +34,10 @@ from .utils import build_data_and_options, validate_api_key
USER_SCHEMA = vol.Schema(
{
vol.Required(CONF_LOCATION): LocationSelector(
LocationSelectorConfig(radius=False)
),
vol.Optional(CONF_LANGUAGE, default=DEFAULT_LANGUAGE): LanguageSelector(
LanguageSelectorConfig(languages=LANGUAGES, native_name=True)
),
vol.Optional(CONF_NAME, default=DEFAULT_NAME): str,
vol.Optional(CONF_LATITUDE): cv.latitude,
vol.Optional(CONF_LONGITUDE): cv.longitude,
vol.Optional(CONF_LANGUAGE, default=DEFAULT_LANGUAGE): vol.In(LANGUAGES),
vol.Required(CONF_API_KEY): str,
vol.Optional(CONF_MODE, default=DEFAULT_OWM_MODE): vol.In(OWM_MODES),
}
@@ -52,9 +45,7 @@ USER_SCHEMA = vol.Schema(
OPTIONS_SCHEMA = vol.Schema(
{
vol.Optional(CONF_LANGUAGE, default=DEFAULT_LANGUAGE): LanguageSelector(
LanguageSelectorConfig(languages=LANGUAGES, native_name=True)
),
vol.Optional(CONF_LANGUAGE, default=DEFAULT_LANGUAGE): vol.In(LANGUAGES),
vol.Optional(CONF_MODE, default=DEFAULT_OWM_MODE): vol.In(OWM_MODES),
}
)
@@ -79,8 +70,8 @@ class OpenWeatherMapConfigFlow(ConfigFlow, domain=DOMAIN):
description_placeholders = {}
if user_input is not None:
latitude = user_input[CONF_LOCATION][CONF_LATITUDE]
longitude = user_input[CONF_LOCATION][CONF_LONGITUDE]
latitude = user_input[CONF_LATITUDE]
longitude = user_input[CONF_LONGITUDE]
mode = user_input[CONF_MODE]
await self.async_set_unique_id(f"{latitude}-{longitude}")
@@ -91,21 +82,15 @@ class OpenWeatherMapConfigFlow(ConfigFlow, domain=DOMAIN):
)
if not errors:
# Flatten location
location = user_input.pop(CONF_LOCATION)
user_input[CONF_LATITUDE] = location[CONF_LATITUDE]
user_input[CONF_LONGITUDE] = location[CONF_LONGITUDE]
data, options = build_data_and_options(user_input)
return self.async_create_entry(
title=DEFAULT_NAME, data=data, options=options
title=user_input[CONF_NAME], data=data, options=options
)
schema_data = user_input
else:
schema_data = {
CONF_LOCATION: {
CONF_LATITUDE: self.hass.config.latitude,
CONF_LONGITUDE: self.hass.config.longitude,
},
CONF_LATITUDE: self.hass.config.latitude,
CONF_LONGITUDE: self.hass.config.longitude,
CONF_LANGUAGE: self.hass.config.language,
}

View File

@@ -121,7 +121,6 @@ WEATHER_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfPressure.HPA,
device_class=SensorDeviceClass.PRESSURE,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=0,
),
SensorEntityDescription(
key=ATTR_API_CLOUDS,
@@ -159,7 +158,6 @@ WEATHER_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfLength.METERS,
device_class=SensorDeviceClass.DISTANCE,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=0,
),
SensorEntityDescription(
key=ATTR_API_CONDITION,
@@ -229,6 +227,7 @@ async def async_setup_entry(
) -> None:
"""Set up OpenWeatherMap sensor entities based on a config entry."""
domain_data = config_entry.runtime_data
name = domain_data.name
unique_id = config_entry.unique_id
assert unique_id is not None
coordinator = domain_data.coordinator
@@ -243,6 +242,7 @@ async def async_setup_entry(
elif domain_data.mode == OWM_MODE_AIRPOLLUTION:
async_add_entities(
OpenWeatherMapSensor(
name,
unique_id,
description,
coordinator,
@@ -252,6 +252,7 @@ async def async_setup_entry(
else:
async_add_entities(
OpenWeatherMapSensor(
name,
unique_id,
description,
coordinator,
@@ -269,6 +270,7 @@ class AbstractOpenWeatherMapSensor(SensorEntity):
def __init__(
self,
name: str,
unique_id: str,
description: SensorEntityDescription,
coordinator: OWMUpdateCoordinator,
@@ -282,6 +284,7 @@ class AbstractOpenWeatherMapSensor(SensorEntity):
entry_type=DeviceEntryType.SERVICE,
identifiers={(DOMAIN, unique_id)},
manufacturer=MANUFACTURER,
name=name,
)
@property

View File

@@ -12,14 +12,16 @@
"data": {
"api_key": "[%key:common::config_flow::data::api_key%]",
"language": "[%key:common::config_flow::data::language%]",
"location": "[%key:common::config_flow::data::location%]",
"latitude": "[%key:common::config_flow::data::latitude%]",
"longitude": "[%key:common::config_flow::data::longitude%]",
"mode": "[%key:common::config_flow::data::mode%]",
"name": "[%key:common::config_flow::data::name%]"
},
"data_description": {
"api_key": "API key for the OpenWeatherMap integration",
"language": "Language for the OpenWeatherMap content",
"location": "Location to get the weather data for",
"latitude": "Latitude of the location",
"longitude": "Longitude of the location",
"mode": "Mode for the OpenWeatherMap API",
"name": "Name for this OpenWeatherMap location"
},

View File

@@ -57,13 +57,14 @@ async def async_setup_entry(
) -> None:
"""Set up OpenWeatherMap weather entity based on a config entry."""
domain_data = config_entry.runtime_data
name = domain_data.name
mode = domain_data.mode
if mode != OWM_MODE_AIRPOLLUTION:
weather_coordinator = domain_data.coordinator
unique_id = f"{config_entry.unique_id}"
owm_weather = OpenWeatherMapWeather(unique_id, mode, weather_coordinator)
owm_weather = OpenWeatherMapWeather(name, unique_id, mode, weather_coordinator)
async_add_entities([owm_weather], False)
@@ -92,6 +93,7 @@ class OpenWeatherMapWeather(SingleCoordinatorWeatherEntity[OWMUpdateCoordinator]
def __init__(
self,
name: str,
unique_id: str,
mode: str,
weather_coordinator: OWMUpdateCoordinator,
@@ -103,6 +105,7 @@ class OpenWeatherMapWeather(SingleCoordinatorWeatherEntity[OWMUpdateCoordinator]
entry_type=DeviceEntryType.SERVICE,
identifiers={(DOMAIN, unique_id)},
manufacturer=MANUFACTURER,
name=name,
)
self.mode = mode

View File

@@ -48,7 +48,7 @@ from .const import (
DEFAULT_OFF_DELAY = 2.0
CONNECT_TIMEOUT = 60.0
CONNECT_TIMEOUT = 30.0
_LOGGER = logging.getLogger(__name__)

View File

@@ -24,7 +24,6 @@
},
"config_subentries": {
"partition": {
"entry_type": "Partition",
"initiate_flow": {
"user": "Add partition"
},
@@ -58,7 +57,6 @@
}
},
"zone": {
"entry_type": "Zone",
"initiate_flow": {
"user": "Add zone"
},
@@ -93,7 +91,6 @@
}
},
"output": {
"entry_type": "Output",
"initiate_flow": {
"user": "Add output"
},
@@ -128,7 +125,6 @@
}
},
"switchable_output": {
"entry_type": "Switchable output",
"initiate_flow": {
"user": "Add switchable output"
},

View File

@@ -2,7 +2,6 @@
from __future__ import annotations
from functools import partial
from typing import Final
from aioshelly.ble.const import BLE_SCRIPT_NAME
@@ -64,7 +63,6 @@ from .repairs import (
)
from .utils import (
async_create_issue_unsupported_firmware,
async_migrate_rpc_virtual_components_unique_ids,
get_coap_context,
get_device_entry_gen,
get_http_port,
@@ -325,12 +323,6 @@ async def _async_setup_rpc_entry(hass: HomeAssistant, entry: ShellyConfigEntry)
translation_placeholders={"device": entry.title},
) from err
await er.async_migrate_entries(
hass,
entry.entry_id,
partial(async_migrate_rpc_virtual_components_unique_ids, device.config),
)
runtime_data.rpc = ShellyRpcCoordinator(hass, entry, device)
runtime_data.rpc.async_setup()
runtime_data.rpc_poll = ShellyRpcPollingCoordinator(hass, entry, device)

View File

@@ -18,7 +18,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.restore_state import RestoreEntity
from .const import CONF_SLEEP_PERIOD, MODEL_FRANKEVER_WATER_VALVE
from .const import CONF_SLEEP_PERIOD
from .coordinator import ShellyConfigEntry, ShellyRpcCoordinator
from .entity import (
BlockEntityDescription,
@@ -270,21 +270,12 @@ RPC_SENSORS: Final = {
entity_registry_enabled_default=False,
entity_category=EntityCategory.DIAGNOSTIC,
),
"boolean_generic": RpcBinarySensorDescription(
"boolean": RpcBinarySensorDescription(
key="boolean",
sub_key="value",
removal_condition=lambda config, _status, key: not is_view_for_platform(
config, key, BINARY_SENSOR_PLATFORM
),
role="generic",
),
"boolean_has_power": RpcBinarySensorDescription(
key="boolean",
sub_key="value",
device_class=BinarySensorDeviceClass.POWER,
entity_category=EntityCategory.DIAGNOSTIC,
role="has_power",
models={MODEL_FRANKEVER_WATER_VALVE},
),
"calibration": RpcBinarySensorDescription(
key="blutrv",

View File

@@ -308,5 +308,3 @@ MODEL_NEO_WATER_VALVE = "NeoWaterValve"
MODEL_FRANKEVER_WATER_VALVE = "WaterValve"
MODEL_LINKEDGO_ST802_THERMOSTAT = "ST-802"
MODEL_LINKEDGO_ST1820_THERMOSTAT = "ST1820"
MODEL_TOP_EV_CHARGER_EVE01 = "EVE01"
MODEL_FRANKEVER_IRRIGATION_CONTROLLER = "Irrigation"

View File

@@ -29,7 +29,6 @@ from .utils import (
get_rpc_device_info,
get_rpc_entity_name,
get_rpc_key_instances,
get_rpc_role_by_key,
)
@@ -190,9 +189,9 @@ def async_setup_rpc_attribute_entities(
if description.models and coordinator.model not in description.models:
continue
if description.role and description.role != get_rpc_role_by_key(
coordinator.device.config, key
):
if description.role and description.role != coordinator.device.config[
key
].get("role", "generic"):
continue
if description.sub_key not in coordinator.device.status[

View File

@@ -9,7 +9,7 @@
"iot_class": "local_push",
"loggers": ["aioshelly"],
"quality_scale": "silver",
"requirements": ["aioshelly==13.12.0"],
"requirements": ["aioshelly==13.11.0"],
"zeroconf": [
{
"type": "_http._tcp.local.",

View File

@@ -24,16 +24,7 @@ from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.entity_registry import RegistryEntry
from .const import (
CONF_SLEEP_PERIOD,
DOMAIN,
LOGGER,
MODEL_FRANKEVER_WATER_VALVE,
MODEL_LINKEDGO_ST802_THERMOSTAT,
MODEL_LINKEDGO_ST1820_THERMOSTAT,
MODEL_TOP_EV_CHARGER_EVE01,
VIRTUAL_NUMBER_MODE_MAP,
)
from .const import CONF_SLEEP_PERIOD, DOMAIN, LOGGER, VIRTUAL_NUMBER_MODE_MAP
from .coordinator import ShellyBlockCoordinator, ShellyConfigEntry, ShellyRpcCoordinator
from .entity import (
BlockEntityDescription,
@@ -192,7 +183,7 @@ RPC_NUMBERS: Final = {
method="blu_trv_set_external_temperature",
entity_class=RpcBluTrvExtTempNumber,
),
"number_generic": RpcNumberDescription(
"number": RpcNumberDescription(
key="number",
sub_key="value",
removal_condition=lambda config, _status, key: not is_view_for_platform(
@@ -206,58 +197,6 @@ RPC_NUMBERS: Final = {
step_fn=lambda config: config["meta"]["ui"].get("step"),
unit=get_virtual_component_unit,
method="number_set",
role="generic",
),
"number_current_limit": RpcNumberDescription(
key="number",
sub_key="value",
max_fn=lambda config: config["max"],
min_fn=lambda config: config["min"],
mode_fn=lambda config: NumberMode.SLIDER,
step_fn=lambda config: config["meta"]["ui"].get("step"),
unit=get_virtual_component_unit,
method="number_set",
role="current_limit",
models={MODEL_TOP_EV_CHARGER_EVE01},
),
"number_position": RpcNumberDescription(
key="number",
sub_key="value",
entity_registry_enabled_default=False,
max_fn=lambda config: config["max"],
min_fn=lambda config: config["min"],
mode_fn=lambda config: NumberMode.SLIDER,
step_fn=lambda config: config["meta"]["ui"].get("step"),
unit=get_virtual_component_unit,
method="number_set",
role="position",
models={MODEL_FRANKEVER_WATER_VALVE},
),
"number_target_humidity": RpcNumberDescription(
key="number",
sub_key="value",
entity_registry_enabled_default=False,
max_fn=lambda config: config["max"],
min_fn=lambda config: config["min"],
mode_fn=lambda config: NumberMode.SLIDER,
step_fn=lambda config: config["meta"]["ui"].get("step"),
unit=get_virtual_component_unit,
method="number_set",
role="target_humidity",
models={MODEL_LINKEDGO_ST802_THERMOSTAT, MODEL_LINKEDGO_ST1820_THERMOSTAT},
),
"number_target_temperature": RpcNumberDescription(
key="number",
sub_key="value",
entity_registry_enabled_default=False,
max_fn=lambda config: config["max"],
min_fn=lambda config: config["min"],
mode_fn=lambda config: NumberMode.SLIDER,
step_fn=lambda config: config["meta"]["ui"].get("step"),
unit=get_virtual_component_unit,
method="number_set",
role="target_temperature",
models={MODEL_LINKEDGO_ST802_THERMOSTAT, MODEL_LINKEDGO_ST1820_THERMOSTAT},
),
"valve_position": RpcNumberDescription(
key="blutrv",

View File

@@ -38,13 +38,12 @@ class RpcSelectDescription(RpcEntityDescription, SelectEntityDescription):
RPC_SELECT_ENTITIES: Final = {
"enum_generic": RpcSelectDescription(
"enum": RpcSelectDescription(
key="enum",
sub_key="value",
removal_condition=lambda config, _status, key: not is_view_for_platform(
config, key, SELECT_PLATFORM
),
role="generic",
),
}

View File

@@ -3,7 +3,8 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import Final, cast
from functools import partial
from typing import Any, Final, cast
from aioshelly.block_device import Block
from aioshelly.const import RPC_GENERATIONS
@@ -36,12 +37,13 @@ from homeassistant.const import (
UnitOfVolume,
UnitOfVolumeFlowRate,
)
from homeassistant.core import HomeAssistant
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.entity_registry import RegistryEntry
from homeassistant.helpers.typing import StateType
from .const import CONF_SLEEP_PERIOD
from .const import CONF_SLEEP_PERIOD, LOGGER
from .coordinator import ShellyBlockCoordinator, ShellyConfigEntry, ShellyRpcCoordinator
from .entity import (
BlockEntityDescription,
@@ -549,7 +551,7 @@ RPC_SENSORS: Final = {
"a_act_power": RpcSensorDescription(
key="em",
sub_key="a_act_power",
name="Power",
name="Active power",
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
@@ -559,7 +561,7 @@ RPC_SENSORS: Final = {
"b_act_power": RpcSensorDescription(
key="em",
sub_key="b_act_power",
name="Power",
name="Active power",
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
@@ -569,7 +571,7 @@ RPC_SENSORS: Final = {
"c_act_power": RpcSensorDescription(
key="em",
sub_key="c_act_power",
name="Power",
name="Active power",
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
@@ -579,7 +581,7 @@ RPC_SENSORS: Final = {
"total_act_power": RpcSensorDescription(
key="em",
sub_key="total_act_power",
name="Power",
name="Total active power",
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
@@ -625,7 +627,7 @@ RPC_SENSORS: Final = {
"total_aprt_power": RpcSensorDescription(
key="em",
sub_key="total_aprt_power",
name="Apparent power",
name="Total apparent power",
native_unit_of_measurement=UnitOfApparentPower.VOLT_AMPERE,
device_class=SensorDeviceClass.APPARENT_POWER,
state_class=SensorStateClass.MEASUREMENT,
@@ -880,7 +882,7 @@ RPC_SENSORS: Final = {
"n_current": RpcSensorDescription(
key="em",
sub_key="n_current",
name="Neutral current",
name="Phase N current",
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
device_class=SensorDeviceClass.CURRENT,
state_class=SensorStateClass.MEASUREMENT,
@@ -890,7 +892,7 @@ RPC_SENSORS: Final = {
"total_current": RpcSensorDescription(
key="em",
sub_key="total_current",
name="Current",
name="Total current",
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
device_class=SensorDeviceClass.CURRENT,
state_class=SensorStateClass.MEASUREMENT,
@@ -1382,7 +1384,7 @@ RPC_SENSORS: Final = {
native_unit_of_measurement="pulse",
state_class=SensorStateClass.TOTAL,
value=lambda status, _: status["total"],
removal_condition=lambda config, _, key: (
removal_condition=lambda config, _status, key: (
config[key]["type"] != "count" or config[key]["enable"] is False
),
),
@@ -1422,7 +1424,7 @@ RPC_SENSORS: Final = {
"text_generic": RpcSensorDescription(
key="text",
sub_key="value",
removal_condition=lambda config, _, key: not is_view_for_platform(
removal_condition=lambda config, _status, key: not is_view_for_platform(
config, key, SENSOR_PLATFORM
),
role="generic",
@@ -1430,7 +1432,7 @@ RPC_SENSORS: Final = {
"number_generic": RpcSensorDescription(
key="number",
sub_key="value",
removal_condition=lambda config, _, key: not is_view_for_platform(
removal_condition=lambda config, _status, key: not is_view_for_platform(
config, key, SENSOR_PLATFORM
),
unit=get_virtual_component_unit,
@@ -1439,7 +1441,7 @@ RPC_SENSORS: Final = {
"enum_generic": RpcSensorDescription(
key="enum",
sub_key="value",
removal_condition=lambda config, _, key: not is_view_for_platform(
removal_condition=lambda config, _status, key: not is_view_for_platform(
config, key, SENSOR_PLATFORM
),
options_fn=lambda config: config["options"],
@@ -1454,7 +1456,7 @@ RPC_SENSORS: Final = {
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
removal_condition=lambda config, _, key: config[key].get("enable", False)
removal_condition=lambda config, _status, key: config[key].get("enable", False)
is False,
entity_class=RpcBluTrvSensor,
),
@@ -1604,7 +1606,7 @@ RPC_SENSORS: Final = {
"object_total_act_energy": RpcSensorDescription(
key="object",
sub_key="value",
name="Energy",
name="Total Active Energy",
value=lambda status, _: float(status["total_act_energy"]),
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
@@ -1616,7 +1618,7 @@ RPC_SENSORS: Final = {
"object_total_power": RpcSensorDescription(
key="object",
sub_key="value",
name="Power",
name="Total Power",
value=lambda status, _: float(status["total_power"]),
native_unit_of_measurement=UnitOfPower.WATT,
suggested_unit_of_measurement=UnitOfPower.KILO_WATT,
@@ -1661,6 +1663,39 @@ RPC_SENSORS: Final = {
}
@callback
def async_migrate_unique_ids(
coordinator: ShellyRpcCoordinator,
entity_entry: er.RegistryEntry,
) -> dict[str, Any] | None:
"""Migrate sensor unique IDs to include role."""
if not entity_entry.entity_id.startswith("sensor."):
return None
for sensor_id in ("text", "number", "enum"):
old_unique_id = entity_entry.unique_id
if old_unique_id.endswith(f"-{sensor_id}"):
if entity_entry.original_device_class == SensorDeviceClass.HUMIDITY:
new_unique_id = f"{old_unique_id}_current_humidity"
elif entity_entry.original_device_class == SensorDeviceClass.TEMPERATURE:
new_unique_id = f"{old_unique_id}_current_temperature"
else:
new_unique_id = f"{old_unique_id}_generic"
LOGGER.debug(
"Migrating unique_id for %s entity from [%s] to [%s]",
entity_entry.entity_id,
old_unique_id,
new_unique_id,
)
return {
"new_unique_id": entity_entry.unique_id.replace(
old_unique_id, new_unique_id
)
}
return None
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ShellyConfigEntry,
@@ -1680,6 +1715,12 @@ async def async_setup_entry(
coordinator = config_entry.runtime_data.rpc
assert coordinator
await er.async_migrate_entries(
hass,
config_entry.entry_id,
partial(async_migrate_unique_ids, coordinator),
)
async_setup_entry_rpc(
hass, config_entry, async_add_entities, RPC_SENSORS, RpcSensor
)

View File

@@ -21,13 +21,6 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.entity_registry import RegistryEntry
from homeassistant.helpers.restore_state import RestoreEntity
from .const import (
MODEL_FRANKEVER_IRRIGATION_CONTROLLER,
MODEL_LINKEDGO_ST802_THERMOSTAT,
MODEL_LINKEDGO_ST1820_THERMOSTAT,
MODEL_NEO_WATER_VALVE,
MODEL_TOP_EV_CHARGER_EVE01,
)
from .coordinator import ShellyBlockCoordinator, ShellyConfigEntry, ShellyRpcCoordinator
from .entity import (
BlockEntityDescription,
@@ -94,7 +87,7 @@ RPC_RELAY_SWITCHES = {
}
RPC_SWITCHES = {
"boolean_generic": RpcSwitchDescription(
"boolean": RpcSwitchDescription(
key="boolean",
sub_key="value",
removal_condition=lambda config, _status, key: not is_view_for_platform(
@@ -104,120 +97,6 @@ RPC_SWITCHES = {
method_on="Boolean.Set",
method_off="Boolean.Set",
method_params_fn=lambda id, value: {"id": id, "value": value},
role="generic",
),
"boolean_anti_freeze": RpcSwitchDescription(
key="boolean",
sub_key="value",
entity_registry_enabled_default=False,
is_on=lambda status: bool(status["value"]),
method_on="Boolean.Set",
method_off="Boolean.Set",
method_params_fn=lambda id, value: {"id": id, "value": value},
role="anti_freeze",
models={MODEL_LINKEDGO_ST802_THERMOSTAT, MODEL_LINKEDGO_ST1820_THERMOSTAT},
),
"boolean_child_lock": RpcSwitchDescription(
key="boolean",
sub_key="value",
is_on=lambda status: bool(status["value"]),
method_on="Boolean.Set",
method_off="Boolean.Set",
method_params_fn=lambda id, value: {"id": id, "value": value},
role="child_lock",
models={MODEL_LINKEDGO_ST1820_THERMOSTAT},
),
"boolean_enable": RpcSwitchDescription(
key="boolean",
sub_key="value",
entity_registry_enabled_default=False,
is_on=lambda status: bool(status["value"]),
method_on="Boolean.Set",
method_off="Boolean.Set",
method_params_fn=lambda id, value: {"id": id, "value": value},
role="enable",
models={MODEL_LINKEDGO_ST802_THERMOSTAT, MODEL_LINKEDGO_ST1820_THERMOSTAT},
),
"boolean_start_charging": RpcSwitchDescription(
key="boolean",
sub_key="value",
is_on=lambda status: bool(status["value"]),
method_on="Boolean.Set",
method_off="Boolean.Set",
method_params_fn=lambda id, value: {"id": id, "value": value},
role="start_charging",
models={MODEL_TOP_EV_CHARGER_EVE01},
),
"boolean_state": RpcSwitchDescription(
key="boolean",
sub_key="value",
entity_registry_enabled_default=False,
is_on=lambda status: bool(status["value"]),
method_on="Boolean.Set",
method_off="Boolean.Set",
method_params_fn=lambda id, value: {"id": id, "value": value},
role="state",
models={MODEL_NEO_WATER_VALVE},
),
"boolean_zone0": RpcSwitchDescription(
key="boolean",
sub_key="value",
is_on=lambda status: bool(status["value"]),
method_on="Boolean.Set",
method_off="Boolean.Set",
method_params_fn=lambda id, value: {"id": id, "value": value},
role="zone0",
models={MODEL_FRANKEVER_IRRIGATION_CONTROLLER},
),
"boolean_zone1": RpcSwitchDescription(
key="boolean",
sub_key="value",
is_on=lambda status: bool(status["value"]),
method_on="Boolean.Set",
method_off="Boolean.Set",
method_params_fn=lambda id, value: {"id": id, "value": value},
role="zone1",
models={MODEL_FRANKEVER_IRRIGATION_CONTROLLER},
),
"boolean_zone2": RpcSwitchDescription(
key="boolean",
sub_key="value",
is_on=lambda status: bool(status["value"]),
method_on="Boolean.Set",
method_off="Boolean.Set",
method_params_fn=lambda id, value: {"id": id, "value": value},
role="zone2",
models={MODEL_FRANKEVER_IRRIGATION_CONTROLLER},
),
"boolean_zone3": RpcSwitchDescription(
key="boolean",
sub_key="value",
is_on=lambda status: bool(status["value"]),
method_on="Boolean.Set",
method_off="Boolean.Set",
method_params_fn=lambda id, value: {"id": id, "value": value},
role="zone3",
models={MODEL_FRANKEVER_IRRIGATION_CONTROLLER},
),
"boolean_zone4": RpcSwitchDescription(
key="boolean",
sub_key="value",
is_on=lambda status: bool(status["value"]),
method_on="Boolean.Set",
method_off="Boolean.Set",
method_params_fn=lambda id, value: {"id": id, "value": value},
role="zone4",
models={MODEL_FRANKEVER_IRRIGATION_CONTROLLER},
),
"boolean_zone5": RpcSwitchDescription(
key="boolean",
sub_key="value",
is_on=lambda status: bool(status["value"]),
method_on="Boolean.Set",
method_off="Boolean.Set",
method_params_fn=lambda id, value: {"id": id, "value": value},
role="zone5",
models={MODEL_FRANKEVER_IRRIGATION_CONTROLLER},
),
"script": RpcSwitchDescription(
key="script",

View File

@@ -38,13 +38,12 @@ class RpcTextDescription(RpcEntityDescription, TextEntityDescription):
RPC_TEXT_ENTITIES: Final = {
"text_generic": RpcTextDescription(
"text": RpcTextDescription(
key="text",
sub_key="value",
removal_condition=lambda config, _status, key: not is_view_for_platform(
config, key, TEXT_PLATFORM
),
role="generic",
),
}

View File

@@ -484,11 +484,6 @@ def get_rpc_key_by_role(keys_dict: dict[str, Any], role: str) -> str | None:
return None
def get_rpc_role_by_key(keys_dict: dict[str, Any], key: str) -> str:
"""Return role by key for RPC device from a dict."""
return cast(str, keys_dict[key].get("role", "generic"))
def id_from_key(key: str) -> int:
"""Return id from key."""
return int(key.split(":")[-1])
@@ -939,35 +934,3 @@ def remove_empty_sub_devices(hass: HomeAssistant, entry: ConfigEntry) -> None:
def format_ble_addr(ble_addr: str) -> str:
"""Format BLE address to use in unique_id."""
return ble_addr.replace(":", "").upper()
@callback
def async_migrate_rpc_virtual_components_unique_ids(
config: dict[str, Any], entity_entry: er.RegistryEntry
) -> dict[str, Any] | None:
"""Migrate RPC virtual components unique_ids to include role in the ID.
This is needed to support multiple components with the same key.
The old unique_id format is: {mac}-{key}-{component}
The new unique_id format is: {mac}-{key}-{component}_{role}
"""
for component in VIRTUAL_COMPONENTS:
if entity_entry.unique_id.endswith(f"-{component!s}"):
key = entity_entry.unique_id.split("-")[-2]
if key not in config:
continue
role = get_rpc_role_by_key(config, key)
new_unique_id = f"{entity_entry.unique_id}_{role}"
LOGGER.debug(
"Migrating unique_id for %s entity from [%s] to [%s]",
entity_entry.entity_id,
entity_entry.unique_id,
new_unique_id,
)
return {
"new_unique_id": entity_entry.unique_id.replace(
entity_entry.unique_id, new_unique_id
)
}
return None

View File

@@ -179,13 +179,6 @@ CAPABILITY_TO_SENSORS: dict[
is_on_key="open",
)
},
Capability.GAS_DETECTOR: {
Attribute.GAS: SmartThingsBinarySensorEntityDescription(
key=Attribute.GAS,
device_class=BinarySensorDeviceClass.GAS,
is_on_key="detected",
)
},
}

View File

@@ -530,6 +530,7 @@ CAPABILITY_TO_SENSORS: dict[
)
],
},
# Haven't seen at devices yet
Capability.ILLUMINANCE_MEASUREMENT: {
Attribute.ILLUMINANCE: [
SmartThingsSensorEntityDescription(
@@ -841,6 +842,7 @@ CAPABILITY_TO_SENSORS: dict[
)
]
},
# Haven't seen at devices yet
Capability.SIGNAL_STRENGTH: {
Attribute.LQI: [
SmartThingsSensorEntityDescription(
@@ -999,6 +1001,7 @@ CAPABILITY_TO_SENSORS: dict[
)
],
},
# Haven't seen at devices yet
Capability.TVOC_MEASUREMENT: {
Attribute.TVOC_LEVEL: [
SmartThingsSensorEntityDescription(
@@ -1009,6 +1012,7 @@ CAPABILITY_TO_SENSORS: dict[
)
]
},
# Haven't seen at devices yet
Capability.ULTRAVIOLET_INDEX: {
Attribute.ULTRAVIOLET_INDEX: [
SmartThingsSensorEntityDescription(

View File

@@ -2,6 +2,7 @@
from __future__ import annotations
import asyncio
from typing import Any
import voluptuous as vol
@@ -20,7 +21,7 @@ from homeassistant.const import (
CONF_PLATFORM,
CONF_TYPE,
)
from homeassistant.core import CALLBACK_TYPE, HomeAssistant
from homeassistant.core import CALLBACK_TYPE, Context, HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import (
config_validation as cv,
@@ -454,8 +455,28 @@ async def async_attach_trigger(
zwave_js_config = await validate_value_updated_trigger_config(
hass, zwave_js_config
)
@callback
def run_action(
extra_trigger_payload: dict[str, Any],
description: str,
context: Context | None = None,
) -> asyncio.Task[Any]:
"""Run action with trigger variables."""
payload = {
"trigger": {
**trigger_info["trigger_data"],
CONF_PLATFORM: VALUE_UPDATED_PLATFORM_TYPE,
"description": description,
**extra_trigger_payload,
}
}
return hass.async_create_task(action(payload, context))
return await attach_value_updated_trigger(
hass, zwave_js_config[CONF_OPTIONS], action, trigger_info
hass, zwave_js_config[CONF_OPTIONS], run_action
)
raise HomeAssistantError(f"Unhandled trigger type {trigger_type}")

View File

@@ -134,6 +134,7 @@ ENTITY_DESCRIPTION_KEY_UNIT_MAP: dict[tuple[str, str], SensorEntityDescription]
device_class=SensorDeviceClass.VOLTAGE,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
suggested_display_precision=0,
),
(
ENTITY_DESC_KEY_VOLTAGE,

View File

@@ -17,19 +17,12 @@ from homeassistant.const import (
ATTR_DEVICE_ID,
ATTR_ENTITY_ID,
CONF_OPTIONS,
CONF_PLATFORM,
)
from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
from homeassistant.helpers import config_validation as cv, device_registry as dr
from homeassistant.helpers.automation import move_top_level_schema_fields_to_options
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.trigger import (
Trigger,
TriggerActionType,
TriggerConfig,
TriggerData,
TriggerInfo,
)
from homeassistant.helpers.trigger import Trigger, TriggerActionRunner, TriggerConfig
from homeassistant.helpers.typing import ConfigType
from ..const import (
@@ -127,17 +120,13 @@ _CONFIG_SCHEMA = vol.Schema(
class EventTrigger(Trigger):
"""Z-Wave JS event trigger."""
_hass: HomeAssistant
_options: dict[str, Any]
_event_source: str
_event_name: str
_event_data_filter: dict
_job: HassJob
_trigger_data: TriggerData
_unsubs: list[Callable]
_platform_type = PLATFORM_TYPE
_action_runner: TriggerActionRunner
@classmethod
async def async_validate_complete_config(
@@ -176,14 +165,12 @@ class EventTrigger(Trigger):
def __init__(self, hass: HomeAssistant, config: TriggerConfig) -> None:
"""Initialize trigger."""
self._hass = hass
super().__init__(hass, config)
assert config.options is not None
self._options = config.options
async def async_attach(
self,
action: TriggerActionType,
trigger_info: TriggerInfo,
async def async_attach_runner(
self, run_action: TriggerActionRunner
) -> CALLBACK_TYPE:
"""Attach a trigger."""
dev_reg = dr.async_get(self._hass)
@@ -198,8 +185,7 @@ class EventTrigger(Trigger):
self._event_source = options[ATTR_EVENT_SOURCE]
self._event_name = options[ATTR_EVENT]
self._event_data_filter = options.get(ATTR_EVENT_DATA, {})
self._job = HassJob(action)
self._trigger_data = trigger_info["trigger_data"]
self._action_runner = run_action
self._unsubs: list[Callable] = []
self._create_zwave_listeners()
@@ -225,9 +211,7 @@ class EventTrigger(Trigger):
if event_data[key] != val:
return
payload = {
**self._trigger_data,
CONF_PLATFORM: self._platform_type,
payload: dict[str, Any] = {
ATTR_EVENT_SOURCE: self._event_source,
ATTR_EVENT: self._event_name,
ATTR_EVENT_DATA: event_data,
@@ -237,21 +221,17 @@ class EventTrigger(Trigger):
f"Z-Wave JS '{self._event_source}' event '{self._event_name}' was emitted"
)
description = primary_desc
if device:
device_name = device.name_by_user or device.name
payload[ATTR_DEVICE_ID] = device.id
home_and_node_id = get_home_and_node_id_from_device_entry(device)
assert home_and_node_id
payload[ATTR_NODE_ID] = home_and_node_id[1]
payload["description"] = f"{primary_desc} on {device_name}"
else:
payload["description"] = primary_desc
description = f"{primary_desc} on {device_name}"
payload["description"] = (
f"{payload['description']} with event data: {event_data}"
)
self._hass.async_run_hass_job(self._job, {"trigger": payload})
description = f"{description} with event data: {event_data}"
self._action_runner(payload, description)
@callback
def _async_remove(self) -> None:

View File

@@ -11,23 +11,12 @@ from zwave_js_server.const import CommandClass
from zwave_js_server.model.driver import Driver
from zwave_js_server.model.value import Value, get_value_id_str
from homeassistant.const import (
ATTR_DEVICE_ID,
ATTR_ENTITY_ID,
CONF_OPTIONS,
CONF_PLATFORM,
MATCH_ALL,
)
from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback
from homeassistant.const import ATTR_DEVICE_ID, ATTR_ENTITY_ID, CONF_OPTIONS, MATCH_ALL
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
from homeassistant.helpers import config_validation as cv, device_registry as dr
from homeassistant.helpers.automation import move_top_level_schema_fields_to_options
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.trigger import (
Trigger,
TriggerActionType,
TriggerConfig,
TriggerInfo,
)
from homeassistant.helpers.trigger import Trigger, TriggerActionRunner, TriggerConfig
from homeassistant.helpers.typing import ConfigType
from ..config_validation import VALUE_SCHEMA
@@ -100,12 +89,7 @@ async def async_validate_trigger_config(
async def async_attach_trigger(
hass: HomeAssistant,
options: ConfigType,
action: TriggerActionType,
trigger_info: TriggerInfo,
*,
platform_type: str = PLATFORM_TYPE,
hass: HomeAssistant, options: ConfigType, run_action: TriggerActionRunner
) -> CALLBACK_TYPE:
"""Listen for state changes based on configuration."""
dev_reg = dr.async_get(hass)
@@ -121,9 +105,6 @@ async def async_attach_trigger(
endpoint = options.get(ATTR_ENDPOINT)
property_key = options.get(ATTR_PROPERTY_KEY)
unsubs: list[Callable] = []
job = HassJob(action)
trigger_data = trigger_info["trigger_data"]
@callback
def async_on_value_updated(
@@ -152,10 +133,8 @@ async def async_attach_trigger(
return
device_name = device.name_by_user or device.name
description = f"Z-Wave value {value.value_id} updated on {device_name}"
payload = {
**trigger_data,
CONF_PLATFORM: platform_type,
ATTR_DEVICE_ID: device.id,
ATTR_NODE_ID: value.node.node_id,
ATTR_COMMAND_CLASS: value.command_class,
@@ -169,10 +148,9 @@ async def async_attach_trigger(
ATTR_PREVIOUS_VALUE_RAW: prev_value_raw,
ATTR_CURRENT_VALUE: curr_value,
ATTR_CURRENT_VALUE_RAW: curr_value_raw,
"description": f"Z-Wave value {value.value_id} updated on {device_name}",
}
hass.async_run_hass_job(job, {"trigger": payload})
run_action(payload, description)
@callback
def async_remove() -> None:
@@ -223,7 +201,6 @@ async def async_attach_trigger(
class ValueUpdatedTrigger(Trigger):
"""Z-Wave JS value updated trigger."""
_hass: HomeAssistant
_options: dict[str, Any]
@classmethod
@@ -245,16 +222,12 @@ class ValueUpdatedTrigger(Trigger):
def __init__(self, hass: HomeAssistant, config: TriggerConfig) -> None:
"""Initialize trigger."""
self._hass = hass
super().__init__(hass, config)
assert config.options is not None
self._options = config.options
async def async_attach(
self,
action: TriggerActionType,
trigger_info: TriggerInfo,
async def async_attach_runner(
self, run_action: TriggerActionRunner
) -> CALLBACK_TYPE:
"""Attach a trigger."""
return await async_attach_trigger(
self._hass, self._options, action, trigger_info
)
return await async_attach_trigger(self._hass, self._options, run_action)

View File

@@ -8,6 +8,7 @@ from typing import TYPE_CHECKING, Final
from .generated.entity_platforms import EntityPlatforms
from .helpers.deprecation import (
DeprecatedConstant,
DeprecatedConstantEnum,
EnumWithDeprecatedMembers,
all_with_deprecated_constants,
@@ -315,6 +316,60 @@ STATE_OK: Final = "ok"
STATE_PROBLEM: Final = "problem"
# #### ALARM CONTROL PANEL STATES ####
# STATE_ALARM_* below are deprecated as of 2024.11
# use the AlarmControlPanelState enum instead.
_DEPRECATED_STATE_ALARM_DISARMED: Final = DeprecatedConstant(
"disarmed",
"AlarmControlPanelState.DISARMED",
"2025.11",
)
_DEPRECATED_STATE_ALARM_ARMED_HOME: Final = DeprecatedConstant(
"armed_home",
"AlarmControlPanelState.ARMED_HOME",
"2025.11",
)
_DEPRECATED_STATE_ALARM_ARMED_AWAY: Final = DeprecatedConstant(
"armed_away",
"AlarmControlPanelState.ARMED_AWAY",
"2025.11",
)
_DEPRECATED_STATE_ALARM_ARMED_NIGHT: Final = DeprecatedConstant(
"armed_night",
"AlarmControlPanelState.ARMED_NIGHT",
"2025.11",
)
_DEPRECATED_STATE_ALARM_ARMED_VACATION: Final = DeprecatedConstant(
"armed_vacation",
"AlarmControlPanelState.ARMED_VACATION",
"2025.11",
)
_DEPRECATED_STATE_ALARM_ARMED_CUSTOM_BYPASS: Final = DeprecatedConstant(
"armed_custom_bypass",
"AlarmControlPanelState.ARMED_CUSTOM_BYPASS",
"2025.11",
)
_DEPRECATED_STATE_ALARM_PENDING: Final = DeprecatedConstant(
"pending",
"AlarmControlPanelState.PENDING",
"2025.11",
)
_DEPRECATED_STATE_ALARM_ARMING: Final = DeprecatedConstant(
"arming",
"AlarmControlPanelState.ARMING",
"2025.11",
)
_DEPRECATED_STATE_ALARM_DISARMING: Final = DeprecatedConstant(
"disarming",
"AlarmControlPanelState.DISARMING",
"2025.11",
)
_DEPRECATED_STATE_ALARM_TRIGGERED: Final = DeprecatedConstant(
"triggered",
"AlarmControlPanelState.TRIGGERED",
"2025.11",
)
# #### STATE AND EVENT ATTRIBUTES ####
# Attribution
ATTR_ATTRIBUTION: Final = "attribution"

View File

@@ -28,8 +28,10 @@ from homeassistant.core import (
CALLBACK_TYPE,
Context,
HassJob,
HassJobType,
HomeAssistant,
callback,
get_hassjob_callable_job_type,
is_callback,
)
from homeassistant.exceptions import HomeAssistantError, TemplateError
@@ -178,6 +180,8 @@ _TRIGGER_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend(
class Trigger(abc.ABC):
"""Trigger class."""
_hass: HomeAssistant
@classmethod
async def async_validate_complete_config(
cls, hass: HomeAssistant, complete_config: ConfigType
@@ -212,14 +216,33 @@ class Trigger(abc.ABC):
def __init__(self, hass: HomeAssistant, config: TriggerConfig) -> None:
"""Initialize trigger."""
self._hass = hass
async def async_attach_action(
self,
action: TriggerAction,
action_payload_builder: TriggerActionPayloadBuilder,
) -> CALLBACK_TYPE:
"""Attach the trigger to an action."""
@callback
def run_action(
extra_trigger_payload: dict[str, Any],
description: str,
context: Context | None = None,
) -> asyncio.Task[Any]:
"""Run action with trigger variables."""
payload = action_payload_builder(extra_trigger_payload, description)
return self._hass.async_create_task(action(payload, context))
return await self.async_attach_runner(run_action)
@abc.abstractmethod
async def async_attach(
self,
action: TriggerActionType,
trigger_info: TriggerInfo,
async def async_attach_runner(
self, run_action: TriggerActionRunner
) -> CALLBACK_TYPE:
"""Attach the trigger."""
"""Attach the trigger to an action runner."""
class TriggerProtocol(Protocol):
@@ -257,7 +280,33 @@ class TriggerConfig:
options: dict[str, Any] | None = None
class TriggerActionType(Protocol):
class TriggerActionRunner(Protocol):
"""Protocol type for the trigger action runner helper callback."""
@callback
def __call__(
self,
extra_trigger_payload: dict[str, Any],
description: str,
context: Context | None = None,
) -> asyncio.Task[Any]:
"""Define trigger action runner type.
Returns:
A Task that allows awaiting for the action to finish.
"""
class TriggerActionPayloadBuilder(Protocol):
"""Protocol type for the trigger action payload builder."""
def __call__(
self, extra_trigger_payload: dict[str, Any], description: str
) -> dict[str, Any]:
"""Define trigger action payload builder type."""
class TriggerAction(Protocol):
"""Protocol type for trigger action callback."""
async def __call__(
@@ -268,6 +317,20 @@ class TriggerActionType(Protocol):
"""Define action callback type."""
class TriggerActionType(Protocol):
"""Protocol type for trigger action callback.
Contrary to TriggerAction, this type supports both sync and async callables.
"""
def __call__(
self,
run_variables: dict[str, Any],
context: Context | None = None,
) -> Coroutine[Any, Any, Any] | Any:
"""Define action callback type."""
class TriggerData(TypedDict):
"""Trigger data."""
@@ -493,6 +556,73 @@ def _trigger_action_wrapper(
return wrapper_func
async def _async_attach_trigger_cls(
hass: HomeAssistant,
trigger_cls: type[Trigger],
trigger_key: str,
conf: ConfigType,
action: Callable,
trigger_info: TriggerInfo,
) -> CALLBACK_TYPE:
"""Initialize a new Trigger class and attach it."""
def action_payload_builder(
extra_trigger_payload: dict[str, Any], description: str
) -> dict[str, Any]:
"""Build action variables."""
payload = {
"trigger": {
**trigger_info["trigger_data"],
CONF_PLATFORM: trigger_key,
"description": description,
**extra_trigger_payload,
}
}
if CONF_VARIABLES in conf:
trigger_variables = conf[CONF_VARIABLES]
payload.update(trigger_variables.async_render(hass, payload))
return payload
# Wrap sync action so that it is always async.
# This simplifies the Trigger action runner interface by always returning a coroutine,
# removing the need for integrations to check for the return type when awaiting the action.
# This can be removed when sync actions are no longer supported.
match get_hassjob_callable_job_type(action):
case HassJobType.Executor:
original_action = action
async def wrapped_executor_action(
run_variables: dict[str, Any], context: Context | None = None
) -> Any:
"""Wrap sync action to be called in executor."""
return await hass.async_add_executor_job(
original_action, run_variables, context
)
action = wrapped_executor_action
case HassJobType.Callback:
original_action = action
async def wrapped_callback_action(
run_variables: dict[str, Any], context: Context | None = None
) -> Any:
"""Wrap callback action to be awaitable."""
return original_action(run_variables, context)
action = wrapped_callback_action
trigger = trigger_cls(
hass,
TriggerConfig(
key=trigger_key,
target=conf.get(CONF_TARGET),
options=conf.get(CONF_OPTIONS),
),
)
return await trigger.async_attach_action(action, action_payload_builder)
async def async_initialize_triggers(
hass: HomeAssistant,
trigger_config: list[ConfigType],
@@ -532,23 +662,17 @@ async def async_initialize_triggers(
trigger_data=trigger_data,
)
action_wrapper = _trigger_action_wrapper(hass, action, conf)
if hasattr(platform, "async_get_triggers"):
trigger_descriptors = await platform.async_get_triggers(hass)
relative_trigger_key = get_relative_description_key(
platform_domain, trigger_key
)
trigger_cls = trigger_descriptors[relative_trigger_key]
trigger = trigger_cls(
hass,
TriggerConfig(
key=trigger_key,
target=conf.get(CONF_TARGET),
options=conf.get(CONF_OPTIONS),
),
coro = _async_attach_trigger_cls(
hass, trigger_cls, trigger_key, conf, action, info
)
coro = trigger.async_attach(action_wrapper, info)
else:
action_wrapper = _trigger_action_wrapper(hass, action, conf)
coro = platform.async_attach_trigger(hass, conf, action_wrapper, info)
triggers.append(create_eager_task(coro))

View File

@@ -6,7 +6,7 @@ aiodns==3.5.0
aiohasupervisor==0.3.3
aiohttp-asyncmdnsresolver==0.1.1
aiohttp-fast-zlib==0.3.0
aiohttp==3.13.0
aiohttp==3.12.15
aiohttp_cors==0.8.1
aiousbwatcher==1.1.1
aiozoneinfo==0.2.3
@@ -74,7 +74,7 @@ voluptuous-openapi==0.1.0
voluptuous-serialize==2.7.0
voluptuous==0.15.2
webrtc-models==0.3.0
yarl==1.22.0
yarl==1.20.1
zeroconf==0.148.0
# Constrain pycryptodome to avoid vulnerability

View File

@@ -28,7 +28,7 @@ dependencies = [
# change behavior based on presence of supervisor. Deprecated with #127228
# Lib can be removed with 2025.11
"aiohasupervisor==0.3.3",
"aiohttp==3.13.0",
"aiohttp==3.12.15",
"aiohttp_cors==0.8.1",
"aiohttp-fast-zlib==0.3.0",
"aiohttp-asyncmdnsresolver==0.1.1",
@@ -78,7 +78,7 @@ dependencies = [
"voluptuous==0.15.2",
"voluptuous-serialize==2.7.0",
"voluptuous-openapi==0.1.0",
"yarl==1.22.0",
"yarl==1.20.1",
"webrtc-models==0.3.0",
"zeroconf==0.148.0",
]
@@ -485,8 +485,6 @@ filterwarnings = [
"ignore:Deprecated call to `pkg_resources.declare_namespace\\(('azure'|'google.*'|'pywinusb'|'repoze'|'xbox'|'zope')\\)`:DeprecationWarning:pkg_resources",
# -- tracked upstream / open PRs
# https://github.com/kbr/fritzconnection/pull/244 - v1.15.0 - 2025-05-17
"ignore:.*invalid escape sequence:SyntaxWarning:.*fritzconnection.core.soaper",
# https://github.com/hacf-fr/meteofrance-api/pull/688 - v1.4.0 - 2025-03-26
"ignore:datetime.*utcnow\\(\\) is deprecated and scheduled for removal:DeprecationWarning:meteofrance_api.model.forecast",
@@ -524,8 +522,8 @@ filterwarnings = [
# https://pypi.org/project/motionblindsble/ - v0.1.3 - 2024-11-12
# https://github.com/LennP/motionblindsble/blob/0.1.3/motionblindsble/device.py#L390
"ignore:Passing additional arguments for BLEDevice is deprecated and has no effect:DeprecationWarning:motionblindsble.device",
# https://pypi.org/project/pyeconet/ - v0.2.0 - 2025-10-05
# https://github.com/w1ll1am23/pyeconet/blob/v0.2.0/src/pyeconet/api.py#L39
# https://pypi.org/project/pyeconet/ - v0.1.28 - 2025-02-15
# https://github.com/w1ll1am23/pyeconet/blob/v0.1.28/src/pyeconet/api.py#L38
"ignore:ssl.PROTOCOL_TLS is deprecated:DeprecationWarning:pyeconet.api",
# https://github.com/thecynic/pylutron - v0.2.18 - 2025-04-15
"ignore:setDaemon\\(\\) is deprecated, set the daemon attribute instead:DeprecationWarning:pylutron",
@@ -567,6 +565,7 @@ filterwarnings = [
# - SyntaxWarning - is with literal
# https://github.com/majuss/lupupy/pull/15 - >0.3.2
# https://pypi.org/project/opuslib/ - v3.0.1 - 2018-01-16
# https://pypi.org/project/plumlightpad/ - v0.0.11 - 2018-10-16
# https://pypi.org/project/pyiss/ - v1.0.1 - 2016-12-19
"ignore:\"is.*\" with '.*' literal:SyntaxWarning:importlib._bootstrap",
@@ -575,6 +574,7 @@ filterwarnings = [
"ignore:aifc was removed in Python 3.13.*'standard-aifc':DeprecationWarning:speech_recognition",
"ignore:telnetlib was removed in Python 3.13.*'standard-telnetlib':DeprecationWarning:homeassistant.components.hddtemp.sensor",
"ignore:telnetlib was removed in Python 3.13.*'standard-telnetlib':DeprecationWarning:ndms2_client.connection",
"ignore:telnetlib was removed in Python 3.13.*'standard-telnetlib':DeprecationWarning:plumlightpad.lightpad",
"ignore:telnetlib was removed in Python 3.13.*'standard-telnetlib':DeprecationWarning:pyws66i",
# -- Websockets 14.1
@@ -605,6 +605,8 @@ filterwarnings = [
"ignore:datetime.*utcnow\\(\\) is deprecated and scheduled for removal:DeprecationWarning:oauth2client.client",
# https://pypi.org/project/pilight/ - v0.1.1 - 2016-10-19
"ignore:pkg_resources is deprecated as an API:UserWarning:pilight",
# https://pypi.org/project/plumlightpad/ - v0.0.11 - 2018-10-16
"ignore:.*invalid escape sequence:SyntaxWarning:.*plumlightpad.plumdiscovery",
# https://pypi.org/project/pure-python-adb/ - v0.3.0.dev0 - 2020-08-05
"ignore:.*invalid escape sequence:SyntaxWarning:.*ppadb",
# https://pypi.org/project/pydub/ - v0.25.1 - 2021-03-10

4
requirements.txt generated
View File

@@ -5,7 +5,7 @@
# Home Assistant Core
aiodns==3.5.0
aiohasupervisor==0.3.3
aiohttp==3.13.0
aiohttp==3.12.15
aiohttp_cors==0.8.1
aiohttp-fast-zlib==0.3.0
aiohttp-asyncmdnsresolver==0.1.1
@@ -50,6 +50,6 @@ uv==0.8.9
voluptuous==0.15.2
voluptuous-serialize==2.7.0
voluptuous-openapi==0.1.0
yarl==1.22.0
yarl==1.20.1
webrtc-models==0.3.0
zeroconf==0.148.0

2
requirements_all.txt generated
View File

@@ -384,7 +384,7 @@ aioruuvigateway==0.1.0
aiosenz==1.0.0
# homeassistant.components.shelly
aioshelly==13.12.0
aioshelly==13.11.0
# homeassistant.components.skybell
aioskybell==22.7.0

View File

@@ -366,7 +366,7 @@ aioruuvigateway==0.1.0
aiosenz==1.0.0
# homeassistant.components.shelly
aioshelly==13.12.0
aioshelly==13.11.0
# homeassistant.components.skybell
aioskybell==22.7.0

View File

@@ -3,12 +3,10 @@
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, STATE_UNAVAILABLE, Platform
from homeassistant.const import STATE_OFF, STATE_ON, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
@@ -69,64 +67,3 @@ 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

View File

@@ -5,7 +5,6 @@ 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
@@ -18,20 +17,14 @@ 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, State
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from . import setup_integration
from tests.common import (
MockConfigEntry,
async_fire_time_changed,
mock_restore_cache,
snapshot_platform,
)
from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform
ENTITY_ID = "cover.cover0"
@@ -169,26 +162,37 @@ 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_restore_cache(hass, [State(ENTITY_ID, cover_state)])
mock_serial_bridge.reset_mock()
await setup_integration(hass, mock_serial_bridge_config_entry)
assert (state := hass.states.get(ENTITY_ID))
assert state.state == cover_state
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
async def test_cover_dynamic(

View File

@@ -13,7 +13,6 @@ from homeassistant.components.application_credentials import (
from homeassistant.components.mcp.const import (
CONF_ACCESS_TOKEN,
CONF_AUTHORIZATION_URL,
CONF_SCOPE,
CONF_TOKEN_URL,
DOMAIN,
)
@@ -101,7 +100,6 @@ def mock_config_entry_with_auth(
"refresh_token": "test-refresh-token",
"expires_at": config_entry_token_expiration.timestamp(),
},
CONF_SCOPE: ["read", "write"],
},
title=TEST_API_NAME,
)

View File

@@ -11,7 +11,6 @@ import respx
from homeassistant import config_entries
from homeassistant.components.mcp.const import (
CONF_AUTHORIZATION_URL,
CONF_SCOPE,
CONF_TOKEN_URL,
DOMAIN,
)
@@ -43,11 +42,9 @@ OAUTH_SERVER_METADATA_RESPONSE = httpx.Response(
{
"authorization_endpoint": OAUTH_AUTHORIZE_URL,
"token_endpoint": OAUTH_TOKEN_URL,
"scopes_supported": ["read", "write"],
}
),
)
SCOPES = ["read", "write"]
CALLBACK_PATH = "/auth/external/callback"
OAUTH_CALLBACK_URL = f"https://example.com{CALLBACK_PATH}"
OAUTH_CODE = "abcd"
@@ -56,7 +53,6 @@ OAUTH_TOKEN_PAYLOAD = {
"access_token": "mock-access-token",
"type": "Bearer",
"expires_in": 60,
"scope": " ".join(SCOPES),
}
@@ -299,7 +295,6 @@ async def perform_oauth_flow(
result: config_entries.ConfigFlowResult,
authorize_url: str = OAUTH_AUTHORIZE_URL,
token_url: str = OAUTH_TOKEN_URL,
scopes: list[str] | None = None,
) -> config_entries.ConfigFlowResult:
"""Perform the common steps of the OAuth flow.
@@ -312,13 +307,10 @@ async def perform_oauth_flow(
"redirect_uri": OAUTH_CALLBACK_URL,
},
)
scope_param = ""
if scopes:
scope_param = "&scope=" + "+".join(scopes)
assert result["url"] == (
f"{authorize_url}?response_type=code&client_id={CLIENT_ID}"
f"&redirect_uri={OAUTH_CALLBACK_URL}"
f"&state={state}{scope_param}"
f"&state={state}"
)
client = await hass_client_no_auth()
@@ -335,14 +327,9 @@ async def perform_oauth_flow(
@pytest.mark.parametrize(
(
"oauth_server_metadata_response",
"expected_authorize_url",
"expected_token_url",
"scopes",
),
("oauth_server_metadata_response", "expected_authorize_url", "expected_token_url"),
[
(OAUTH_SERVER_METADATA_RESPONSE, OAUTH_AUTHORIZE_URL, OAUTH_TOKEN_URL, SCOPES),
(OAUTH_SERVER_METADATA_RESPONSE, OAUTH_AUTHORIZE_URL, OAUTH_TOKEN_URL),
(
httpx.Response(
status_code=200,
@@ -355,13 +342,11 @@ async def perform_oauth_flow(
),
f"{MCP_SERVER_BASE_URL}/authorize-path",
f"{MCP_SERVER_BASE_URL}/token-path",
None,
),
(
httpx.Response(status_code=404),
f"{MCP_SERVER_BASE_URL}/authorize",
f"{MCP_SERVER_BASE_URL}/token",
None,
),
],
ids=(
@@ -382,7 +367,6 @@ async def test_authentication_flow(
oauth_server_metadata_response: httpx.Response,
expected_authorize_url: str,
expected_token_url: str,
scopes: list[str] | None,
) -> None:
"""Test for an OAuth authentication flow for an MCP server."""
@@ -421,7 +405,6 @@ async def test_authentication_flow(
result,
authorize_url=expected_authorize_url,
token_url=expected_token_url,
scopes=scopes,
)
# Client now accepts credentials
@@ -440,7 +423,6 @@ async def test_authentication_flow(
CONF_URL: MCP_SERVER_URL,
CONF_AUTHORIZATION_URL: expected_authorize_url,
CONF_TOKEN_URL: expected_token_url,
CONF_SCOPE: scopes,
}
assert token
token.pop("expires_at")
@@ -554,7 +536,6 @@ async def test_authentication_flow_server_failure_abort(
aioclient_mock,
hass_client_no_auth,
result,
scopes=SCOPES,
)
# Client fails with an error
@@ -610,7 +591,6 @@ async def test_authentication_flow_server_missing_tool_capabilities(
aioclient_mock,
hass_client_no_auth,
result,
scopes=SCOPES,
)
# Client can now authenticate
@@ -648,9 +628,7 @@ async def test_reauth_flow(
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
result = await perform_oauth_flow(
hass, aioclient_mock, hass_client_no_auth, result, scopes=SCOPES
)
result = await perform_oauth_flow(hass, aioclient_mock, hass_client_no_auth, result)
# Verify we can connect to the server
response = Mock()
@@ -670,7 +648,6 @@ async def test_reauth_flow(
CONF_URL: MCP_SERVER_URL,
CONF_AUTHORIZATION_URL: OAUTH_AUTHORIZE_URL,
CONF_TOKEN_URL: OAUTH_TOKEN_URL,
CONF_SCOPE: ["read", "write"],
}
assert token
token.pop("expires_at")

View File

@@ -48,10 +48,6 @@ def mock_nintendo_authenticator() -> Generator[MagicMock]:
"homeassistant.components.nintendo_parental.config_flow.Authenticator",
new=mock_auth_class,
),
patch(
"homeassistant.components.nintendo_parental.coordinator.NintendoParental.update",
return_value=None,
),
):
mock_auth = MagicMock()
mock_auth._id_token = API_TOKEN

View File

@@ -94,7 +94,6 @@ async def test_service_call(
[
(NordPoolAuthenticationError, "authentication_error"),
(NordPoolError, "connection_error"),
(TimeoutError, "connection_error"),
],
)
@pytest.mark.freeze_time("2025-10-01T18:00:00+00:00")

View File

@@ -17,17 +17,14 @@ from pyopenweathermap import (
from pyopenweathermap.client.owm_abstract_client import OWMClient
import pytest
from homeassistant.components.openweathermap.const import (
DEFAULT_LANGUAGE,
DEFAULT_NAME,
DOMAIN,
)
from homeassistant.components.openweathermap.const import DEFAULT_LANGUAGE, DOMAIN
from homeassistant.const import (
CONF_API_KEY,
CONF_LANGUAGE,
CONF_LATITUDE,
CONF_LONGITUDE,
CONF_MODE,
CONF_NAME,
)
from tests.common import MockConfigEntry, patch
@@ -53,6 +50,7 @@ def mock_config_entry(mode: str) -> MockConfigEntry:
CONF_API_KEY: API_KEY,
CONF_LATITUDE: LATITUDE,
CONF_LONGITUDE: LONGITUDE,
CONF_NAME: NAME,
},
options={
CONF_MODE: mode,
@@ -61,7 +59,6 @@ def mock_config_entry(mode: str) -> MockConfigEntry:
entry_id="test",
version=5,
unique_id=f"{LATITUDE}-{LONGITUDE}",
title=DEFAULT_NAME,
)

View File

@@ -41,7 +41,7 @@
'attributes': ReadOnlyDict({
'attribution': 'Data provided by OpenWeatherMap',
'device_class': 'aqi',
'friendly_name': 'OpenWeatherMap Air quality index',
'friendly_name': 'openweathermap Air quality index',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'context': <ANY>,
@@ -94,7 +94,7 @@
'attributes': ReadOnlyDict({
'attribution': 'Data provided by OpenWeatherMap',
'device_class': 'carbon_monoxide',
'friendly_name': 'OpenWeatherMap Carbon monoxide',
'friendly_name': 'openweathermap Carbon monoxide',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'μg/m³',
}),
@@ -148,7 +148,7 @@
'attributes': ReadOnlyDict({
'attribution': 'Data provided by OpenWeatherMap',
'device_class': 'nitrogen_dioxide',
'friendly_name': 'OpenWeatherMap Nitrogen dioxide',
'friendly_name': 'openweathermap Nitrogen dioxide',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'μg/m³',
}),
@@ -202,7 +202,7 @@
'attributes': ReadOnlyDict({
'attribution': 'Data provided by OpenWeatherMap',
'device_class': 'nitrogen_monoxide',
'friendly_name': 'OpenWeatherMap Nitrogen monoxide',
'friendly_name': 'openweathermap Nitrogen monoxide',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'μg/m³',
}),
@@ -256,7 +256,7 @@
'attributes': ReadOnlyDict({
'attribution': 'Data provided by OpenWeatherMap',
'device_class': 'ozone',
'friendly_name': 'OpenWeatherMap Ozone',
'friendly_name': 'openweathermap Ozone',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'μg/m³',
}),
@@ -310,7 +310,7 @@
'attributes': ReadOnlyDict({
'attribution': 'Data provided by OpenWeatherMap',
'device_class': 'pm10',
'friendly_name': 'OpenWeatherMap PM10',
'friendly_name': 'openweathermap PM10',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'μg/m³',
}),
@@ -364,7 +364,7 @@
'attributes': ReadOnlyDict({
'attribution': 'Data provided by OpenWeatherMap',
'device_class': 'pm25',
'friendly_name': 'OpenWeatherMap PM2.5',
'friendly_name': 'openweathermap PM2.5',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'μg/m³',
}),
@@ -418,7 +418,7 @@
'attributes': ReadOnlyDict({
'attribution': 'Data provided by OpenWeatherMap',
'device_class': 'sulphur_dioxide',
'friendly_name': 'OpenWeatherMap Sulphur dioxide',
'friendly_name': 'openweathermap Sulphur dioxide',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'μg/m³',
}),
@@ -471,7 +471,7 @@
StateSnapshot({
'attributes': ReadOnlyDict({
'attribution': 'Data provided by OpenWeatherMap',
'friendly_name': 'OpenWeatherMap Cloud coverage',
'friendly_name': 'openweathermap Cloud coverage',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': '%',
}),
@@ -522,7 +522,7 @@
StateSnapshot({
'attributes': ReadOnlyDict({
'attribution': 'Data provided by OpenWeatherMap',
'friendly_name': 'OpenWeatherMap Condition',
'friendly_name': 'openweathermap Condition',
}),
'context': <ANY>,
'entity_id': 'sensor.openweathermap_condition',
@@ -577,7 +577,7 @@
'attributes': ReadOnlyDict({
'attribution': 'Data provided by OpenWeatherMap',
'device_class': 'temperature',
'friendly_name': 'OpenWeatherMap Dew Point',
'friendly_name': 'openweathermap Dew Point',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
}),
@@ -634,7 +634,7 @@
'attributes': ReadOnlyDict({
'attribution': 'Data provided by OpenWeatherMap',
'device_class': 'temperature',
'friendly_name': 'OpenWeatherMap Feels like temperature',
'friendly_name': 'openweathermap Feels like temperature',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
}),
@@ -688,7 +688,7 @@
'attributes': ReadOnlyDict({
'attribution': 'Data provided by OpenWeatherMap',
'device_class': 'humidity',
'friendly_name': 'OpenWeatherMap Humidity',
'friendly_name': 'openweathermap Humidity',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': '%',
}),
@@ -739,7 +739,7 @@
StateSnapshot({
'attributes': ReadOnlyDict({
'attribution': 'Data provided by OpenWeatherMap',
'friendly_name': 'OpenWeatherMap Precipitation kind',
'friendly_name': 'openweathermap Precipitation kind',
}),
'context': <ANY>,
'entity_id': 'sensor.openweathermap_precipitation_kind',
@@ -774,7 +774,7 @@
'name': None,
'options': dict({
'sensor': dict({
'suggested_display_precision': 0,
'suggested_display_precision': 2,
}),
}),
'original_device_class': <SensorDeviceClass.PRESSURE: 'pressure'>,
@@ -794,7 +794,7 @@
'attributes': ReadOnlyDict({
'attribution': 'Data provided by OpenWeatherMap',
'device_class': 'pressure',
'friendly_name': 'OpenWeatherMap Pressure',
'friendly_name': 'openweathermap Pressure',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfPressure.HPA: 'hPa'>,
}),
@@ -851,7 +851,7 @@
'attributes': ReadOnlyDict({
'attribution': 'Data provided by OpenWeatherMap',
'device_class': 'precipitation_intensity',
'friendly_name': 'OpenWeatherMap Rain',
'friendly_name': 'openweathermap Rain',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR: 'mm/h'>,
}),
@@ -908,7 +908,7 @@
'attributes': ReadOnlyDict({
'attribution': 'Data provided by OpenWeatherMap',
'device_class': 'precipitation_intensity',
'friendly_name': 'OpenWeatherMap Snow',
'friendly_name': 'openweathermap Snow',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR: 'mm/h'>,
}),
@@ -965,7 +965,7 @@
'attributes': ReadOnlyDict({
'attribution': 'Data provided by OpenWeatherMap',
'device_class': 'temperature',
'friendly_name': 'OpenWeatherMap Temperature',
'friendly_name': 'openweathermap Temperature',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
}),
@@ -1018,7 +1018,7 @@
StateSnapshot({
'attributes': ReadOnlyDict({
'attribution': 'Data provided by OpenWeatherMap',
'friendly_name': 'OpenWeatherMap UV Index',
'friendly_name': 'openweathermap UV Index',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'UV index',
}),
@@ -1055,7 +1055,7 @@
'name': None,
'options': dict({
'sensor': dict({
'suggested_display_precision': 0,
'suggested_display_precision': 2,
}),
}),
'original_device_class': <SensorDeviceClass.DISTANCE: 'distance'>,
@@ -1075,7 +1075,7 @@
'attributes': ReadOnlyDict({
'attribution': 'Data provided by OpenWeatherMap',
'device_class': 'distance',
'friendly_name': 'OpenWeatherMap Visibility',
'friendly_name': 'openweathermap Visibility',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfLength.METERS: 'm'>,
}),
@@ -1126,7 +1126,7 @@
StateSnapshot({
'attributes': ReadOnlyDict({
'attribution': 'Data provided by OpenWeatherMap',
'friendly_name': 'OpenWeatherMap Weather',
'friendly_name': 'openweathermap Weather',
}),
'context': <ANY>,
'entity_id': 'sensor.openweathermap_weather',
@@ -1175,7 +1175,7 @@
StateSnapshot({
'attributes': ReadOnlyDict({
'attribution': 'Data provided by OpenWeatherMap',
'friendly_name': 'OpenWeatherMap Weather Code',
'friendly_name': 'openweathermap Weather Code',
}),
'context': <ANY>,
'entity_id': 'sensor.openweathermap_weather_code',
@@ -1227,7 +1227,7 @@
'attributes': ReadOnlyDict({
'attribution': 'Data provided by OpenWeatherMap',
'device_class': 'wind_direction',
'friendly_name': 'OpenWeatherMap Wind bearing',
'friendly_name': 'openweathermap Wind bearing',
'state_class': <SensorStateClass.MEASUREMENT_ANGLE: 'measurement_angle'>,
'unit_of_measurement': '°',
}),
@@ -1287,7 +1287,7 @@
'attributes': ReadOnlyDict({
'attribution': 'Data provided by OpenWeatherMap',
'device_class': 'wind_speed',
'friendly_name': 'OpenWeatherMap Wind gust',
'friendly_name': 'openweathermap Wind gust',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfSpeed.KILOMETERS_PER_HOUR: 'km/h'>,
}),
@@ -1347,7 +1347,7 @@
'attributes': ReadOnlyDict({
'attribution': 'Data provided by OpenWeatherMap',
'device_class': 'wind_speed',
'friendly_name': 'OpenWeatherMap Wind speed',
'friendly_name': 'openweathermap Wind speed',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfSpeed.KILOMETERS_PER_HOUR: 'km/h'>,
}),
@@ -1400,7 +1400,7 @@
StateSnapshot({
'attributes': ReadOnlyDict({
'attribution': 'Data provided by OpenWeatherMap',
'friendly_name': 'OpenWeatherMap Cloud coverage',
'friendly_name': 'openweathermap Cloud coverage',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': '%',
}),
@@ -1451,7 +1451,7 @@
StateSnapshot({
'attributes': ReadOnlyDict({
'attribution': 'Data provided by OpenWeatherMap',
'friendly_name': 'OpenWeatherMap Condition',
'friendly_name': 'openweathermap Condition',
}),
'context': <ANY>,
'entity_id': 'sensor.openweathermap_condition',
@@ -1506,7 +1506,7 @@
'attributes': ReadOnlyDict({
'attribution': 'Data provided by OpenWeatherMap',
'device_class': 'temperature',
'friendly_name': 'OpenWeatherMap Dew Point',
'friendly_name': 'openweathermap Dew Point',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
}),
@@ -1563,7 +1563,7 @@
'attributes': ReadOnlyDict({
'attribution': 'Data provided by OpenWeatherMap',
'device_class': 'temperature',
'friendly_name': 'OpenWeatherMap Feels like temperature',
'friendly_name': 'openweathermap Feels like temperature',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
}),
@@ -1617,7 +1617,7 @@
'attributes': ReadOnlyDict({
'attribution': 'Data provided by OpenWeatherMap',
'device_class': 'humidity',
'friendly_name': 'OpenWeatherMap Humidity',
'friendly_name': 'openweathermap Humidity',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': '%',
}),
@@ -1668,7 +1668,7 @@
StateSnapshot({
'attributes': ReadOnlyDict({
'attribution': 'Data provided by OpenWeatherMap',
'friendly_name': 'OpenWeatherMap Precipitation kind',
'friendly_name': 'openweathermap Precipitation kind',
}),
'context': <ANY>,
'entity_id': 'sensor.openweathermap_precipitation_kind',
@@ -1703,7 +1703,7 @@
'name': None,
'options': dict({
'sensor': dict({
'suggested_display_precision': 0,
'suggested_display_precision': 2,
}),
}),
'original_device_class': <SensorDeviceClass.PRESSURE: 'pressure'>,
@@ -1723,7 +1723,7 @@
'attributes': ReadOnlyDict({
'attribution': 'Data provided by OpenWeatherMap',
'device_class': 'pressure',
'friendly_name': 'OpenWeatherMap Pressure',
'friendly_name': 'openweathermap Pressure',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfPressure.HPA: 'hPa'>,
}),
@@ -1780,7 +1780,7 @@
'attributes': ReadOnlyDict({
'attribution': 'Data provided by OpenWeatherMap',
'device_class': 'precipitation_intensity',
'friendly_name': 'OpenWeatherMap Rain',
'friendly_name': 'openweathermap Rain',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR: 'mm/h'>,
}),
@@ -1837,7 +1837,7 @@
'attributes': ReadOnlyDict({
'attribution': 'Data provided by OpenWeatherMap',
'device_class': 'precipitation_intensity',
'friendly_name': 'OpenWeatherMap Snow',
'friendly_name': 'openweathermap Snow',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR: 'mm/h'>,
}),
@@ -1894,7 +1894,7 @@
'attributes': ReadOnlyDict({
'attribution': 'Data provided by OpenWeatherMap',
'device_class': 'temperature',
'friendly_name': 'OpenWeatherMap Temperature',
'friendly_name': 'openweathermap Temperature',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
}),
@@ -1947,7 +1947,7 @@
StateSnapshot({
'attributes': ReadOnlyDict({
'attribution': 'Data provided by OpenWeatherMap',
'friendly_name': 'OpenWeatherMap UV Index',
'friendly_name': 'openweathermap UV Index',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'UV index',
}),
@@ -1984,7 +1984,7 @@
'name': None,
'options': dict({
'sensor': dict({
'suggested_display_precision': 0,
'suggested_display_precision': 2,
}),
}),
'original_device_class': <SensorDeviceClass.DISTANCE: 'distance'>,
@@ -2004,7 +2004,7 @@
'attributes': ReadOnlyDict({
'attribution': 'Data provided by OpenWeatherMap',
'device_class': 'distance',
'friendly_name': 'OpenWeatherMap Visibility',
'friendly_name': 'openweathermap Visibility',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfLength.METERS: 'm'>,
}),
@@ -2055,7 +2055,7 @@
StateSnapshot({
'attributes': ReadOnlyDict({
'attribution': 'Data provided by OpenWeatherMap',
'friendly_name': 'OpenWeatherMap Weather',
'friendly_name': 'openweathermap Weather',
}),
'context': <ANY>,
'entity_id': 'sensor.openweathermap_weather',
@@ -2104,7 +2104,7 @@
StateSnapshot({
'attributes': ReadOnlyDict({
'attribution': 'Data provided by OpenWeatherMap',
'friendly_name': 'OpenWeatherMap Weather Code',
'friendly_name': 'openweathermap Weather Code',
}),
'context': <ANY>,
'entity_id': 'sensor.openweathermap_weather_code',
@@ -2156,7 +2156,7 @@
'attributes': ReadOnlyDict({
'attribution': 'Data provided by OpenWeatherMap',
'device_class': 'wind_direction',
'friendly_name': 'OpenWeatherMap Wind bearing',
'friendly_name': 'openweathermap Wind bearing',
'state_class': <SensorStateClass.MEASUREMENT_ANGLE: 'measurement_angle'>,
'unit_of_measurement': '°',
}),
@@ -2216,7 +2216,7 @@
'attributes': ReadOnlyDict({
'attribution': 'Data provided by OpenWeatherMap',
'device_class': 'wind_speed',
'friendly_name': 'OpenWeatherMap Wind gust',
'friendly_name': 'openweathermap Wind gust',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfSpeed.KILOMETERS_PER_HOUR: 'km/h'>,
}),
@@ -2276,7 +2276,7 @@
'attributes': ReadOnlyDict({
'attribution': 'Data provided by OpenWeatherMap',
'device_class': 'wind_speed',
'friendly_name': 'OpenWeatherMap Wind speed',
'friendly_name': 'openweathermap Wind speed',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfSpeed.KILOMETERS_PER_HOUR: 'km/h'>,
}),

View File

@@ -65,7 +65,7 @@
'attribution': 'Data provided by OpenWeatherMap',
'cloud_coverage': 75,
'dew_point': 4.0,
'friendly_name': 'OpenWeatherMap',
'friendly_name': 'openweathermap',
'humidity': 82,
'precipitation_unit': <UnitOfPrecipitationDepth.MILLIMETERS: 'mm'>,
'pressure': 1000.0,
@@ -129,7 +129,7 @@
'attribution': 'Data provided by OpenWeatherMap',
'cloud_coverage': 75,
'dew_point': 4.0,
'friendly_name': 'OpenWeatherMap',
'friendly_name': 'openweathermap',
'humidity': 82,
'precipitation_unit': <UnitOfPrecipitationDepth.MILLIMETERS: 'mm'>,
'pressure': 1000.0,
@@ -194,7 +194,7 @@
'attribution': 'Data provided by OpenWeatherMap',
'cloud_coverage': 75,
'dew_point': 4.0,
'friendly_name': 'OpenWeatherMap',
'friendly_name': 'openweathermap',
'humidity': 82,
'precipitation_unit': <UnitOfPrecipitationDepth.MILLIMETERS: 'mm'>,
'pressure': 1000.0,

View File

@@ -7,7 +7,6 @@ import pytest
from homeassistant.components.openweathermap.const import (
DEFAULT_LANGUAGE,
DEFAULT_NAME,
DEFAULT_OWM_MODE,
DOMAIN,
OWM_MODE_V30,
@@ -17,9 +16,9 @@ from homeassistant.const import (
CONF_API_KEY,
CONF_LANGUAGE,
CONF_LATITUDE,
CONF_LOCATION,
CONF_LONGITUDE,
CONF_MODE,
CONF_NAME,
)
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
@@ -29,6 +28,7 @@ from .conftest import LATITUDE, LONGITUDE
from tests.common import MockConfigEntry
CONFIG = {
CONF_NAME: "openweathermap",
CONF_API_KEY: "foo",
CONF_LATITUDE: LATITUDE,
CONF_LONGITUDE: LONGITUDE,
@@ -36,13 +36,6 @@ CONFIG = {
CONF_MODE: OWM_MODE_V30,
}
USER_INPUT = {
CONF_API_KEY: "foo",
CONF_LOCATION: {CONF_LATITUDE: LATITUDE, CONF_LONGITUDE: LONGITUDE},
CONF_LANGUAGE: DEFAULT_LANGUAGE,
CONF_MODE: OWM_MODE_V30,
}
VALID_YAML_CONFIG = {CONF_API_KEY: "foo"}
@@ -54,32 +47,31 @@ async def test_successful_config_flow(
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "user"
assert result["errors"] == {}
# create entry
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
USER_INPUT,
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}, data=CONFIG
)
await hass.async_block_till_done()
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["title"] == DEFAULT_NAME
assert result["data"][CONF_LATITUDE] == USER_INPUT[CONF_LOCATION][CONF_LATITUDE]
assert result["data"][CONF_LONGITUDE] == USER_INPUT[CONF_LOCATION][CONF_LONGITUDE]
assert result["data"][CONF_API_KEY] == USER_INPUT[CONF_API_KEY]
# validate entry state
await hass.async_block_till_done()
conf_entries = hass.config_entries.async_entries(DOMAIN)
entry = conf_entries[0]
assert entry.state is ConfigEntryState.LOADED
# unload entry
await hass.config_entries.async_unload(conf_entries[0].entry_id)
await hass.async_block_till_done()
assert entry.state is ConfigEntryState.NOT_LOADED
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["title"] == CONFIG[CONF_NAME]
assert result["data"][CONF_LATITUDE] == CONFIG[CONF_LATITUDE]
assert result["data"][CONF_LONGITUDE] == CONFIG[CONF_LONGITUDE]
assert result["data"][CONF_API_KEY] == CONFIG[CONF_API_KEY]
@pytest.mark.parametrize("mode", [OWM_MODE_V30], indirect=True)
async def test_abort_config_flow(
@@ -92,14 +84,13 @@ async def test_abort_config_flow(
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "user"
assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
USER_INPUT,
)
result = await hass.config_entries.flow.async_configure(result["flow_id"], CONFIG)
assert result["type"] is FlowResultType.ABORT
@@ -165,26 +156,19 @@ async def test_form_invalid_api_key(
owm_client_mock: AsyncMock,
) -> None:
"""Test that the form is served with no input."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "user"
assert result["errors"] == {}
# invalid api key
owm_client_mock.validate_key.return_value = False
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
USER_INPUT,
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}, data=CONFIG
)
assert result["type"] is FlowResultType.FORM
assert result["errors"] == {"base": "invalid_api_key"}
# valid api key
owm_client_mock.validate_key.return_value = True
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
USER_INPUT,
result["flow_id"], user_input=CONFIG
)
assert result["type"] is FlowResultType.CREATE_ENTRY
@@ -193,23 +177,17 @@ async def test_form_api_call_error(
owm_client_mock: AsyncMock,
) -> None:
"""Test setting up with api call error."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
assert result["type"] is FlowResultType.FORM
assert result["errors"] == {}
# simulate api call error
owm_client_mock.validate_key.side_effect = RequestError("oops")
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
USER_INPUT,
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}, data=CONFIG
)
assert result["type"] is FlowResultType.FORM
assert result["errors"] == {"base": "cannot_connect"}
# simulate successful api call
owm_client_mock.validate_key.side_effect = None
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
USER_INPUT,
result["flow_id"], user_input=CONFIG
)
assert result["type"] is FlowResultType.CREATE_ENTRY

View File

@@ -3071,7 +3071,7 @@
'state': 'unknown',
})
# ---
# name: test_shelly_pro_3em[sensor.test_name_phase_a_power-entry]
# name: test_shelly_pro_3em[sensor.test_name_phase_a_active_power-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
@@ -3086,7 +3086,7 @@
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.test_name_phase_a_power',
'entity_id': 'sensor.test_name_phase_a_active_power',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
@@ -3101,7 +3101,7 @@
}),
'original_device_class': <SensorDeviceClass.POWER: 'power'>,
'original_icon': None,
'original_name': 'Power',
'original_name': 'Active power',
'platform': 'shelly',
'previous_unique_id': None,
'suggested_object_id': None,
@@ -3111,16 +3111,16 @@
'unit_of_measurement': <UnitOfPower.WATT: 'W'>,
})
# ---
# name: test_shelly_pro_3em[sensor.test_name_phase_a_power-state]
# name: test_shelly_pro_3em[sensor.test_name_phase_a_active_power-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'power',
'friendly_name': 'Test name Phase A Power',
'friendly_name': 'Test name Phase A Active power',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfPower.WATT: 'W'>,
}),
'context': <ANY>,
'entity_id': 'sensor.test_name_phase_a_power',
'entity_id': 'sensor.test_name_phase_a_active_power',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
@@ -3521,7 +3521,7 @@
'state': '227.0',
})
# ---
# name: test_shelly_pro_3em[sensor.test_name_phase_b_power-entry]
# name: test_shelly_pro_3em[sensor.test_name_phase_b_active_power-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
@@ -3536,7 +3536,7 @@
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.test_name_phase_b_power',
'entity_id': 'sensor.test_name_phase_b_active_power',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
@@ -3551,7 +3551,7 @@
}),
'original_device_class': <SensorDeviceClass.POWER: 'power'>,
'original_icon': None,
'original_name': 'Power',
'original_name': 'Active power',
'platform': 'shelly',
'previous_unique_id': None,
'suggested_object_id': None,
@@ -3561,16 +3561,16 @@
'unit_of_measurement': <UnitOfPower.WATT: 'W'>,
})
# ---
# name: test_shelly_pro_3em[sensor.test_name_phase_b_power-state]
# name: test_shelly_pro_3em[sensor.test_name_phase_b_active_power-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'power',
'friendly_name': 'Test name Phase B Power',
'friendly_name': 'Test name Phase B Active power',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfPower.WATT: 'W'>,
}),
'context': <ANY>,
'entity_id': 'sensor.test_name_phase_b_power',
'entity_id': 'sensor.test_name_phase_b_active_power',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
@@ -3971,7 +3971,7 @@
'state': '230.0',
})
# ---
# name: test_shelly_pro_3em[sensor.test_name_phase_c_power-entry]
# name: test_shelly_pro_3em[sensor.test_name_phase_c_active_power-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
@@ -3986,7 +3986,7 @@
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.test_name_phase_c_power',
'entity_id': 'sensor.test_name_phase_c_active_power',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
@@ -4001,7 +4001,7 @@
}),
'original_device_class': <SensorDeviceClass.POWER: 'power'>,
'original_icon': None,
'original_name': 'Power',
'original_name': 'Active power',
'platform': 'shelly',
'previous_unique_id': None,
'suggested_object_id': None,
@@ -4011,16 +4011,16 @@
'unit_of_measurement': <UnitOfPower.WATT: 'W'>,
})
# ---
# name: test_shelly_pro_3em[sensor.test_name_phase_c_power-state]
# name: test_shelly_pro_3em[sensor.test_name_phase_c_active_power-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'power',
'friendly_name': 'Test name Phase C Power',
'friendly_name': 'Test name Phase C Active power',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfPower.WATT: 'W'>,
}),
'context': <ANY>,
'entity_id': 'sensor.test_name_phase_c_power',
'entity_id': 'sensor.test_name_phase_c_active_power',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
@@ -4421,7 +4421,7 @@
'state': '230.2',
})
# ---
# name: test_shelly_pro_3em[sensor.test_name_neutral_current-entry]
# name: test_shelly_pro_3em[sensor.test_name_phase_n_current-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
@@ -4436,7 +4436,7 @@
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.test_name_neutral_current',
'entity_id': 'sensor.test_name_phase_n_current',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
@@ -4451,7 +4451,7 @@
}),
'original_device_class': <SensorDeviceClass.CURRENT: 'current'>,
'original_icon': None,
'original_name': 'Neutral current',
'original_name': 'Phase N current',
'platform': 'shelly',
'previous_unique_id': None,
'suggested_object_id': None,
@@ -4461,16 +4461,16 @@
'unit_of_measurement': <UnitOfElectricCurrent.AMPERE: 'A'>,
})
# ---
# name: test_shelly_pro_3em[sensor.test_name_neutral_current-state]
# name: test_shelly_pro_3em[sensor.test_name_phase_n_current-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'current',
'friendly_name': 'Test name Neutral current',
'friendly_name': 'Test name Phase N current',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfElectricCurrent.AMPERE: 'A'>,
}),
'context': <ANY>,
'entity_id': 'sensor.test_name_neutral_current',
'entity_id': 'sensor.test_name_phase_n_current',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
@@ -4645,7 +4645,7 @@
'state': '5415.41419',
})
# ---
# name: test_shelly_pro_3em[sensor.test_name_power-entry]
# name: test_shelly_pro_3em[sensor.test_name_total_active_power-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
@@ -4660,7 +4660,7 @@
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.test_name_power',
'entity_id': 'sensor.test_name_total_active_power',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
@@ -4675,7 +4675,7 @@
}),
'original_device_class': <SensorDeviceClass.POWER: 'power'>,
'original_icon': None,
'original_name': 'Power',
'original_name': 'Total active power',
'platform': 'shelly',
'previous_unique_id': None,
'suggested_object_id': None,
@@ -4685,16 +4685,16 @@
'unit_of_measurement': <UnitOfPower.WATT: 'W'>,
})
# ---
# name: test_shelly_pro_3em[sensor.test_name_power-state]
# name: test_shelly_pro_3em[sensor.test_name_total_active_power-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'power',
'friendly_name': 'Test name Power',
'friendly_name': 'Test name Total active power',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfPower.WATT: 'W'>,
}),
'context': <ANY>,
'entity_id': 'sensor.test_name_power',
'entity_id': 'sensor.test_name_total_active_power',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
@@ -4760,7 +4760,7 @@
'state': '0.0',
})
# ---
# name: test_shelly_pro_3em[sensor.test_name_apparent_power-entry]
# name: test_shelly_pro_3em[sensor.test_name_total_apparent_power-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
@@ -4775,7 +4775,7 @@
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.test_name_apparent_power',
'entity_id': 'sensor.test_name_total_apparent_power',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
@@ -4790,7 +4790,7 @@
}),
'original_device_class': <SensorDeviceClass.APPARENT_POWER: 'apparent_power'>,
'original_icon': None,
'original_name': 'Apparent power',
'original_name': 'Total apparent power',
'platform': 'shelly',
'previous_unique_id': None,
'suggested_object_id': None,
@@ -4800,23 +4800,23 @@
'unit_of_measurement': <UnitOfApparentPower.VOLT_AMPERE: 'VA'>,
})
# ---
# name: test_shelly_pro_3em[sensor.test_name_apparent_power-state]
# name: test_shelly_pro_3em[sensor.test_name_total_apparent_power-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'apparent_power',
'friendly_name': 'Test name Apparent power',
'friendly_name': 'Test name Total apparent power',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfApparentPower.VOLT_AMPERE: 'VA'>,
}),
'context': <ANY>,
'entity_id': 'sensor.test_name_apparent_power',
'entity_id': 'sensor.test_name_total_apparent_power',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '2525.779',
})
# ---
# name: test_shelly_pro_3em[sensor.test_name_current-entry]
# name: test_shelly_pro_3em[sensor.test_name_total_current-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
@@ -4831,7 +4831,7 @@
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.test_name_current',
'entity_id': 'sensor.test_name_total_current',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
@@ -4846,7 +4846,7 @@
}),
'original_device_class': <SensorDeviceClass.CURRENT: 'current'>,
'original_icon': None,
'original_name': 'Current',
'original_name': 'Total current',
'platform': 'shelly',
'previous_unique_id': None,
'suggested_object_id': None,
@@ -4856,16 +4856,16 @@
'unit_of_measurement': <UnitOfElectricCurrent.AMPERE: 'A'>,
})
# ---
# name: test_shelly_pro_3em[sensor.test_name_current-state]
# name: test_shelly_pro_3em[sensor.test_name_total_current-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'current',
'friendly_name': 'Test name Current',
'friendly_name': 'Test name Total current',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfElectricCurrent.AMPERE: 'A'>,
}),
'context': <ANY>,
'entity_id': 'sensor.test_name_current',
'entity_id': 'sensor.test_name_total_current',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,

View File

@@ -440,7 +440,7 @@ async def test_rpc_device_virtual_binary_sensor(
assert state.state == STATE_ON
assert (entry := entity_registry.async_get(entity_id))
assert entry.unique_id == "123456789ABC-boolean:203-boolean_generic"
assert entry.unique_id == "123456789ABC-boolean:203-boolean"
monkeypatch.setitem(mock_rpc_device.status["boolean:203"], "value", False)
mock_rpc_device.mock_update()
@@ -472,7 +472,7 @@ async def test_rpc_remove_virtual_binary_sensor_when_mode_toggle(
hass,
BINARY_SENSOR_DOMAIN,
"test_name_boolean_200",
"boolean:200-boolean_generic",
"boolean:200-boolean",
config_entry,
device_id=device_entry.id,
)
@@ -498,7 +498,7 @@ async def test_rpc_remove_virtual_binary_sensor_when_orphaned(
hass,
BINARY_SENSOR_DOMAIN,
"test_name_boolean_200",
"boolean:200-boolean_generic",
"boolean:200-boolean",
config_entry,
device_id=device_entry.id,
)
@@ -507,13 +507,13 @@ async def test_rpc_remove_virtual_binary_sensor_when_orphaned(
sub_device_entry = register_sub_device(
device_registry,
config_entry,
"boolean:201-boolean_generic",
"boolean:201-boolean",
)
entity_id2 = register_entity(
hass,
BINARY_SENSOR_DOMAIN,
"boolean_201",
"boolean:201-boolean_generic",
"boolean:201-boolean",
config_entry,
device_id=sub_device_entry.id,
)

View File

@@ -347,7 +347,7 @@ async def test_shelly_pro_3em(
config_entry = await init_integration(hass, gen=2, model=MODEL_PRO_EM3)
# Main device
entity_id = "sensor.test_name_power"
entity_id = "sensor.test_name_total_active_power"
state = hass.states.get(entity_id)
assert state
@@ -360,7 +360,7 @@ async def test_shelly_pro_3em(
assert device_entry.name == "Test name"
# Phase A sub-device
entity_id = "sensor.test_name_phase_a_power"
entity_id = "sensor.test_name_phase_a_active_power"
state = hass.states.get(entity_id)
assert state
@@ -373,7 +373,7 @@ async def test_shelly_pro_3em(
assert device_entry.name == "Test name Phase A"
# Phase B sub-device
entity_id = "sensor.test_name_phase_b_power"
entity_id = "sensor.test_name_phase_b_active_power"
state = hass.states.get(entity_id)
assert state
@@ -386,7 +386,7 @@ async def test_shelly_pro_3em(
assert device_entry.name == "Test name Phase B"
# Phase C sub-device
entity_id = "sensor.test_name_phase_c_power"
entity_id = "sensor.test_name_phase_c_active_power"
state = hass.states.get(entity_id)
assert state
@@ -423,7 +423,7 @@ async def test_shelly_pro_3em_with_emeter_name(
await init_integration(hass, gen=2, model=MODEL_PRO_EM3)
# Main device
entity_id = "sensor.test_name_power"
entity_id = "sensor.test_name_total_active_power"
state = hass.states.get(entity_id)
assert state
@@ -436,7 +436,7 @@ async def test_shelly_pro_3em_with_emeter_name(
assert device_entry.name == "Test name"
# Phase A sub-device
entity_id = "sensor.test_name_phase_a_power"
entity_id = "sensor.test_name_phase_a_active_power"
state = hass.states.get(entity_id)
assert state
@@ -449,7 +449,7 @@ async def test_shelly_pro_3em_with_emeter_name(
assert device_entry.name == "Test name Phase A"
# Phase B sub-device
entity_id = "sensor.test_name_phase_b_power"
entity_id = "sensor.test_name_phase_b_active_power"
state = hass.states.get(entity_id)
assert state
@@ -462,7 +462,7 @@ async def test_shelly_pro_3em_with_emeter_name(
assert device_entry.name == "Test name Phase B"
# Phase C sub-device
entity_id = "sensor.test_name_phase_c_power"
entity_id = "sensor.test_name_phase_c_active_power"
state = hass.states.get(entity_id)
assert state

View File

@@ -331,7 +331,7 @@ async def test_rpc_device_virtual_number(
assert state.attributes.get(ATTR_MODE) is mode
assert (entry := entity_registry.async_get(entity_id))
assert entry.unique_id == "123456789ABC-number:203-number_generic"
assert entry.unique_id == "123456789ABC-number:203-number"
monkeypatch.setitem(mock_rpc_device.status["number:203"], "value", 78.9)
mock_rpc_device.mock_update()
@@ -380,7 +380,7 @@ async def test_rpc_remove_virtual_number_when_mode_label(
hass,
NUMBER_DOMAIN,
"test_name_number_200",
"number:200-number_generic",
"number:200-number",
config_entry,
device_id=device_entry.id,
)
@@ -404,7 +404,7 @@ async def test_rpc_remove_virtual_number_when_orphaned(
hass,
NUMBER_DOMAIN,
"test_name_number_200",
"number:200-number_generic",
"number:200-number",
config_entry,
device_id=device_entry.id,
)

View File

@@ -76,7 +76,7 @@ async def test_rpc_device_virtual_enum(
]
assert (entry := entity_registry.async_get(entity_id))
assert entry.unique_id == "123456789ABC-enum:203-enum_generic"
assert entry.unique_id == "123456789ABC-enum:203-enum"
monkeypatch.setitem(mock_rpc_device.status["enum:203"], "value", "option 2")
mock_rpc_device.mock_update()
@@ -128,7 +128,7 @@ async def test_rpc_remove_virtual_enum_when_mode_label(
hass,
SELECT_PLATFORM,
"test_name_enum_200",
"enum:200-enum_generic",
"enum:200-enum",
config_entry,
device_id=device_entry.id,
)
@@ -152,7 +152,7 @@ async def test_rpc_remove_virtual_enum_when_orphaned(
hass,
SELECT_PLATFORM,
"test_name_enum_200",
"enum:200-enum_generic",
"enum:200-enum",
config_entry,
device_id=device_entry.id,
)

View File

@@ -1080,12 +1080,12 @@ async def test_rpc_device_virtual_text_sensor(
@pytest.mark.parametrize(
("old_id", "new_id", "role"),
("old_id", "new_id", "device_class"),
[
("enum", "enum_generic", None),
("enum", "enum_generic", SensorDeviceClass.ENUM),
("number", "number_generic", None),
("number", "number_current_humidity", "current_humidity"),
("number", "number_current_temperature", "current_temperature"),
("number", "number_current_humidity", SensorDeviceClass.HUMIDITY),
("number", "number_current_temperature", SensorDeviceClass.TEMPERATURE),
("text", "text_generic", None),
],
)
@@ -1094,24 +1094,15 @@ async def test_migrate_unique_id_virtual_components_roles(
mock_rpc_device: Mock,
entity_registry: EntityRegistry,
caplog: pytest.LogCaptureFixture,
monkeypatch: pytest.MonkeyPatch,
old_id: str,
new_id: str,
role: str | None,
device_class: SensorDeviceClass | None,
) -> None:
"""Test migration of unique_id for virtual components to include role."""
entry = await init_integration(hass, 3, skip_setup=True)
unique_base = f"{MOCK_MAC}-{old_id}:200"
old_unique_id = f"{unique_base}-{old_id}"
new_unique_id = f"{unique_base}-{new_id}"
config = deepcopy(mock_rpc_device.config)
if role:
config[f"{old_id}:200"] = {
"role": role,
}
else:
config[f"{old_id}:200"] = {}
monkeypatch.setattr(mock_rpc_device, "config", config)
entity = entity_registry.async_get_or_create(
suggested_object_id="test_name_test_sensor",
@@ -1120,6 +1111,7 @@ async def test_migrate_unique_id_virtual_components_roles(
platform=DOMAIN,
unique_id=old_unique_id,
config_entry=entry,
original_device_class=device_class,
)
assert entity.unique_id == old_unique_id

View File

@@ -645,7 +645,7 @@ async def test_rpc_device_virtual_switch(
assert state.state == STATE_ON
assert (entry := entity_registry.async_get(entity_id))
assert entry.unique_id == "123456789ABC-boolean:200-boolean_generic"
assert entry.unique_id == "123456789ABC-boolean:200-boolean"
monkeypatch.setitem(mock_rpc_device.status["boolean:200"], "value", False)
await hass.services.async_call(
@@ -715,7 +715,7 @@ async def test_rpc_remove_virtual_switch_when_mode_label(
hass,
SWITCH_DOMAIN,
"test_name_boolean_200",
"boolean:200-boolean_generic",
"boolean:200-boolean",
config_entry,
device_id=device_entry.id,
)
@@ -741,7 +741,7 @@ async def test_rpc_remove_virtual_switch_when_orphaned(
hass,
SWITCH_DOMAIN,
"test_name_boolean_200",
"boolean:200-boolean_generic",
"boolean:200-boolean",
config_entry,
device_id=device_entry.id,
)
@@ -750,13 +750,13 @@ async def test_rpc_remove_virtual_switch_when_orphaned(
sub_device_entry = register_sub_device(
device_registry,
config_entry,
"boolean:201-boolean_generic",
"boolean:201-boolean",
)
entity_id2 = register_entity(
hass,
SWITCH_DOMAIN,
"boolean_201",
"boolean:201-boolean_generic",
"boolean:201-boolean",
config_entry,
device_id=sub_device_entry.id,
)

View File

@@ -62,7 +62,7 @@ async def test_rpc_device_virtual_text(
assert state.state == "lorem ipsum"
assert (entry := entity_registry.async_get(entity_id))
assert entry.unique_id == "123456789ABC-text:203-text_generic"
assert entry.unique_id == "123456789ABC-text:203-text"
monkeypatch.setitem(mock_rpc_device.status["text:203"], "value", "dolor sit amet")
mock_rpc_device.mock_update()
@@ -107,7 +107,7 @@ async def test_rpc_remove_virtual_text_when_mode_label(
hass,
TEXT_PLATFORM,
"test_name_text_200",
"text:200-text_generic",
"text:200-text",
config_entry,
device_id=device_entry.id,
)
@@ -131,7 +131,7 @@ async def test_rpc_remove_virtual_text_when_orphaned(
hass,
TEXT_PLATFORM,
"test_name_text_200",
"text:200-text_generic",
"text:200-text",
config_entry,
device_id=device_entry.id,
)

View File

@@ -97,7 +97,6 @@ def mock_smartthings() -> Generator[AsyncMock]:
@pytest.fixture(
params=[
"aq_sensor_3_ikea",
"aeotec_ms6",
"da_ac_airsensor_01001",
"da_ac_rac_000001",
"da_ac_rac_000003",
@@ -157,7 +156,6 @@ def mock_smartthings() -> Generator[AsyncMock]:
"heatit_ztrm3_thermostat",
"heatit_zpushwall",
"generic_ef00_v1",
"gas_detector",
"bosch_radiator_thermostat_ii",
"im_speaker_ai_0001",
"im_smarttag2_ble_uwb",

View File

@@ -1,62 +0,0 @@
{
"components": {
"main": {
"ultravioletIndex": {
"ultravioletIndex": {
"value": 0,
"timestamp": "2025-09-30T15:13:46.521Z"
}
},
"relativeHumidityMeasurement": {
"humidity": {
"value": 60.0,
"unit": "%",
"timestamp": "2025-09-30T15:13:45.441Z"
}
},
"temperatureMeasurement": {
"temperatureRange": {
"value": null
},
"temperature": {
"value": 22.2,
"unit": "C",
"timestamp": "2025-09-30T16:13:50.478Z"
}
},
"refresh": {},
"motionSensor": {
"motion": {
"value": "inactive",
"timestamp": "2025-09-30T15:33:27.594Z"
}
},
"illuminanceMeasurement": {
"illuminance": {
"value": 30,
"unit": "lux",
"timestamp": "2025-09-30T15:13:52.607Z"
}
},
"battery": {
"quantity": {
"value": null
},
"battery": {
"value": 100,
"unit": "%",
"timestamp": "2025-09-30T15:13:46.166Z"
},
"type": {
"value": null
}
},
"tamperAlert": {
"tamper": {
"value": "clear",
"timestamp": "2025-09-30T14:06:07.064Z"
}
}
}
}
}

View File

@@ -1,25 +0,0 @@
{
"components": {
"main": {
"momentary": {},
"gasDetector": {
"gas": {
"value": "clear",
"timestamp": "2025-10-02T03:18:27.139Z"
}
},
"signalStrength": {
"rssi": {
"value": -71,
"unit": "dBm",
"timestamp": "2025-10-07T04:17:08.419Z"
},
"lqi": {
"value": 148,
"timestamp": "2025-10-07T04:32:08.512Z"
}
},
"refresh": {}
}
}
}

View File

@@ -1,86 +0,0 @@
{
"items": [
{
"deviceId": "00f9233e-fdaa-4020-99d4-e0073e53996a",
"name": "aeotec-ms6",
"label": "Parent's Bedroom Sensor",
"manufacturerName": "SmartThingsCommunity",
"presentationId": "6d160aa8-7f54-3611-b7de-0b335d162529",
"deviceManufacturerCode": "0086-0102-0064",
"locationId": "3478ae40-8bd4-40b8-b7e6-f25e3cf86409",
"ownerId": "fe7f9079-8e23-8307-fb7e-4d58929391cf",
"roomId": "f1bb7871-3a3d-48da-b23f-0e1297e8acb0",
"components": [
{
"id": "main",
"label": "main",
"capabilities": [
{
"id": "motionSensor",
"version": 1
},
{
"id": "temperatureMeasurement",
"version": 1
},
{
"id": "relativeHumidityMeasurement",
"version": 1
},
{
"id": "illuminanceMeasurement",
"version": 1
},
{
"id": "ultravioletIndex",
"version": 1
},
{
"id": "tamperAlert",
"version": 1
},
{
"id": "battery",
"version": 1
},
{
"id": "refresh",
"version": 1
}
],
"categories": [
{
"name": "MotionSensor",
"categoryType": "manufacturer"
}
],
"optional": false
}
],
"createTime": "2025-04-17T05:47:05.803Z",
"parentDeviceId": "9fdfde11-206e-47af-9e47-9c314d8d965f",
"profile": {
"id": "9893d370-2af6-32a0-86c5-f1a6d2b9fea7"
},
"zwave": {
"networkId": "BE",
"driverId": "42930682-019d-4dbe-8098-760d7afb3c7f",
"executingLocally": true,
"hubId": "9fdfde11-206e-47af-9e47-9c314d8d965f",
"networkSecurityLevel": "ZWAVE_LEGACY_NON_SECURE",
"provisioningState": "PROVISIONED",
"manufacturerId": 134,
"productType": 258,
"productId": 100,
"fingerprintType": "ZWAVE_MANUFACTURER",
"fingerprintId": "Aeotec/MS6/US"
},
"type": "ZWAVE",
"restrictionTier": 0,
"allowed": null,
"executionContext": "LOCAL",
"relationships": []
}
],
"_links": {}
}

View File

@@ -1,66 +0,0 @@
{
"items": [
{
"deviceId": "d830b46f-f094-4560-b8c3-7690032fdb4c",
"name": "generic-ef00-v1",
"label": "Gas Detector",
"manufacturerName": "SmartThingsCommunity",
"presentationId": "d4b88195-fd5b-39d3-ac6f-7070655f08ab",
"deviceManufacturerCode": "_TZE284_chbyv06x",
"locationId": "7139bb09-31e3-4fad-bf03-b9ad02e57b41",
"ownerId": "00126705-d35b-27ee-d18b-17620d9929e7",
"roomId": "5adccb3a-8ae7-41c0-bc58-7ba80ff78a18",
"components": [
{
"id": "main",
"label": "Detector",
"capabilities": [
{
"id": "gasDetector",
"version": 1
},
{
"id": "momentary",
"version": 1
},
{
"id": "signalStrength",
"version": 1
},
{
"id": "refresh",
"version": 1
}
],
"categories": [
{
"name": "Siren",
"categoryType": "manufacturer"
}
],
"optional": false
}
],
"createTime": "2025-05-25T04:55:42.440Z",
"profile": {
"id": "1d34dd9d-6840-3df6-a6d0-5d9f4a4af2e1"
},
"zigbee": {
"eui": "A4C138C524A5BC8D",
"networkId": "1575",
"driverId": "bc7fd1bc-eb00-4b7f-8977-172acf823508",
"executingLocally": true,
"hubId": "0afe704f-eabb-4e4d-8333-6c73903e4f84",
"provisioningState": "DRIVER_SWITCH",
"fingerprintType": "ZIGBEE_GENERIC",
"fingerprintId": "GenericEF00"
},
"type": "ZIGBEE",
"restrictionTier": 0,
"allowed": null,
"executionContext": "LOCAL",
"relationships": []
}
],
"_links": {}
}

View File

@@ -1,102 +1,4 @@
# serializer version: 1
# name: test_all_entities[aeotec_ms6][binary_sensor.parent_s_bedroom_sensor_motion-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': None,
'entity_id': 'binary_sensor.parent_s_bedroom_sensor_motion',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <BinarySensorDeviceClass.MOTION: 'motion'>,
'original_icon': None,
'original_name': 'Motion',
'platform': 'smartthings',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '00f9233e-fdaa-4020-99d4-e0073e53996a_main_motionSensor_motion_motion',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[aeotec_ms6][binary_sensor.parent_s_bedroom_sensor_motion-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'motion',
'friendly_name': "Parent's Bedroom Sensor Motion",
}),
'context': <ANY>,
'entity_id': 'binary_sensor.parent_s_bedroom_sensor_motion',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_all_entities[aeotec_ms6][binary_sensor.parent_s_bedroom_sensor_tamper-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'binary_sensor.parent_s_bedroom_sensor_tamper',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <BinarySensorDeviceClass.TAMPER: 'tamper'>,
'original_icon': None,
'original_name': 'Tamper',
'platform': 'smartthings',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '00f9233e-fdaa-4020-99d4-e0073e53996a_main_tamperAlert_tamper_tamper',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[aeotec_ms6][binary_sensor.parent_s_bedroom_sensor_tamper-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'tamper',
'friendly_name': "Parent's Bedroom Sensor Tamper",
}),
'context': <ANY>,
'entity_id': 'binary_sensor.parent_s_bedroom_sensor_tamper',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_all_entities[c2c_arlo_pro_3_switch][binary_sensor.2nd_floor_hallway_motion-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
@@ -2670,55 +2572,6 @@
'state': 'off',
})
# ---
# name: test_all_entities[gas_detector][binary_sensor.gas_detector_gas-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': None,
'entity_id': 'binary_sensor.gas_detector_gas',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <BinarySensorDeviceClass.GAS: 'gas'>,
'original_icon': None,
'original_name': 'Gas',
'platform': 'smartthings',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': 'd830b46f-f094-4560-b8c3-7690032fdb4c_main_gasDetector_gas_gas',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[gas_detector][binary_sensor.gas_detector_gas-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'gas',
'friendly_name': 'Gas Detector Gas',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.gas_detector_gas',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_all_entities[iphone][binary_sensor.iphone_presence-entry]
EntityRegistryEntrySnapshot({
'aliases': set({

View File

@@ -64,37 +64,6 @@
'via_device_id': None,
})
# ---
# name: test_devices[aeotec_ms6]
DeviceRegistryEntrySnapshot({
'area_id': None,
'config_entries': <ANY>,
'config_entries_subentries': <ANY>,
'configuration_url': 'https://account.smartthings.com',
'connections': set({
}),
'disabled_by': None,
'entry_type': None,
'hw_version': None,
'id': <ANY>,
'identifiers': set({
tuple(
'smartthings',
'00f9233e-fdaa-4020-99d4-e0073e53996a',
),
}),
'labels': set({
}),
'manufacturer': None,
'model': None,
'model_id': None,
'name': "Parent's Bedroom Sensor",
'name_by_user': None,
'primary_config_entry': <ANY>,
'serial_number': None,
'sw_version': None,
'via_device_id': None,
})
# ---
# name: test_devices[aq_sensor_3_ikea]
DeviceRegistryEntrySnapshot({
'area_id': None,
@@ -1335,37 +1304,6 @@
'via_device_id': None,
})
# ---
# name: test_devices[gas_detector]
DeviceRegistryEntrySnapshot({
'area_id': None,
'config_entries': <ANY>,
'config_entries_subentries': <ANY>,
'configuration_url': 'https://account.smartthings.com',
'connections': set({
}),
'disabled_by': None,
'entry_type': None,
'hw_version': None,
'id': <ANY>,
'identifiers': set({
tuple(
'smartthings',
'd830b46f-f094-4560-b8c3-7690032fdb4c',
),
}),
'labels': set({
}),
'manufacturer': None,
'model': None,
'model_id': None,
'name': 'Gas Detector',
'name_by_user': None,
'primary_config_entry': <ANY>,
'serial_number': None,
'sw_version': None,
'via_device_id': None,
})
# ---
# name: test_devices[gas_meter]
DeviceRegistryEntrySnapshot({
'area_id': None,

View File

@@ -163,269 +163,6 @@
'state': 'unknown',
})
# ---
# name: test_all_entities[aeotec_ms6][sensor.parent_s_bedroom_sensor_battery-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.parent_s_bedroom_sensor_battery',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <SensorDeviceClass.BATTERY: 'battery'>,
'original_icon': None,
'original_name': 'Battery',
'platform': 'smartthings',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '00f9233e-fdaa-4020-99d4-e0073e53996a_main_battery_battery_battery',
'unit_of_measurement': '%',
})
# ---
# name: test_all_entities[aeotec_ms6][sensor.parent_s_bedroom_sensor_battery-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'battery',
'friendly_name': "Parent's Bedroom Sensor Battery",
'unit_of_measurement': '%',
}),
'context': <ANY>,
'entity_id': 'sensor.parent_s_bedroom_sensor_battery',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '100',
})
# ---
# name: test_all_entities[aeotec_ms6][sensor.parent_s_bedroom_sensor_humidity-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.parent_s_bedroom_sensor_humidity',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <SensorDeviceClass.HUMIDITY: 'humidity'>,
'original_icon': None,
'original_name': 'Humidity',
'platform': 'smartthings',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '00f9233e-fdaa-4020-99d4-e0073e53996a_main_relativeHumidityMeasurement_humidity_humidity',
'unit_of_measurement': '%',
})
# ---
# name: test_all_entities[aeotec_ms6][sensor.parent_s_bedroom_sensor_humidity-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'humidity',
'friendly_name': "Parent's Bedroom Sensor Humidity",
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': '%',
}),
'context': <ANY>,
'entity_id': 'sensor.parent_s_bedroom_sensor_humidity',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '60.0',
})
# ---
# name: test_all_entities[aeotec_ms6][sensor.parent_s_bedroom_sensor_illuminance-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.parent_s_bedroom_sensor_illuminance',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <SensorDeviceClass.ILLUMINANCE: 'illuminance'>,
'original_icon': None,
'original_name': 'Illuminance',
'platform': 'smartthings',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '00f9233e-fdaa-4020-99d4-e0073e53996a_main_illuminanceMeasurement_illuminance_illuminance',
'unit_of_measurement': 'lx',
})
# ---
# name: test_all_entities[aeotec_ms6][sensor.parent_s_bedroom_sensor_illuminance-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'illuminance',
'friendly_name': "Parent's Bedroom Sensor Illuminance",
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'lx',
}),
'context': <ANY>,
'entity_id': 'sensor.parent_s_bedroom_sensor_illuminance',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '30',
})
# ---
# name: test_all_entities[aeotec_ms6][sensor.parent_s_bedroom_sensor_temperature-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.parent_s_bedroom_sensor_temperature',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
'sensor': dict({
'suggested_display_precision': 1,
}),
}),
'original_device_class': <SensorDeviceClass.TEMPERATURE: 'temperature'>,
'original_icon': None,
'original_name': 'Temperature',
'platform': 'smartthings',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '00f9233e-fdaa-4020-99d4-e0073e53996a_main_temperatureMeasurement_temperature_temperature',
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
})
# ---
# name: test_all_entities[aeotec_ms6][sensor.parent_s_bedroom_sensor_temperature-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'temperature',
'friendly_name': "Parent's Bedroom Sensor Temperature",
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
}),
'context': <ANY>,
'entity_id': 'sensor.parent_s_bedroom_sensor_temperature',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '22.2',
})
# ---
# name: test_all_entities[aeotec_ms6][sensor.parent_s_bedroom_sensor_uv_index-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.parent_s_bedroom_sensor_uv_index',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'UV index',
'platform': 'smartthings',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'uv_index',
'unique_id': '00f9233e-fdaa-4020-99d4-e0073e53996a_main_ultravioletIndex_ultravioletIndex_ultravioletIndex',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[aeotec_ms6][sensor.parent_s_bedroom_sensor_uv_index-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': "Parent's Bedroom Sensor UV index",
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'context': <ANY>,
'entity_id': 'sensor.parent_s_bedroom_sensor_uv_index',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '0',
})
# ---
# name: test_all_entities[aq_sensor_3_ikea][sensor.aq_sensor_3_ikea_humidity-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
@@ -12914,110 +12651,6 @@
'state': 'unknown',
})
# ---
# name: test_all_entities[gas_detector][sensor.gas_detector_link_quality-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.gas_detector_link_quality',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Link quality',
'platform': 'smartthings',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'link_quality',
'unique_id': 'd830b46f-f094-4560-b8c3-7690032fdb4c_main_signalStrength_lqi_lqi',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[gas_detector][sensor.gas_detector_link_quality-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Gas Detector Link quality',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'context': <ANY>,
'entity_id': 'sensor.gas_detector_link_quality',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '148',
})
# ---
# name: test_all_entities[gas_detector][sensor.gas_detector_signal_strength-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.gas_detector_signal_strength',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <SensorDeviceClass.SIGNAL_STRENGTH: 'signal_strength'>,
'original_icon': None,
'original_name': 'Signal strength',
'platform': 'smartthings',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': 'd830b46f-f094-4560-b8c3-7690032fdb4c_main_signalStrength_rssi_rssi',
'unit_of_measurement': 'dBm',
})
# ---
# name: test_all_entities[gas_detector][sensor.gas_detector_signal_strength-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'signal_strength',
'friendly_name': 'Gas Detector Signal strength',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'dBm',
}),
'context': <ANY>,
'entity_id': 'sensor.gas_detector_signal_strength',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '-71',
})
# ---
# name: test_all_entities[gas_meter][sensor.gas_meter_gas-entry]
EntityRegistryEntrySnapshot({
'aliases': set({

View File

@@ -24,9 +24,7 @@ from homeassistant.helpers.trigger import (
DATA_PLUGGABLE_ACTIONS,
PluggableAction,
Trigger,
TriggerActionType,
TriggerConfig,
TriggerInfo,
TriggerActionRunner,
_async_get_trigger_platform,
async_initialize_triggers,
async_validate_trigger_config,
@@ -449,7 +447,31 @@ async def test_pluggable_action(
assert not plug_2
async def test_platform_multiple_triggers(hass: HomeAssistant) -> None:
class TriggerActionFunctionTypeHelper:
"""Helper for testing different trigger action function types."""
def __init__(self) -> None:
"""Init helper."""
self.action_calls = []
@callback
def cb_action(self, *args):
"""Callback action."""
self.action_calls.append([*args])
def sync_action(self, *args):
"""Sync action."""
self.action_calls.append([*args])
async def async_action(self, *args):
"""Async action."""
self.action_calls.append([*args])
@pytest.mark.parametrize("action_method", ["cb_action", "sync_action", "async_action"])
async def test_platform_multiple_triggers(
hass: HomeAssistant, action_method: str
) -> None:
"""Test a trigger platform with multiple trigger."""
class MockTrigger(Trigger):
@@ -462,30 +484,23 @@ async def test_platform_multiple_triggers(hass: HomeAssistant) -> None:
"""Validate config."""
return config
def __init__(self, hass: HomeAssistant, config: TriggerConfig) -> None:
"""Initialize trigger."""
class MockTrigger1(MockTrigger):
"""Mock trigger 1."""
async def async_attach(
self,
action: TriggerActionType,
trigger_info: TriggerInfo,
async def async_attach_runner(
self, run_action: TriggerActionRunner
) -> CALLBACK_TYPE:
"""Attach a trigger."""
action({"trigger": "test_trigger_1"})
run_action({"extra": "test_trigger_1"}, "trigger 1 desc")
class MockTrigger2(MockTrigger):
"""Mock trigger 2."""
async def async_attach(
self,
action: TriggerActionType,
trigger_info: TriggerInfo,
async def async_attach_runner(
self, run_action: TriggerActionRunner
) -> CALLBACK_TYPE:
"""Attach a trigger."""
action({"trigger": "test_trigger_2"})
run_action({"extra": "test_trigger_2"}, "trigger 2 desc")
async def async_get_triggers(hass: HomeAssistant) -> dict[str, type[Trigger]]:
return {
@@ -508,22 +523,41 @@ async def test_platform_multiple_triggers(hass: HomeAssistant) -> None:
log_cb = MagicMock()
action_calls = []
action_helper = TriggerActionFunctionTypeHelper()
action_method = getattr(action_helper, action_method)
@callback
def cb_action(*args):
action_calls.append([*args])
await async_initialize_triggers(hass, config_1, action_method, "test", "", log_cb)
assert len(action_helper.action_calls) == 1
assert action_helper.action_calls[0][0] == {
"trigger": {
"alias": None,
"description": "trigger 1 desc",
"extra": "test_trigger_1",
"id": "0",
"idx": "0",
"platform": "test",
}
}
action_helper.action_calls.clear()
await async_initialize_triggers(hass, config_1, cb_action, "test", "", log_cb)
assert action_calls == [[{"trigger": "test_trigger_1"}]]
action_calls.clear()
await async_initialize_triggers(hass, config_2, cb_action, "test", "", log_cb)
assert action_calls == [[{"trigger": "test_trigger_2"}]]
action_calls.clear()
await async_initialize_triggers(hass, config_2, action_method, "test", "", log_cb)
assert len(action_helper.action_calls) == 1
assert action_helper.action_calls[0][0] == {
"trigger": {
"alias": None,
"description": "trigger 2 desc",
"extra": "test_trigger_2",
"id": "0",
"idx": "0",
"platform": "test.trig_2",
}
}
action_helper.action_calls.clear()
with pytest.raises(KeyError):
await async_initialize_triggers(hass, config_3, cb_action, "test", "", log_cb)
await async_initialize_triggers(
hass, config_3, action_method, "test", "", log_cb
)
async def test_platform_migrate_trigger(hass: HomeAssistant) -> None:

View File

@@ -8,11 +8,13 @@ from unittest.mock import Mock, patch
import pytest
from homeassistant import const
from homeassistant.components import alarm_control_panel
from .common import (
extract_stack_to_frame,
help_test_all,
import_and_test_deprecated_constant,
import_and_test_deprecated_constant_enum,
)
@@ -50,6 +52,30 @@ def test_deprecated_constant_name_changes(
)
def _create_tuples_alarm_states(
enum: type[Enum], constant_prefix: str, remove_in_version: str
) -> list[tuple[Enum, str]]:
return [(enum_field, constant_prefix, remove_in_version) for enum_field in enum]
@pytest.mark.parametrize(
("enum", "constant_prefix", "remove_in_version"),
_create_tuples_alarm_states(
alarm_control_panel.AlarmControlPanelState, "STATE_ALARM_", "2025.11"
),
)
def test_deprecated_constants_alarm(
caplog: pytest.LogCaptureFixture,
enum: Enum,
constant_prefix: str,
remove_in_version: str,
) -> None:
"""Test deprecated constants."""
import_and_test_deprecated_constant_enum(
caplog, const, enum, constant_prefix, remove_in_version
)
def test_deprecated_unit_of_conductivity_alias() -> None:
"""Test UnitOfConductivity deprecation."""