mirror of
https://github.com/home-assistant/core.git
synced 2026-05-28 19:53:18 +02:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 287dea745b |
@@ -24,7 +24,6 @@ The following platforms have extra guidelines:
|
||||
## Entity platforms
|
||||
|
||||
- Ensure `async_added_to_hass()` and `async_will_remove_from_hass()` have symmetrical behavior. For example, if a subscription is created in `async_added_to_hass()`, it should be unsubscribed in `async_will_remove_from_hass()`. Also, if something is torn down in `async_will_remove_from_hass()`, it should be set up in `async_added_to_hass()`.
|
||||
- Entity base class (e.g. `SensorEntity`, `TrackerEntity`) provide a stable API for child classes to inherit from. Do not suggest redeclaring or duplicating attributes, properties, or methods the base class already provides, and do not add guards against the parent's behavior changing — rely on the base class instead.
|
||||
|
||||
## Integration Quality Scale
|
||||
|
||||
|
||||
@@ -27,7 +27,6 @@ The following platforms have extra guidelines:
|
||||
## Entity platforms
|
||||
|
||||
- Ensure `async_added_to_hass()` and `async_will_remove_from_hass()` have symmetrical behavior. For example, if a subscription is created in `async_added_to_hass()`, it should be unsubscribed in `async_will_remove_from_hass()`. Also, if something is torn down in `async_will_remove_from_hass()`, it should be set up in `async_added_to_hass()`.
|
||||
- Entity base class (e.g. `SensorEntity`, `TrackerEntity`) provide a stable API for child classes to inherit from. Do not suggest redeclaring or duplicating attributes, properties, or methods the base class already provides, and do not add guards against the parent's behavior changing — rely on the base class instead.
|
||||
|
||||
## Integration Quality Scale
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
"lg_netcast",
|
||||
"lg_soundbar",
|
||||
"lg_thinq",
|
||||
"lg_tv_rs232",
|
||||
"webostv"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ class AlexaVoiceEvent(AmazonEntity, EventEntity):
|
||||
|
||||
_attr_event_types = [EVENT_TYPE]
|
||||
coordinator: AmazonDevicesCoordinator
|
||||
_last_seen_timestamp: int = 0 # January 1, 1970 at 12:00:00 AM
|
||||
_last_seen_timestamp: int | None = None
|
||||
|
||||
@callback
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
@@ -71,8 +71,7 @@ class AlexaVoiceEvent(AmazonEntity, EventEntity):
|
||||
)
|
||||
return
|
||||
|
||||
if vocal_record.timestamp <= self._last_seen_timestamp:
|
||||
# Discard old events that have already been processed
|
||||
if vocal_record.timestamp == self._last_seen_timestamp:
|
||||
return
|
||||
|
||||
self._last_seen_timestamp = vocal_record.timestamp
|
||||
|
||||
@@ -38,13 +38,11 @@ from homeassistant.components.media_player import (
|
||||
)
|
||||
from homeassistant.const import CONF_NAME
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from . import AppleTvConfigEntry, AppleTVManager
|
||||
from .browse_media import build_app_list
|
||||
from .const import DOMAIN
|
||||
from .entity import AppleTVEntity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -128,6 +126,7 @@ class AppleTvMediaPlayer(
|
||||
@callback
|
||||
def async_device_connected(self, atv: AppleTV) -> None:
|
||||
"""Handle when connection is made to device."""
|
||||
# NB: Do not use _is_feature_available here as it only works when playing
|
||||
if atv.features.in_state(FeatureState.Available, FeatureName.PushUpdates):
|
||||
atv.push_updater.listener = self
|
||||
atv.push_updater.start()
|
||||
@@ -353,41 +352,21 @@ class AppleTvMediaPlayer(
|
||||
media_id = async_process_play_media_url(self.hass, play_item.url)
|
||||
media_type = MediaType.MUSIC
|
||||
|
||||
use_stream_file = self._is_feature_available(FeatureName.StreamFile) and (
|
||||
if self._is_feature_available(FeatureName.StreamFile) and (
|
||||
media_type == MediaType.MUSIC or await is_streamable(media_id)
|
||||
)
|
||||
|
||||
try:
|
||||
if use_stream_file:
|
||||
_LOGGER.debug("Streaming %s via RAOP", media_id)
|
||||
await self.atv.stream.stream_file(media_id)
|
||||
elif self._is_feature_available(FeatureName.PlayUrl) and (
|
||||
(parsed_url := URL(media_id)).is_absolute() and parsed_url.host
|
||||
):
|
||||
_LOGGER.debug("Playing %s via AirPlay", media_id)
|
||||
await self.atv.stream.play_url(media_id)
|
||||
else:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="streaming_not_supported",
|
||||
)
|
||||
except exceptions.NotSupportedError as ex:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="streaming_not_supported",
|
||||
) from ex
|
||||
except (
|
||||
exceptions.BlockedStateError,
|
||||
exceptions.ConnectionLostError,
|
||||
exceptions.InvalidStateError,
|
||||
exceptions.OperationTimeoutError,
|
||||
exceptions.PlaybackError,
|
||||
exceptions.ProtocolError,
|
||||
) as ex:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="stream_failed",
|
||||
) from ex
|
||||
):
|
||||
_LOGGER.debug("Streaming %s via RAOP", media_id)
|
||||
await self.atv.stream.stream_file(media_id)
|
||||
elif self._is_feature_available(FeatureName.PlayUrl) and (
|
||||
(parsed_url := URL(media_id)).is_absolute() and parsed_url.host
|
||||
):
|
||||
_LOGGER.debug("Playing %s via AirPlay", media_id)
|
||||
await self.atv.stream.play_url(media_id)
|
||||
else:
|
||||
_LOGGER.error(
|
||||
"Media streaming is not possible with current configuration for %s",
|
||||
media_id,
|
||||
)
|
||||
|
||||
@property
|
||||
def media_image_hash(self) -> str | None:
|
||||
@@ -481,7 +460,7 @@ class AppleTvMediaPlayer(
|
||||
|
||||
def _is_feature_available(self, feature: FeatureName) -> bool:
|
||||
"""Return if a feature is available."""
|
||||
if self.atv:
|
||||
if self.atv and self._playing:
|
||||
return self.atv.features.in_state(FeatureState.Available, feature)
|
||||
return False
|
||||
|
||||
|
||||
@@ -81,12 +81,6 @@
|
||||
},
|
||||
"not_connected": {
|
||||
"message": "Apple TV is not connected"
|
||||
},
|
||||
"stream_failed": {
|
||||
"message": "Failed to stream media to the Apple TV"
|
||||
},
|
||||
"streaming_not_supported": {
|
||||
"message": "Streaming the requested media is not supported"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
|
||||
@@ -92,7 +92,7 @@ class BroadlinkRadioFrequency(BroadlinkEntity, RadioFrequencyTransmitterEntity):
|
||||
"""Representation of a Broadlink RF transmitter."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
_attr_translation_key = "rf_transmitter"
|
||||
_attr_name = None
|
||||
|
||||
def __init__(self, device: BroadlinkDevice) -> None:
|
||||
"""Initialize the entity."""
|
||||
|
||||
@@ -54,11 +54,6 @@
|
||||
"name": "IR emitter"
|
||||
}
|
||||
},
|
||||
"radio_frequency": {
|
||||
"rf_transmitter": {
|
||||
"name": "RF transmitter"
|
||||
}
|
||||
},
|
||||
"select": {
|
||||
"day_of_week": {
|
||||
"name": "Day of week",
|
||||
|
||||
@@ -86,6 +86,7 @@ class DucoCoordinator(DataUpdateCoordinator[DucoData]):
|
||||
"""Fetch node data from the Duco box."""
|
||||
try:
|
||||
nodes = await self.client.async_get_nodes()
|
||||
lan_info = await self.client.async_get_lan_info()
|
||||
except DucoConnectionError as err:
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
@@ -99,18 +100,7 @@ class DucoCoordinator(DataUpdateCoordinator[DucoData]):
|
||||
translation_placeholders={"error": repr(err)},
|
||||
) from err
|
||||
|
||||
# LAN info only backs the diagnostic RSSI sensor, so failures on this
|
||||
# supplemental endpoint, including connection failures, should not make
|
||||
# the primary node entities unavailable.
|
||||
rssi_wifi = self.data.rssi_wifi if self.data else None
|
||||
try:
|
||||
lan_info = await self.client.async_get_lan_info()
|
||||
except DucoError as err:
|
||||
_LOGGER.debug("Could not fetch Duco LAN info", exc_info=err)
|
||||
else:
|
||||
rssi_wifi = lan_info.rssi_wifi
|
||||
|
||||
return DucoData(
|
||||
nodes={node.node_id: node for node in nodes},
|
||||
rssi_wifi=rssi_wifi,
|
||||
rssi_wifi=lan_info.rssi_wifi,
|
||||
)
|
||||
|
||||
@@ -21,5 +21,5 @@
|
||||
"integration_type": "system",
|
||||
"preview_features": { "winter_mode": {} },
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["home-assistant-frontend==20260527.1"]
|
||||
"requirements": ["home-assistant-frontend==20260527.0"]
|
||||
}
|
||||
|
||||
@@ -199,7 +199,6 @@ DEVICE_CLASS_TO_GOOGLE_TYPES = {
|
||||
(media_player.DOMAIN, media_player.MediaPlayerDeviceClass.RECEIVER): TYPE_RECEIVER,
|
||||
(media_player.DOMAIN, media_player.MediaPlayerDeviceClass.SPEAKER): TYPE_SPEAKER,
|
||||
(media_player.DOMAIN, media_player.MediaPlayerDeviceClass.TV): TYPE_TV,
|
||||
(media_player.DOMAIN, media_player.MediaPlayerDeviceClass.PROJECTOR): TYPE_TV,
|
||||
(sensor.DOMAIN, sensor.SensorDeviceClass.AQI): TYPE_SENSOR,
|
||||
(sensor.DOMAIN, sensor.SensorDeviceClass.HUMIDITY): TYPE_SENSOR,
|
||||
(sensor.DOMAIN, sensor.SensorDeviceClass.TEMPERATURE): TYPE_SENSOR,
|
||||
|
||||
@@ -2728,11 +2728,7 @@ class ChannelTrait(_Trait):
|
||||
if (
|
||||
domain == media_player.DOMAIN
|
||||
and (features & MediaPlayerEntityFeature.PLAY_MEDIA)
|
||||
and device_class
|
||||
in (
|
||||
media_player.MediaPlayerDeviceClass.TV,
|
||||
media_player.MediaPlayerDeviceClass.PROJECTOR,
|
||||
)
|
||||
and device_class == media_player.MediaPlayerDeviceClass.TV
|
||||
):
|
||||
return True
|
||||
|
||||
|
||||
@@ -202,10 +202,7 @@ def get_accessory( # noqa: C901
|
||||
|
||||
if device_class == MediaPlayerDeviceClass.RECEIVER:
|
||||
a_type = "ReceiverMediaPlayer"
|
||||
elif device_class in (
|
||||
MediaPlayerDeviceClass.TV,
|
||||
MediaPlayerDeviceClass.PROJECTOR,
|
||||
):
|
||||
elif device_class == MediaPlayerDeviceClass.TV:
|
||||
a_type = "TelevisionMediaPlayer"
|
||||
elif validate_media_player_features(state, feature_list):
|
||||
a_type = "MediaPlayer"
|
||||
|
||||
@@ -695,11 +695,7 @@ def state_needs_accessory_mode(state: State) -> bool:
|
||||
return (
|
||||
state.domain == MEDIA_PLAYER_DOMAIN
|
||||
and state.attributes.get(ATTR_DEVICE_CLASS)
|
||||
in (
|
||||
MediaPlayerDeviceClass.TV,
|
||||
MediaPlayerDeviceClass.RECEIVER,
|
||||
MediaPlayerDeviceClass.PROJECTOR,
|
||||
)
|
||||
in (MediaPlayerDeviceClass.TV, MediaPlayerDeviceClass.RECEIVER)
|
||||
) or (
|
||||
state.domain == REMOTE_DOMAIN
|
||||
and state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||
|
||||
@@ -108,7 +108,5 @@ def create_matter_ble_proxy(hass: HomeAssistant, ws_url: str) -> MatterBleProxy:
|
||||
ws_url=ws_url,
|
||||
scan_source=HaBluetoothScanSource(hass),
|
||||
device_resolver=HaBluetoothDeviceResolver(hass),
|
||||
task_factory=lambda coro: hass.async_create_background_task(
|
||||
coro, name="matter_ble_proxy"
|
||||
),
|
||||
task_factory=hass.async_create_task,
|
||||
)
|
||||
|
||||
@@ -155,7 +155,6 @@ class MediaPlayerDeviceClass(StrEnum):
|
||||
TV = "tv"
|
||||
SPEAKER = "speaker"
|
||||
RECEIVER = "receiver"
|
||||
PROJECTOR = "projector"
|
||||
|
||||
|
||||
DEVICE_CLASSES_SCHEMA = vol.All(vol.Lower, vol.Coerce(MediaPlayerDeviceClass))
|
||||
|
||||
@@ -34,12 +34,6 @@
|
||||
"playing": "mdi:cast-connected"
|
||||
}
|
||||
},
|
||||
"projector": {
|
||||
"default": "mdi:projector",
|
||||
"state": {
|
||||
"off": "mdi:projector-off"
|
||||
}
|
||||
},
|
||||
"receiver": {
|
||||
"default": "mdi:audio-video",
|
||||
"state": {
|
||||
|
||||
@@ -261,9 +261,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"projector": {
|
||||
"name": "Projector"
|
||||
},
|
||||
"receiver": {
|
||||
"name": "Receiver"
|
||||
},
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
"""Diagnostics support for OVHcloud AI Endpoints."""
|
||||
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from openai import __title__, __version__
|
||||
|
||||
from homeassistant.components.diagnostics import async_redact_data
|
||||
from homeassistant.const import CONF_API_KEY, CONF_PROMPT
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from . import OVHcloudAIEndpointsConfigEntry
|
||||
|
||||
|
||||
TO_REDACT = {CONF_API_KEY, CONF_PROMPT}
|
||||
|
||||
|
||||
async def async_get_config_entry_diagnostics(
|
||||
hass: HomeAssistant, entry: OVHcloudAIEndpointsConfigEntry
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a config entry."""
|
||||
return {
|
||||
"client": f"{__title__}=={__version__}",
|
||||
"title": entry.title,
|
||||
"entry_id": entry.entry_id,
|
||||
"entry_version": f"{entry.version}.{entry.minor_version}",
|
||||
"state": entry.state.value,
|
||||
"data": async_redact_data(entry.data, TO_REDACT),
|
||||
"options": async_redact_data(entry.options, TO_REDACT),
|
||||
"subentries": {
|
||||
subentry.subentry_id: {
|
||||
"title": subentry.title,
|
||||
"subentry_type": subentry.subentry_type,
|
||||
"data": async_redact_data(subentry.data, TO_REDACT),
|
||||
}
|
||||
for subentry in entry.subentries.values()
|
||||
},
|
||||
"entities": {
|
||||
entity_entry.entity_id: entity_entry.extended_dict
|
||||
for entity_entry in er.async_entries_for_config_entry(
|
||||
er.async_get(hass), entry.entry_id
|
||||
)
|
||||
},
|
||||
}
|
||||
@@ -49,7 +49,7 @@ rules:
|
||||
|
||||
# Gold
|
||||
devices: done
|
||||
diagnostics: done
|
||||
diagnostics: todo
|
||||
discovery-update-info:
|
||||
status: exempt
|
||||
comment: Service can't be discovered
|
||||
|
||||
@@ -4,7 +4,7 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
PLATFORMS = [Platform.BUTTON, Platform.MEDIA_PLAYER]
|
||||
PLATFORMS = [Platform.MEDIA_PLAYER]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
@@ -1,176 +0,0 @@
|
||||
"""Button platform for Samsung IR integration."""
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from infrared_protocols.codes.samsung.tv import SamsungTVCode
|
||||
|
||||
from homeassistant.components.button import ButtonEntity, ButtonEntityDescription
|
||||
from homeassistant.components.infrared import InfraredEmitterConsumerEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import CONF_DEVICE_TYPE, CONF_INFRARED_EMITTER_ENTITY_ID, SamsungDeviceType
|
||||
from .entity import SamsungIrEntity
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class SamsungIrButtonEntityDescription(ButtonEntityDescription):
|
||||
"""Describes Samsung IR button entity."""
|
||||
|
||||
command_code: SamsungTVCode
|
||||
|
||||
|
||||
TV_BUTTON_DESCRIPTIONS: tuple[SamsungIrButtonEntityDescription, ...] = (
|
||||
SamsungIrButtonEntityDescription(
|
||||
key="power", translation_key="power", command_code=SamsungTVCode.POWER
|
||||
),
|
||||
SamsungIrButtonEntityDescription(
|
||||
key="source", translation_key="source", command_code=SamsungTVCode.SOURCE
|
||||
),
|
||||
SamsungIrButtonEntityDescription(
|
||||
key="settings", translation_key="settings", command_code=SamsungTVCode.SETTINGS
|
||||
),
|
||||
SamsungIrButtonEntityDescription(
|
||||
key="info", translation_key="info", command_code=SamsungTVCode.INFO
|
||||
),
|
||||
SamsungIrButtonEntityDescription(
|
||||
key="exit", translation_key="exit", command_code=SamsungTVCode.EXIT
|
||||
),
|
||||
SamsungIrButtonEntityDescription(
|
||||
key="return", translation_key="return", command_code=SamsungTVCode.RETURN
|
||||
),
|
||||
SamsungIrButtonEntityDescription(
|
||||
key="home", translation_key="home", command_code=SamsungTVCode.HOME
|
||||
),
|
||||
SamsungIrButtonEntityDescription(
|
||||
key="red", translation_key="red", command_code=SamsungTVCode.RED
|
||||
),
|
||||
SamsungIrButtonEntityDescription(
|
||||
key="green", translation_key="green", command_code=SamsungTVCode.GREEN
|
||||
),
|
||||
SamsungIrButtonEntityDescription(
|
||||
key="yellow", translation_key="yellow", command_code=SamsungTVCode.YELLOW
|
||||
),
|
||||
SamsungIrButtonEntityDescription(
|
||||
key="blue", translation_key="blue", command_code=SamsungTVCode.BLUE
|
||||
),
|
||||
SamsungIrButtonEntityDescription(
|
||||
key="up", translation_key="up", command_code=SamsungTVCode.NAV_UP
|
||||
),
|
||||
SamsungIrButtonEntityDescription(
|
||||
key="down", translation_key="down", command_code=SamsungTVCode.NAV_DOWN
|
||||
),
|
||||
SamsungIrButtonEntityDescription(
|
||||
key="left", translation_key="left", command_code=SamsungTVCode.NAV_LEFT
|
||||
),
|
||||
SamsungIrButtonEntityDescription(
|
||||
key="right", translation_key="right", command_code=SamsungTVCode.NAV_RIGHT
|
||||
),
|
||||
SamsungIrButtonEntityDescription(
|
||||
key="ok", translation_key="ok", command_code=SamsungTVCode.OK
|
||||
),
|
||||
SamsungIrButtonEntityDescription(
|
||||
key="previous_channel",
|
||||
translation_key="previous_channel",
|
||||
command_code=SamsungTVCode.PREVIOUS_CHANNEL,
|
||||
),
|
||||
SamsungIrButtonEntityDescription(
|
||||
key="num_0", translation_key="num_0", command_code=SamsungTVCode.NUM_0
|
||||
),
|
||||
SamsungIrButtonEntityDescription(
|
||||
key="num_1", translation_key="num_1", command_code=SamsungTVCode.NUM_1
|
||||
),
|
||||
SamsungIrButtonEntityDescription(
|
||||
key="num_2", translation_key="num_2", command_code=SamsungTVCode.NUM_2
|
||||
),
|
||||
SamsungIrButtonEntityDescription(
|
||||
key="num_3", translation_key="num_3", command_code=SamsungTVCode.NUM_3
|
||||
),
|
||||
SamsungIrButtonEntityDescription(
|
||||
key="num_4", translation_key="num_4", command_code=SamsungTVCode.NUM_4
|
||||
),
|
||||
SamsungIrButtonEntityDescription(
|
||||
key="num_5", translation_key="num_5", command_code=SamsungTVCode.NUM_5
|
||||
),
|
||||
SamsungIrButtonEntityDescription(
|
||||
key="num_6", translation_key="num_6", command_code=SamsungTVCode.NUM_6
|
||||
),
|
||||
SamsungIrButtonEntityDescription(
|
||||
key="num_7", translation_key="num_7", command_code=SamsungTVCode.NUM_7
|
||||
),
|
||||
SamsungIrButtonEntityDescription(
|
||||
key="num_8", translation_key="num_8", command_code=SamsungTVCode.NUM_8
|
||||
),
|
||||
SamsungIrButtonEntityDescription(
|
||||
key="num_9", translation_key="num_9", command_code=SamsungTVCode.NUM_9
|
||||
),
|
||||
SamsungIrButtonEntityDescription(
|
||||
key="fast_forward",
|
||||
translation_key="fast_forward",
|
||||
command_code=SamsungTVCode.FAST_FORWARD,
|
||||
),
|
||||
SamsungIrButtonEntityDescription(
|
||||
key="rewind", translation_key="rewind", command_code=SamsungTVCode.REWIND
|
||||
),
|
||||
SamsungIrButtonEntityDescription(
|
||||
key="record", translation_key="record", command_code=SamsungTVCode.RECORD
|
||||
),
|
||||
SamsungIrButtonEntityDescription(
|
||||
key="tools", translation_key="tools", command_code=SamsungTVCode.TOOLS
|
||||
),
|
||||
SamsungIrButtonEntityDescription(
|
||||
key="browser", translation_key="browser", command_code=SamsungTVCode.BROWSER
|
||||
),
|
||||
SamsungIrButtonEntityDescription(
|
||||
key="ad_subtitle",
|
||||
translation_key="ad_subtitle",
|
||||
command_code=SamsungTVCode.AD_SUBTITLE,
|
||||
),
|
||||
SamsungIrButtonEntityDescription(
|
||||
key="e_manual",
|
||||
translation_key="e_manual",
|
||||
command_code=SamsungTVCode.E_MANUAL,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Samsung IR buttons from config entry."""
|
||||
infrared_emitter_entity_id = entry.data[CONF_INFRARED_EMITTER_ENTITY_ID]
|
||||
device_type = entry.data[CONF_DEVICE_TYPE]
|
||||
if device_type != SamsungDeviceType.TV:
|
||||
return
|
||||
async_add_entities(
|
||||
[
|
||||
SamsungIrButton(entry, infrared_emitter_entity_id, description)
|
||||
for description in TV_BUTTON_DESCRIPTIONS
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
class SamsungIrButton(SamsungIrEntity, InfraredEmitterConsumerEntity, ButtonEntity):
|
||||
"""Samsung IR button entity."""
|
||||
|
||||
entity_description: SamsungIrButtonEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
entry: ConfigEntry,
|
||||
infrared_emitter_entity_id: str,
|
||||
description: SamsungIrButtonEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize Samsung IR button."""
|
||||
super().__init__(entry, unique_id_suffix=description.key)
|
||||
self._infrared_emitter_entity_id = infrared_emitter_entity_id
|
||||
self.entity_description = description
|
||||
|
||||
async def async_press(self) -> None:
|
||||
"""Press the button."""
|
||||
await self._send_command(self.entity_description.command_code.to_command())
|
||||
@@ -19,112 +19,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"button": {
|
||||
"ad_subtitle": {
|
||||
"name": "AD/Subtitle"
|
||||
},
|
||||
"blue": {
|
||||
"name": "Blue"
|
||||
},
|
||||
"browser": {
|
||||
"name": "Browser"
|
||||
},
|
||||
"down": {
|
||||
"name": "[%key:common::entity::button::down::name%]"
|
||||
},
|
||||
"e_manual": {
|
||||
"name": "E-Manual"
|
||||
},
|
||||
"exit": {
|
||||
"name": "[%key:common::entity::button::exit::name%]"
|
||||
},
|
||||
"fast_forward": {
|
||||
"name": "Fast forward"
|
||||
},
|
||||
"green": {
|
||||
"name": "Green"
|
||||
},
|
||||
"home": {
|
||||
"name": "[%key:common::entity::button::home::name%]"
|
||||
},
|
||||
"info": {
|
||||
"name": "[%key:common::entity::button::info::name%]"
|
||||
},
|
||||
"left": {
|
||||
"name": "[%key:common::entity::button::left::name%]"
|
||||
},
|
||||
"num_0": {
|
||||
"name": "[%key:common::entity::button::num_0::name%]"
|
||||
},
|
||||
"num_1": {
|
||||
"name": "[%key:common::entity::button::num_1::name%]"
|
||||
},
|
||||
"num_2": {
|
||||
"name": "[%key:common::entity::button::num_2::name%]"
|
||||
},
|
||||
"num_3": {
|
||||
"name": "[%key:common::entity::button::num_3::name%]"
|
||||
},
|
||||
"num_4": {
|
||||
"name": "[%key:common::entity::button::num_4::name%]"
|
||||
},
|
||||
"num_5": {
|
||||
"name": "[%key:common::entity::button::num_5::name%]"
|
||||
},
|
||||
"num_6": {
|
||||
"name": "[%key:common::entity::button::num_6::name%]"
|
||||
},
|
||||
"num_7": {
|
||||
"name": "[%key:common::entity::button::num_7::name%]"
|
||||
},
|
||||
"num_8": {
|
||||
"name": "[%key:common::entity::button::num_8::name%]"
|
||||
},
|
||||
"num_9": {
|
||||
"name": "[%key:common::entity::button::num_9::name%]"
|
||||
},
|
||||
"ok": {
|
||||
"name": "[%key:common::entity::button::ok::name%]"
|
||||
},
|
||||
"power": {
|
||||
"name": "[%key:common::entity::button::power::name%]"
|
||||
},
|
||||
"previous_channel": {
|
||||
"name": "Previous channel"
|
||||
},
|
||||
"record": {
|
||||
"name": "Record"
|
||||
},
|
||||
"red": {
|
||||
"name": "Red"
|
||||
},
|
||||
"return": {
|
||||
"name": "Return"
|
||||
},
|
||||
"rewind": {
|
||||
"name": "Rewind"
|
||||
},
|
||||
"right": {
|
||||
"name": "[%key:common::entity::button::right::name%]"
|
||||
},
|
||||
"settings": {
|
||||
"name": "Settings"
|
||||
},
|
||||
"source": {
|
||||
"name": "Source"
|
||||
},
|
||||
"tools": {
|
||||
"name": "Tools"
|
||||
},
|
||||
"up": {
|
||||
"name": "[%key:common::entity::button::up::name%]"
|
||||
},
|
||||
"yellow": {
|
||||
"name": "Yellow"
|
||||
}
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"device_type": {
|
||||
"options": {
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
"requirements": [
|
||||
"getmac==0.9.5",
|
||||
"samsungctl[websocket]==0.7.1",
|
||||
"samsungtvws[async,encrypted]==3.0.5",
|
||||
"samsungtvws[async,encrypted]==2.7.2",
|
||||
"wakeonlan==3.3.0",
|
||||
"async-upnp-client==0.46.2"
|
||||
],
|
||||
|
||||
@@ -6,7 +6,6 @@ import logging
|
||||
|
||||
from sense_energy import (
|
||||
ASyncSenseable,
|
||||
SenseAPIException,
|
||||
SenseAuthenticationException,
|
||||
SenseMFARequiredException,
|
||||
)
|
||||
@@ -89,10 +88,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: SenseConfigEntry) -> boo
|
||||
) from err
|
||||
except SENSE_WEBSOCKET_EXCEPTIONS as err:
|
||||
raise ConfigEntryNotReady(str(err) or "Error during realtime update") from err
|
||||
except SenseAPIException as err:
|
||||
raise ConfigEntryNotReady(
|
||||
str(err) or "API error retrieving realtime data"
|
||||
) from err
|
||||
|
||||
trends_coordinator = SenseTrendCoordinator(hass, entry, gateway)
|
||||
realtime_coordinator = SenseRealtimeCoordinator(hass, entry, gateway)
|
||||
|
||||
@@ -6,7 +6,6 @@ from typing import TYPE_CHECKING
|
||||
|
||||
from sense_energy import (
|
||||
ASyncSenseable,
|
||||
SenseAPIException,
|
||||
SenseAuthenticationException,
|
||||
SenseMFARequiredException,
|
||||
)
|
||||
@@ -94,8 +93,6 @@ class SenseRealtimeCoordinator(SenseCoordinator):
|
||||
try:
|
||||
await self._gateway.update_realtime()
|
||||
except SENSE_TIMEOUT_EXCEPTIONS as ex:
|
||||
raise UpdateFailed(f"Timeout retrieving realtime data: {ex}") from ex
|
||||
_LOGGER.error("Timeout retrieving data: %s", ex)
|
||||
except SENSE_WEBSOCKET_EXCEPTIONS as ex:
|
||||
raise UpdateFailed(f"Failed to update realtime data: {ex}") from ex
|
||||
except SenseAPIException as ex:
|
||||
raise UpdateFailed(f"API error retrieving realtime data: {ex}") from ex
|
||||
_LOGGER.error("Failed to update data: %s", ex)
|
||||
|
||||
@@ -72,10 +72,8 @@ async def async_setup_entry(
|
||||
for device in entry_data.devices.values()
|
||||
for component in device.status
|
||||
if (
|
||||
Capability.SWITCH in device.status[component]
|
||||
and any(
|
||||
capability in device.status[component] for capability in CAPABILITIES
|
||||
)
|
||||
Capability.SWITCH in device.status[MAIN]
|
||||
and any(capability in device.status[MAIN] for capability in CAPABILITIES)
|
||||
and Capability.SAMSUNG_CE_LAMP not in device.status[component]
|
||||
)
|
||||
]
|
||||
|
||||
@@ -50,7 +50,6 @@ DEVICE_CLASS_MAP: dict[Category | str, MediaPlayerDeviceClass] = {
|
||||
Category.SPEAKER: MediaPlayerDeviceClass.SPEAKER,
|
||||
Category.TELEVISION: MediaPlayerDeviceClass.TV,
|
||||
Category.RECEIVER: MediaPlayerDeviceClass.RECEIVER,
|
||||
Category.PROJECTOR: MediaPlayerDeviceClass.PROJECTOR,
|
||||
}
|
||||
|
||||
VALUE_TO_STATE = {
|
||||
|
||||
@@ -75,6 +75,10 @@ class VolvoLock(VolvoEntity, LockEntity):
|
||||
|
||||
def _update_state(self, api_field: VolvoCarsApiBaseModel | None) -> None:
|
||||
"""Update the state of the entity."""
|
||||
if api_field is None:
|
||||
self._attr_is_locked = None
|
||||
return
|
||||
|
||||
assert isinstance(api_field, VolvoCarsValue)
|
||||
self._attr_is_locked = api_field.value == "LOCKED"
|
||||
|
||||
|
||||
@@ -3759,12 +3759,6 @@
|
||||
"iot_class": "cloud_push",
|
||||
"name": "LG ThinQ"
|
||||
},
|
||||
"lg_tv_rs232": {
|
||||
"integration_type": "device",
|
||||
"config_flow": true,
|
||||
"iot_class": "local_polling",
|
||||
"name": "LG TV via Serial"
|
||||
},
|
||||
"webostv": {
|
||||
"integration_type": "device",
|
||||
"config_flow": true,
|
||||
@@ -3773,6 +3767,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"lg_tv_rs232": {
|
||||
"name": "LG TV via Serial",
|
||||
"integration_type": "device",
|
||||
"config_flow": true,
|
||||
"iot_class": "local_polling"
|
||||
},
|
||||
"libre_hardware_monitor": {
|
||||
"name": "Libre Hardware Monitor",
|
||||
"integration_type": "device",
|
||||
|
||||
@@ -31,6 +31,7 @@ from homeassistant.const import (
|
||||
MAX_LENGTH_STATE_DOMAIN,
|
||||
MAX_LENGTH_STATE_ENTITY_ID,
|
||||
STATE_UNAVAILABLE,
|
||||
STATE_UNKNOWN,
|
||||
EntityCategory,
|
||||
Platform,
|
||||
)
|
||||
@@ -1950,10 +1951,9 @@ class EntityRegistry(BaseRegistry):
|
||||
This should only be used when an entity needs to be migrated between
|
||||
integrations.
|
||||
"""
|
||||
# import here to avoid circular import
|
||||
from .entity import entity_sources # noqa: PLC0415
|
||||
|
||||
if entity_id in entity_sources(self.hass):
|
||||
if (
|
||||
state := self.hass.states.get(entity_id)
|
||||
) is not None and state.state != STATE_UNKNOWN:
|
||||
raise ValueError("Only entities that haven't been loaded can be migrated")
|
||||
|
||||
old = self.entities[entity_id]
|
||||
|
||||
@@ -39,7 +39,7 @@ habluetooth==6.7.9
|
||||
hass-nabucasa==2.2.0
|
||||
hassil==3.5.0
|
||||
home-assistant-bluetooth==2.0.0
|
||||
home-assistant-frontend==20260527.1
|
||||
home-assistant-frontend==20260527.0
|
||||
home-assistant-intents==2026.5.5
|
||||
httpx==0.28.1
|
||||
ifaddr==0.2.0
|
||||
|
||||
@@ -99,8 +99,6 @@ Every check has a code following the
|
||||
| `W7407` | [`home-assistant-config-flow-polling-field`](#w7407-home-assistant-config-flow-polling-field) | Config flow should not include polling interval fields |
|
||||
| `W7408` | [`home-assistant-config-flow-name-field`](#w7408-home-assistant-config-flow-name-field) | Config flow should not include name fields |
|
||||
| `R7402` | [`home-assistant-unused-test-fixture-argument`](#r7402-home-assistant-unused-test-fixture-argument) | Unused test function argument should use `@pytest.mark.usefixtures` |
|
||||
| `W7418` | [`home-assistant-tests-direct-async-setup-entry`](#w7418-home-assistant-tests-direct-async-setup-entry) | Tests should not call an integration's `async_setup_entry` directly |
|
||||
| `W7420` | [`home-assistant-tests-direct-platform-async-setup-entry`](#w7420-home-assistant-tests-direct-platform-async-setup-entry) | Tests should not call a platform's `async_setup_entry` directly |
|
||||
| `W7422` | [`home-assistant-tests-direct-async-setup`](#w7422-home-assistant-tests-direct-async-setup) | Tests should not call an integration's `async_setup` directly |
|
||||
|
||||
|
||||
@@ -344,27 +342,6 @@ only needed for its side effects.
|
||||
This rule only applies to `test_*` functions, not to fixture functions.
|
||||
|
||||
|
||||
## `home_assistant_tests_direct_async_setup_entry` checker
|
||||
|
||||
Detects tests that call an integration's `async_setup_entry` directly.
|
||||
|
||||
### `W7418`: `home-assistant-tests-direct-async-setup-entry`
|
||||
|
||||
Tests should not invoke an integration's `async_setup_entry` from
|
||||
`__init__.py` directly. Instead, tests should let Home Assistant perform
|
||||
the setup via `await hass.config_entries.async_setup(entry.entry_id)` so
|
||||
that the real setup pipeline (platforms, services, listeners, unload
|
||||
handlers, etc.) is exercised.
|
||||
|
||||
### `W7420`: `home-assistant-tests-direct-platform-async-setup-entry`
|
||||
|
||||
Same as `W7418`, but for an entity platform's `async_setup_entry` (e.g.
|
||||
`homeassistant.components.<integration>.sensor.async_setup_entry`).
|
||||
Tests should drive setup through `hass.config_entries.async_setup` so
|
||||
the platform is loaded via the normal Home Assistant flow.
|
||||
|
||||
See [epic #77](https://github.com/home-assistant/epics/issues/77).
|
||||
|
||||
|
||||
## `home_assistant_tests_direct_async_setup` checker
|
||||
|
||||
|
||||
@@ -1,138 +0,0 @@
|
||||
"""Checker for direct calls to ``async_setup_entry`` from tests.
|
||||
|
||||
Tests should not invoke an integration's ``async_setup_entry`` directly
|
||||
(either the one in ``__init__.py`` or in an entity-platform module).
|
||||
Instead, tests should let Home Assistant perform the setup via
|
||||
``await hass.config_entries.async_setup(entry.entry_id)`` so that the
|
||||
real setup pipeline (platforms, services, listeners, unload handlers,
|
||||
etc.) is exercised.
|
||||
|
||||
This checker flags any call to ``async_setup_entry`` (whether awaited or
|
||||
not, accessed as a name or an attribute) made from a test module whose
|
||||
target resolves to a module-level function defined under
|
||||
``homeassistant.components.*``. The integration-init case and the
|
||||
entity-platform case get separate messages so violations can be tracked
|
||||
and fixed independently.
|
||||
"""
|
||||
|
||||
from enum import Enum
|
||||
|
||||
import astroid
|
||||
from astroid import nodes
|
||||
from pylint.checkers import BaseChecker
|
||||
from pylint.lint import PyLinter
|
||||
|
||||
from pylint_home_assistant.helpers.module_info import is_test_module, parse_module
|
||||
|
||||
|
||||
class _SetupKind(Enum):
|
||||
"""The kind of integration ``async_setup_entry`` being called."""
|
||||
|
||||
INIT = "init"
|
||||
PLATFORM = "platform"
|
||||
|
||||
|
||||
def _resolve_integration_async_setup_entry(call: nodes.Call) -> _SetupKind | None:
|
||||
"""Return the kind of integration ``async_setup_entry`` *call* targets.
|
||||
|
||||
Returns ``_SetupKind.INIT`` if the target is in the integration's
|
||||
``__init__`` module, ``_SetupKind.PLATFORM`` if it is in an
|
||||
entity-platform module, or ``None`` if the call does not resolve to
|
||||
an integration's ``async_setup_entry``.
|
||||
"""
|
||||
func = call.func
|
||||
match func:
|
||||
case nodes.Attribute(attrname="async_setup_entry"):
|
||||
pass
|
||||
case nodes.Name(name="async_setup_entry"):
|
||||
pass
|
||||
case _:
|
||||
return None
|
||||
|
||||
seen_qnames: set[str] = set()
|
||||
try:
|
||||
for inferred in func.infer():
|
||||
if inferred is astroid.Uninferable:
|
||||
continue
|
||||
if not isinstance(inferred, (nodes.FunctionDef, nodes.AsyncFunctionDef)):
|
||||
continue
|
||||
# Require the function to be defined at module level so that
|
||||
# class methods named ``async_setup_entry`` (whose qname
|
||||
# includes the class name) are not classified as integration
|
||||
# setup functions.
|
||||
if not isinstance(inferred.parent, nodes.Module):
|
||||
continue
|
||||
module_qname = inferred.parent.qname()
|
||||
if not module_qname or module_qname in seen_qnames:
|
||||
continue
|
||||
seen_qnames.add(module_qname)
|
||||
parsed = parse_module(module_qname)
|
||||
if parsed is None:
|
||||
continue
|
||||
return _SetupKind.INIT if parsed.module is None else _SetupKind.PLATFORM
|
||||
except astroid.exceptions.InferenceError, astroid.exceptions.AstroidError:
|
||||
return None
|
||||
return None
|
||||
|
||||
|
||||
class DirectAsyncSetupEntry(BaseChecker):
|
||||
"""Checker for direct calls to async_setup_entry in tests."""
|
||||
|
||||
name = "home_assistant_tests_direct_async_setup_entry"
|
||||
priority = -1
|
||||
msgs = {
|
||||
"W7418": (
|
||||
(
|
||||
"Do not call `async_setup_entry` directly from tests; use "
|
||||
"`await hass.config_entries.async_setup(entry.entry_id)` instead"
|
||||
),
|
||||
"home-assistant-tests-direct-async-setup-entry",
|
||||
(
|
||||
"Used when a test module calls an integration's "
|
||||
"`async_setup_entry` from `__init__.py` directly. Tests should "
|
||||
"let Home Assistant drive the setup so the full setup pipeline "
|
||||
"is exercised."
|
||||
),
|
||||
),
|
||||
"W7420": (
|
||||
(
|
||||
"Do not call a platform's `async_setup_entry` directly from "
|
||||
"tests; use `await hass.config_entries.async_setup(entry.entry_id)`"
|
||||
" instead"
|
||||
),
|
||||
"home-assistant-tests-direct-platform-async-setup-entry",
|
||||
(
|
||||
"Used when a test module calls an integration entity platform's "
|
||||
"`async_setup_entry` directly. Tests should let Home Assistant "
|
||||
"drive the setup so the full setup pipeline is exercised."
|
||||
),
|
||||
),
|
||||
}
|
||||
options = ()
|
||||
|
||||
_in_test_module: bool = False
|
||||
|
||||
def visit_module(self, node: nodes.Module) -> None:
|
||||
"""Record whether the current module is a test module."""
|
||||
self._in_test_module = is_test_module(node.name)
|
||||
|
||||
def visit_call(self, node: nodes.Call) -> None:
|
||||
"""Flag direct calls to an integration's async_setup_entry."""
|
||||
if not self._in_test_module:
|
||||
return
|
||||
match _resolve_integration_async_setup_entry(node):
|
||||
case _SetupKind.INIT:
|
||||
self.add_message(
|
||||
"home-assistant-tests-direct-async-setup-entry",
|
||||
node=node,
|
||||
)
|
||||
case _SetupKind.PLATFORM:
|
||||
self.add_message(
|
||||
"home-assistant-tests-direct-platform-async-setup-entry",
|
||||
node=node,
|
||||
)
|
||||
|
||||
|
||||
def register(linter: PyLinter) -> None:
|
||||
"""Register the checker."""
|
||||
linter.register_checker(DirectAsyncSetupEntry(linter))
|
||||
+1
-1
@@ -5,7 +5,7 @@ To update, run python3 -m script.hassfest
|
||||
|
||||
from typing import Final
|
||||
|
||||
FRONTEND_VERSION: Final[str] = "20260527.1"
|
||||
FRONTEND_VERSION: Final[str] = "20260527.0"
|
||||
|
||||
MDI_ICONS: Final[set[str]] = {
|
||||
"ab-testing",
|
||||
|
||||
Generated
+2
-2
@@ -1266,7 +1266,7 @@ hole==0.9.0
|
||||
holidays==0.97
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20260527.1
|
||||
home-assistant-frontend==20260527.0
|
||||
|
||||
# homeassistant.components.conversation
|
||||
home-assistant-intents==2026.5.5
|
||||
@@ -2929,7 +2929,7 @@ rxv==0.7.0
|
||||
samsungctl[websocket]==0.7.1
|
||||
|
||||
# homeassistant.components.samsungtv
|
||||
samsungtvws[async,encrypted]==3.0.5
|
||||
samsungtvws[async,encrypted]==2.7.2
|
||||
|
||||
# homeassistant.components.sanix
|
||||
sanix==1.0.6
|
||||
|
||||
@@ -3,38 +3,20 @@
|
||||
from pathlib import Path
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from pyatv.const import FeatureName, FeatureState
|
||||
from pyatv.exceptions import (
|
||||
BlockedStateError,
|
||||
ConnectionLostError,
|
||||
InvalidStateError,
|
||||
NotSupportedError,
|
||||
OperationTimeoutError,
|
||||
PlaybackError,
|
||||
ProtocolError,
|
||||
)
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.apple_tv.const import DOMAIN
|
||||
from homeassistant.components.media_player import (
|
||||
ATTR_MEDIA_CONTENT_ID,
|
||||
ATTR_MEDIA_CONTENT_TYPE,
|
||||
DOMAIN as MP_DOMAIN,
|
||||
SERVICE_PLAY_MEDIA,
|
||||
BrowseMedia,
|
||||
MediaClass,
|
||||
MediaType,
|
||||
)
|
||||
from homeassistant.components.media_source import PlayMedia
|
||||
from homeassistant.const import ATTR_ENTITY_ID
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
|
||||
from tests.typing import WebSocketGenerator
|
||||
|
||||
ENTITY_ID = "media_player.living_room_living_room"
|
||||
_MUSIC_URL = "http://example.local:8123/api/tts_proxy/abc.mp3"
|
||||
_VIDEO_URL = "http://example.local:8123/video.mp4"
|
||||
|
||||
pytestmark = pytest.mark.usefixtures("init_integration")
|
||||
|
||||
@@ -104,188 +86,3 @@ async def test_play_media_launches_app(
|
||||
|
||||
mock_atv.apps.launch_app.assert_awaited_once_with("com.netflix.Netflix")
|
||||
mock_atv.stream.stream_file.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("media_type", "media_id", "called_method", "stream_file_state"),
|
||||
[
|
||||
pytest.param(
|
||||
MediaType.MUSIC,
|
||||
_MUSIC_URL,
|
||||
"stream_file",
|
||||
FeatureState.Available,
|
||||
id="music_via_raop",
|
||||
),
|
||||
pytest.param(
|
||||
MediaType.VIDEO,
|
||||
_VIDEO_URL,
|
||||
"play_url",
|
||||
FeatureState.Unsupported,
|
||||
id="video_via_airplay",
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_play_media_selects_streaming_method(
|
||||
hass: HomeAssistant,
|
||||
mock_atv: AsyncMock,
|
||||
media_type: MediaType,
|
||||
media_id: str,
|
||||
called_method: str,
|
||||
stream_file_state: FeatureState,
|
||||
) -> None:
|
||||
"""Streaming path is selected from device feature state, not _playing."""
|
||||
mock_atv.features.set_state(FeatureName.StreamFile, stream_file_state)
|
||||
|
||||
await hass.services.async_call(
|
||||
MP_DOMAIN,
|
||||
SERVICE_PLAY_MEDIA,
|
||||
{
|
||||
ATTR_ENTITY_ID: ENTITY_ID,
|
||||
ATTR_MEDIA_CONTENT_TYPE: media_type,
|
||||
ATTR_MEDIA_CONTENT_ID: media_id,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
getattr(mock_atv.stream, called_method).assert_awaited_once_with(media_id)
|
||||
|
||||
|
||||
async def test_play_media_falls_back_to_play_url(
|
||||
hass: HomeAssistant,
|
||||
mock_atv: AsyncMock,
|
||||
) -> None:
|
||||
"""When StreamFile is unavailable, play_url is used for video."""
|
||||
mock_atv.features.set_state(FeatureName.StreamFile, FeatureState.Unsupported)
|
||||
|
||||
await hass.services.async_call(
|
||||
MP_DOMAIN,
|
||||
SERVICE_PLAY_MEDIA,
|
||||
{
|
||||
ATTR_ENTITY_ID: ENTITY_ID,
|
||||
ATTR_MEDIA_CONTENT_TYPE: MediaType.VIDEO,
|
||||
ATTR_MEDIA_CONTENT_ID: _VIDEO_URL,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
mock_atv.stream.play_url.assert_awaited_once_with(_VIDEO_URL)
|
||||
mock_atv.stream.stream_file.assert_not_called()
|
||||
|
||||
|
||||
async def test_play_media_raises_when_no_streaming_method(
|
||||
hass: HomeAssistant,
|
||||
mock_atv: AsyncMock,
|
||||
) -> None:
|
||||
"""Raise HomeAssistantError when no streaming method is available."""
|
||||
mock_atv.features.set_state(FeatureName.StreamFile, FeatureState.Unsupported)
|
||||
mock_atv.features.set_state(FeatureName.PlayUrl, FeatureState.Unsupported)
|
||||
|
||||
with pytest.raises(HomeAssistantError) as exc_info:
|
||||
await hass.services.async_call(
|
||||
MP_DOMAIN,
|
||||
SERVICE_PLAY_MEDIA,
|
||||
{
|
||||
ATTR_ENTITY_ID: ENTITY_ID,
|
||||
ATTR_MEDIA_CONTENT_TYPE: MediaType.MUSIC,
|
||||
ATTR_MEDIA_CONTENT_ID: _MUSIC_URL,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert exc_info.value.translation_key == "streaming_not_supported"
|
||||
assert exc_info.value.translation_domain == DOMAIN
|
||||
mock_atv.stream.stream_file.assert_not_called()
|
||||
mock_atv.stream.play_url.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("stream_attr", "media_type", "media_id", "stream_file_state"),
|
||||
[
|
||||
(
|
||||
"stream_file",
|
||||
MediaType.MUSIC,
|
||||
_MUSIC_URL,
|
||||
FeatureState.Available,
|
||||
),
|
||||
(
|
||||
"play_url",
|
||||
MediaType.VIDEO,
|
||||
_VIDEO_URL,
|
||||
FeatureState.Unsupported,
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("exc_class", "expected_translation_key"),
|
||||
[
|
||||
(BlockedStateError, "stream_failed"),
|
||||
(ConnectionLostError, "stream_failed"),
|
||||
(InvalidStateError, "stream_failed"),
|
||||
(NotSupportedError, "streaming_not_supported"),
|
||||
(OperationTimeoutError, "stream_failed"),
|
||||
(PlaybackError, "stream_failed"),
|
||||
(ProtocolError, "stream_failed"),
|
||||
],
|
||||
)
|
||||
async def test_play_media_raises_ha_error_on_pyatv_failure(
|
||||
hass: HomeAssistant,
|
||||
mock_atv: AsyncMock,
|
||||
stream_attr: str,
|
||||
media_type: MediaType,
|
||||
media_id: str,
|
||||
stream_file_state: FeatureState,
|
||||
exc_class: type[Exception],
|
||||
expected_translation_key: str,
|
||||
) -> None:
|
||||
"""Pyatv streaming exceptions surface as a translated HomeAssistantError."""
|
||||
mock_atv.features.set_state(FeatureName.StreamFile, stream_file_state)
|
||||
getattr(mock_atv.stream, stream_attr).side_effect = exc_class("error")
|
||||
|
||||
with pytest.raises(HomeAssistantError) as exc_info:
|
||||
await hass.services.async_call(
|
||||
MP_DOMAIN,
|
||||
SERVICE_PLAY_MEDIA,
|
||||
{
|
||||
ATTR_ENTITY_ID: ENTITY_ID,
|
||||
ATTR_MEDIA_CONTENT_TYPE: media_type,
|
||||
ATTR_MEDIA_CONTENT_ID: media_id,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert exc_info.value.translation_key == expected_translation_key
|
||||
assert exc_info.value.translation_domain == DOMAIN
|
||||
|
||||
|
||||
async def test_browse_media_uses_media_source(
|
||||
hass: HomeAssistant,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
) -> None:
|
||||
"""async_browse_media routes to media_source when streaming is available."""
|
||||
browse_result = BrowseMedia(
|
||||
title="Media",
|
||||
media_class=MediaClass.DIRECTORY,
|
||||
media_content_id="",
|
||||
media_content_type="",
|
||||
can_play=False,
|
||||
can_expand=True,
|
||||
children=[],
|
||||
)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.apple_tv.media_player.media_source.async_browse_media",
|
||||
new_callable=AsyncMock,
|
||||
return_value=browse_result,
|
||||
) as mock_browse:
|
||||
client = await hass_ws_client()
|
||||
await client.send_json(
|
||||
{
|
||||
"id": 1,
|
||||
"type": "media_player/browse_media",
|
||||
"entity_id": ENTITY_ID,
|
||||
}
|
||||
)
|
||||
response = await client.receive_json()
|
||||
|
||||
assert response["success"]
|
||||
mock_browse.assert_called_once()
|
||||
|
||||
@@ -81,7 +81,6 @@ async def test_config_exceptions(
|
||||
),
|
||||
pytest.raises(config_error),
|
||||
):
|
||||
# pylint: disable-next=home-assistant-tests-direct-async-setup-entry
|
||||
await async_setup_entry(hass, config_entry)
|
||||
|
||||
|
||||
|
||||
@@ -98,29 +98,6 @@ async def test_setup_entry_success(
|
||||
assert init_integration.state is ConfigEntryState.LOADED
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"exception",
|
||||
[
|
||||
pytest.param(DucoError("lan info error"), id="duco_error"),
|
||||
pytest.param(DucoConnectionError("lan info offline"), id="connection_error"),
|
||||
],
|
||||
)
|
||||
async def test_setup_entry_ignores_lan_info_failures(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_duco_client: AsyncMock,
|
||||
exception: Exception,
|
||||
) -> None:
|
||||
"""Test setup succeeds when the supplemental LAN info endpoint fails."""
|
||||
mock_duco_client.async_get_lan_info.side_effect = exception
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert mock_config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
|
||||
@pytest.mark.parametrize("unsupported_board_info", UNSUPPORTED_BOARD_INFOS)
|
||||
async def test_setup_entry_unsupported_board_info(
|
||||
hass: HomeAssistant,
|
||||
|
||||
@@ -122,34 +122,24 @@ async def test_coordinator_update_duco_error_marks_unavailable(
|
||||
assert state.state == STATE_UNAVAILABLE
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"exception",
|
||||
[
|
||||
pytest.param(DucoError("lan info error"), id="duco_error"),
|
||||
pytest.param(DucoConnectionError("lan info offline"), id="connection_error"),
|
||||
],
|
||||
)
|
||||
@pytest.mark.usefixtures("entity_registry_enabled_by_default", "init_integration")
|
||||
async def test_lan_info_failures_keep_node_entities_available(
|
||||
async def test_lan_info_duco_error_marks_unavailable(
|
||||
hass: HomeAssistant,
|
||||
mock_duco_client: AsyncMock,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
exception: Exception,
|
||||
) -> None:
|
||||
"""Test node entities stay available when LAN info retrieval fails."""
|
||||
mock_duco_client.async_get_lan_info = AsyncMock(side_effect=exception)
|
||||
"""Test entities become unavailable when async_get_lan_info raises DucoError."""
|
||||
mock_duco_client.async_get_lan_info = AsyncMock(
|
||||
side_effect=DucoError("lan info error")
|
||||
)
|
||||
|
||||
freezer.tick(SCAN_INTERVAL)
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
state = hass.states.get("sensor.office_co2_carbon_dioxide")
|
||||
assert state is not None
|
||||
assert state.state == "405"
|
||||
|
||||
state = hass.states.get("sensor.living_signal_strength")
|
||||
assert state is not None
|
||||
assert state.state == "-60"
|
||||
assert state.state == STATE_UNAVAILABLE
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
||||
@@ -83,7 +83,6 @@ async def test_setup_entry_successful(hass: HomeAssistant) -> None:
|
||||
"homeassistant.components.emulated_roku.binding.EmulatedRokuServer",
|
||||
return_value=Mock(start=AsyncMock(), close=AsyncMock()),
|
||||
) as instantiate:
|
||||
# pylint: disable-next=home-assistant-tests-direct-async-setup-entry
|
||||
assert await emulated_roku.async_setup_entry(hass, entry) is True
|
||||
|
||||
assert len(instantiate.mock_calls) == 1
|
||||
@@ -102,7 +101,6 @@ async def test_unload_entry(hass: HomeAssistant) -> None:
|
||||
"homeassistant.components.emulated_roku.binding.EmulatedRokuServer",
|
||||
return_value=Mock(start=AsyncMock(), close=AsyncMock()),
|
||||
):
|
||||
# pylint: disable-next=home-assistant-tests-direct-async-setup-entry
|
||||
assert await emulated_roku.async_setup_entry(hass, entry) is True
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
@@ -4176,12 +4176,6 @@ async def test_channel(hass: HomeAssistant) -> None:
|
||||
media_player.MediaPlayerDeviceClass.TV,
|
||||
None,
|
||||
)
|
||||
assert trait.ChannelTrait.supported(
|
||||
media_player.DOMAIN,
|
||||
MediaPlayerEntityFeature.PLAY_MEDIA,
|
||||
media_player.MediaPlayerDeviceClass.PROJECTOR,
|
||||
None,
|
||||
)
|
||||
assert (
|
||||
trait.ChannelTrait.supported(
|
||||
media_player.DOMAIN,
|
||||
|
||||
@@ -246,13 +246,6 @@ def test_type_covers(type_name, entity_id, state, attrs) -> None:
|
||||
{ATTR_DEVICE_CLASS: MediaPlayerDeviceClass.RECEIVER},
|
||||
{},
|
||||
),
|
||||
(
|
||||
"TelevisionMediaPlayer",
|
||||
"media_player.projector",
|
||||
"on",
|
||||
{ATTR_DEVICE_CLASS: MediaPlayerDeviceClass.PROJECTOR},
|
||||
{},
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_type_media_player(type_name, entity_id, state, attrs, config) -> None:
|
||||
|
||||
@@ -32,7 +32,6 @@ async def test_ha_mqtt_publish(
|
||||
mock_discovery.start.return_value = []
|
||||
mock_discovery_class.return_value = mock_discovery
|
||||
|
||||
# pylint: disable-next=home-assistant-tests-direct-async-setup-entry
|
||||
await inels.async_setup_entry(hass, config_entry)
|
||||
|
||||
topic, payload, qos, retain = "test/topic", "test_payload", 1, True
|
||||
@@ -61,7 +60,6 @@ async def test_ha_mqtt_subscribe(
|
||||
mock_discovery.start.return_value = []
|
||||
mock_discovery_class.return_value = mock_discovery
|
||||
|
||||
# pylint: disable-next=home-assistant-tests-direct-async-setup-entry
|
||||
await inels.async_setup_entry(hass, config_entry)
|
||||
|
||||
topic = "test/topic"
|
||||
@@ -84,7 +82,6 @@ async def test_ha_mqtt_not_available(
|
||||
),
|
||||
pytest.raises(ConfigEntryNotReady, match="MQTT integration not available"),
|
||||
):
|
||||
# pylint: disable-next=home-assistant-tests-direct-async-setup-entry
|
||||
await inels.async_setup_entry(hass, config_entry)
|
||||
|
||||
|
||||
|
||||
@@ -62,7 +62,6 @@ async def test_async_setup_entry_connection_error(
|
||||
)
|
||||
|
||||
with pytest.raises(ConfigEntryNotReady):
|
||||
# pylint: disable-next=home-assistant-tests-direct-async-setup-entry
|
||||
await async_setup_entry(hass, mock_config_entry)
|
||||
|
||||
assert mock_iometer_client.get_current_status.await_count == 1
|
||||
|
||||
@@ -42,7 +42,6 @@ async def test_setup_demo_platform(hass: HomeAssistant) -> None:
|
||||
"""Test setup."""
|
||||
mock = MagicMock()
|
||||
add_entities = mock.MagicMock()
|
||||
# pylint: disable-next=home-assistant-tests-direct-platform-async-setup-entry
|
||||
await demo.async_setup_entry(hass, {}, add_entities)
|
||||
assert add_entities.call_count == 1
|
||||
|
||||
|
||||
@@ -69,15 +69,9 @@ def test_create_matter_ble_proxy_wires_ha_backends(hass: HomeAssistant) -> None:
|
||||
assert kwargs["ws_url"] == "ws://localhost:5580/ble"
|
||||
assert isinstance(kwargs["scan_source"], HaBluetoothScanSource)
|
||||
assert isinstance(kwargs["device_resolver"], HaBluetoothDeviceResolver)
|
||||
assert kwargs["task_factory"] == hass.async_create_task
|
||||
assert result is proxy_cls.return_value
|
||||
|
||||
coro = MagicMock()
|
||||
with patch.object(hass, "async_create_background_task") as bg_task:
|
||||
task = kwargs["task_factory"](coro)
|
||||
|
||||
bg_task.assert_called_once_with(coro, name="matter_ble_proxy")
|
||||
assert task is bg_task.return_value
|
||||
|
||||
|
||||
async def test_scan_source_start_registers_passive_callback(
|
||||
hass: HomeAssistant,
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
# serializer version: 1
|
||||
# name: test_entry_diagnostics
|
||||
dict({
|
||||
'data': dict({
|
||||
'api_key': '**REDACTED**',
|
||||
}),
|
||||
'entities': dict({
|
||||
'conversation.meta_llama_3_3_70b_instruct': dict({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'categories': dict({
|
||||
}),
|
||||
'device_class': None,
|
||||
'disabled_by': None,
|
||||
'entity_category': None,
|
||||
'entity_id': 'conversation.meta_llama_3_3_70b_instruct',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'labels': list([
|
||||
]),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
'conversation': dict({
|
||||
'should_expose': False,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': None,
|
||||
'platform': 'ovhcloud_ai_endpoints',
|
||||
'translation_key': None,
|
||||
}),
|
||||
}),
|
||||
'entry_version': '1.1',
|
||||
'options': dict({
|
||||
}),
|
||||
'state': 'loaded',
|
||||
'subentries': list([
|
||||
dict({
|
||||
'data': dict({
|
||||
'model': 'Meta-Llama-3_3-70B-Instruct',
|
||||
'prompt': '**REDACTED**',
|
||||
}),
|
||||
'subentry_type': 'conversation',
|
||||
'title': 'Meta-Llama-3_3-70B-Instruct',
|
||||
}),
|
||||
]),
|
||||
'title': 'OVHcloud AI Endpoints',
|
||||
})
|
||||
# ---
|
||||
@@ -17,13 +17,7 @@ import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components import conversation
|
||||
from homeassistant.components.ovhcloud_ai_endpoints.entity import (
|
||||
_convert_content_to_chat_message,
|
||||
_decode_tool_arguments,
|
||||
)
|
||||
from homeassistant.const import CONF_LLM_HASS_API, MATCH_ALL
|
||||
from homeassistant.core import Context, HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import entity_registry as er, intent
|
||||
from homeassistant.helpers.llm import ToolInput
|
||||
|
||||
@@ -465,58 +459,3 @@ async def test_openai_error(
|
||||
)
|
||||
|
||||
assert result.response.response_type == intent.IntentResponseType.ERROR
|
||||
|
||||
|
||||
async def test_supported_languages(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_openai_client: AsyncMock,
|
||||
) -> None:
|
||||
"""The conversation entity must advertise universal language support."""
|
||||
await setup_integration(hass, mock_config_entry, mock_openai_client)
|
||||
|
||||
agent = conversation.async_get_agent(
|
||||
hass, "conversation.meta_llama_3_3_70b_instruct"
|
||||
)
|
||||
assert agent is not None
|
||||
assert agent.supported_languages == MATCH_ALL
|
||||
|
||||
|
||||
async def test_converse_error(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_openai_client: AsyncMock,
|
||||
) -> None:
|
||||
"""A ConverseError from chat_log.async_provide_llm_data surfaces as ERROR."""
|
||||
await setup_integration(hass, mock_config_entry, mock_openai_client)
|
||||
|
||||
subentry = next(iter(mock_config_entry.subentries.values()))
|
||||
hass.config_entries.async_update_subentry(
|
||||
mock_config_entry,
|
||||
subentry,
|
||||
data={**subentry.data, CONF_LLM_HASS_API: "invalid_llm_api"},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
result = await conversation.async_converse(
|
||||
hass,
|
||||
"hello",
|
||||
None,
|
||||
Context(),
|
||||
agent_id="conversation.meta_llama_3_3_70b_instruct",
|
||||
)
|
||||
|
||||
assert result.response.response_type is intent.IntentResponseType.ERROR
|
||||
|
||||
|
||||
def test_decode_tool_arguments_invalid_json() -> None:
|
||||
"""Malformed tool-call JSON arguments raise HomeAssistantError."""
|
||||
with pytest.raises(HomeAssistantError, match="Unexpected tool argument response"):
|
||||
_decode_tool_arguments("{not-json")
|
||||
|
||||
|
||||
def test_convert_content_unmapped() -> None:
|
||||
"""Content that cannot be mapped to a Completions message returns None."""
|
||||
assert (
|
||||
_convert_content_to_chat_message(conversation.SystemContent(content="")) is None
|
||||
)
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
"""Test OVHcloud AI Endpoints diagnostics."""
|
||||
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from . import setup_integration
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
from tests.components.diagnostics import get_diagnostics_for_config_entry
|
||||
from tests.typing import ClientSessionGenerator
|
||||
|
||||
|
||||
async def test_entry_diagnostics(
|
||||
hass: HomeAssistant,
|
||||
hass_client: ClientSessionGenerator,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_openai_client: AsyncMock,
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> None:
|
||||
"""Test config entry diagnostics."""
|
||||
await setup_integration(hass, mock_config_entry, mock_openai_client)
|
||||
|
||||
diagnostics = await get_diagnostics_for_config_entry(
|
||||
hass, hass_client, mock_config_entry
|
||||
)
|
||||
|
||||
assert diagnostics.pop("client").startswith("openai==")
|
||||
diagnostics.pop("entry_id")
|
||||
subentries = diagnostics.pop("subentries")
|
||||
diagnostics["subentries"] = list(subentries.values())
|
||||
for entity in diagnostics["entities"].values():
|
||||
for key in (
|
||||
"config_entry_id",
|
||||
"config_subentry_id",
|
||||
"created_at",
|
||||
"device_id",
|
||||
"id",
|
||||
"modified_at",
|
||||
"unique_id",
|
||||
):
|
||||
entity.pop(key, None)
|
||||
|
||||
assert diagnostics == snapshot
|
||||
@@ -2,9 +2,7 @@
|
||||
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
import httpx
|
||||
from openai import AuthenticationError, BadRequestError, OpenAIError
|
||||
import pytest
|
||||
from openai import OpenAIError
|
||||
|
||||
from homeassistant.components.ovhcloud_ai_endpoints.const import DOMAIN
|
||||
from homeassistant.config_entries import ConfigEntryState, ConfigSubentry
|
||||
@@ -31,47 +29,16 @@ async def test_setup_unload(
|
||||
assert mock_config_entry.state is ConfigEntryState.NOT_LOADED
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("exception", "state"),
|
||||
[
|
||||
(
|
||||
AuthenticationError(
|
||||
message="invalid key",
|
||||
response=httpx.Response(
|
||||
status_code=401,
|
||||
request=httpx.Request(method="POST", url="https://example.com"),
|
||||
),
|
||||
body=None,
|
||||
),
|
||||
ConfigEntryState.SETUP_ERROR,
|
||||
),
|
||||
(
|
||||
BadRequestError(
|
||||
message="invalid parameter",
|
||||
response=httpx.Response(
|
||||
status_code=400,
|
||||
request=httpx.Request(method="POST", url="https://example.com"),
|
||||
),
|
||||
body=None,
|
||||
),
|
||||
ConfigEntryState.LOADED,
|
||||
),
|
||||
(OpenAIError("boom"), ConfigEntryState.SETUP_RETRY),
|
||||
(Exception("boom"), ConfigEntryState.SETUP_ERROR),
|
||||
],
|
||||
)
|
||||
async def test_setup_errors(
|
||||
async def test_setup_cannot_connect(
|
||||
hass: HomeAssistant,
|
||||
mock_openai_client: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
exception: Exception,
|
||||
state: ConfigEntryState,
|
||||
) -> None:
|
||||
"""Assert appropriate behavior according to various HTTP responses."""
|
||||
mock_openai_client.chat.completions.create.side_effect = exception
|
||||
"""Test that a connection error surfaces a setup retry."""
|
||||
mock_openai_client.chat.completions.create.side_effect = OpenAIError("boom")
|
||||
|
||||
await setup_integration(hass, mock_config_entry, mock_openai_client)
|
||||
assert mock_config_entry.state is state
|
||||
assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY
|
||||
|
||||
|
||||
async def test_new_subentry_creates_entity_and_device(
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,101 +0,0 @@
|
||||
"""Tests for the Samsung Infrared button platform."""
|
||||
|
||||
from infrared_protocols.codes.samsung.tv import SamsungTVCode
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS
|
||||
from homeassistant.const import ATTR_ENTITY_ID, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from tests.common import MockConfigEntry, snapshot_platform
|
||||
from tests.components.common import assert_availability_follows_source_entity
|
||||
from tests.components.infrared import EMITTER_ENTITY_ID
|
||||
from tests.components.infrared.common import MockInfraredEmitterEntity
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def platforms() -> list[Platform]:
|
||||
"""Return platforms to set up."""
|
||||
return [Platform.BUTTON]
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
async def test_entities(
|
||||
hass: HomeAssistant,
|
||||
snapshot: SnapshotAssertion,
|
||||
entity_registry: er.EntityRegistry,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test button entities are created with correct attributes."""
|
||||
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("entity_id", "expected_code"),
|
||||
[
|
||||
("button.samsung_tv_power", SamsungTVCode.POWER),
|
||||
("button.samsung_tv_source", SamsungTVCode.SOURCE),
|
||||
("button.samsung_tv_settings", SamsungTVCode.SETTINGS),
|
||||
("button.samsung_tv_info", SamsungTVCode.INFO),
|
||||
("button.samsung_tv_exit", SamsungTVCode.EXIT),
|
||||
("button.samsung_tv_return", SamsungTVCode.RETURN),
|
||||
("button.samsung_tv_home", SamsungTVCode.HOME),
|
||||
("button.samsung_tv_red", SamsungTVCode.RED),
|
||||
("button.samsung_tv_green", SamsungTVCode.GREEN),
|
||||
("button.samsung_tv_yellow", SamsungTVCode.YELLOW),
|
||||
("button.samsung_tv_blue", SamsungTVCode.BLUE),
|
||||
("button.samsung_tv_up", SamsungTVCode.NAV_UP),
|
||||
("button.samsung_tv_down", SamsungTVCode.NAV_DOWN),
|
||||
("button.samsung_tv_left", SamsungTVCode.NAV_LEFT),
|
||||
("button.samsung_tv_right", SamsungTVCode.NAV_RIGHT),
|
||||
("button.samsung_tv_ok", SamsungTVCode.OK),
|
||||
("button.samsung_tv_previous_channel", SamsungTVCode.PREVIOUS_CHANNEL),
|
||||
("button.samsung_tv_number_0", SamsungTVCode.NUM_0),
|
||||
("button.samsung_tv_number_1", SamsungTVCode.NUM_1),
|
||||
("button.samsung_tv_number_2", SamsungTVCode.NUM_2),
|
||||
("button.samsung_tv_number_3", SamsungTVCode.NUM_3),
|
||||
("button.samsung_tv_number_4", SamsungTVCode.NUM_4),
|
||||
("button.samsung_tv_number_5", SamsungTVCode.NUM_5),
|
||||
("button.samsung_tv_number_6", SamsungTVCode.NUM_6),
|
||||
("button.samsung_tv_number_7", SamsungTVCode.NUM_7),
|
||||
("button.samsung_tv_number_8", SamsungTVCode.NUM_8),
|
||||
("button.samsung_tv_number_9", SamsungTVCode.NUM_9),
|
||||
("button.samsung_tv_fast_forward", SamsungTVCode.FAST_FORWARD),
|
||||
("button.samsung_tv_rewind", SamsungTVCode.REWIND),
|
||||
("button.samsung_tv_record", SamsungTVCode.RECORD),
|
||||
("button.samsung_tv_tools", SamsungTVCode.TOOLS),
|
||||
("button.samsung_tv_browser", SamsungTVCode.BROWSER),
|
||||
("button.samsung_tv_ad_subtitle", SamsungTVCode.AD_SUBTITLE),
|
||||
("button.samsung_tv_e_manual", SamsungTVCode.E_MANUAL),
|
||||
],
|
||||
)
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
async def test_button_press_sends_correct_code(
|
||||
hass: HomeAssistant,
|
||||
mock_infrared_emitter_entity: MockInfraredEmitterEntity,
|
||||
entity_id: str,
|
||||
expected_code: SamsungTVCode,
|
||||
) -> None:
|
||||
"""Test pressing each button sends the correct IR code."""
|
||||
await hass.services.async_call(
|
||||
BUTTON_DOMAIN,
|
||||
SERVICE_PRESS,
|
||||
{ATTR_ENTITY_ID: entity_id},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert len(mock_infrared_emitter_entity.send_command_calls) == 1
|
||||
assert (
|
||||
mock_infrared_emitter_entity.send_command_calls[0] == expected_code.to_command()
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
async def test_button_availability_follows_ir_entity(
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
"""Test button becomes unavailable when IR entity is unavailable."""
|
||||
entity_id = "button.samsung_tv_source"
|
||||
await assert_availability_follows_source_entity(hass, entity_id, EMITTER_ENTITY_ID)
|
||||
@@ -1,21 +1,19 @@
|
||||
"""The tests for Sense binary sensor platform."""
|
||||
|
||||
from datetime import timedelta
|
||||
from unittest.mock import MagicMock, patch
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
from sense_energy import SenseAPIException
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
|
||||
from homeassistant.components.sense.const import ACTIVE_UPDATE_RATE, DOMAIN
|
||||
from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE, Platform
|
||||
from homeassistant.components.sense.const import ACTIVE_UPDATE_RATE
|
||||
from homeassistant.const import STATE_OFF, STATE_ON, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.util.dt import utcnow
|
||||
|
||||
from . import setup_platform
|
||||
from .const import DEVICE_1_ID, DEVICE_1_NAME, DEVICE_2_NAME, MONITOR_ID
|
||||
from .const import DEVICE_1_NAME, DEVICE_2_NAME
|
||||
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform
|
||||
|
||||
@@ -68,50 +66,3 @@ async def test_on_off_sensors(
|
||||
|
||||
state = hass.states.get(f"binary_sensor.{DEVICE_2_NAME.lower()}_power")
|
||||
assert state.state == STATE_ON
|
||||
|
||||
|
||||
async def test_realtime_update_exception(
|
||||
hass: HomeAssistant,
|
||||
mock_sense: MagicMock,
|
||||
config_entry: MockConfigEntry,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
) -> None:
|
||||
"""Test that binary sensor entities become unavailable on realtime coordinator failure."""
|
||||
await setup_platform(hass, config_entry, Platform.BINARY_SENSOR)
|
||||
|
||||
state = hass.states.get(f"binary_sensor.{DEVICE_1_NAME.lower()}_power")
|
||||
assert state is not None
|
||||
assert state.state != STATE_UNAVAILABLE
|
||||
|
||||
mock_sense.update_realtime.side_effect = SenseAPIException("api error")
|
||||
|
||||
freezer.tick(timedelta(seconds=ACTIVE_UPDATE_RATE))
|
||||
async_fire_time_changed(hass, freezer())
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(f"binary_sensor.{DEVICE_1_NAME.lower()}_power")
|
||||
assert state.state == STATE_UNAVAILABLE
|
||||
|
||||
|
||||
async def test_migrate_unique_ids(
|
||||
hass: HomeAssistant,
|
||||
mock_sense: MagicMock,
|
||||
config_entry: MockConfigEntry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test that entities registered under the old bare device-ID unique_id are migrated."""
|
||||
config_entry.add_to_hass(hass)
|
||||
old_entry = entity_registry.async_get_or_create(
|
||||
BINARY_SENSOR_DOMAIN,
|
||||
DOMAIN,
|
||||
DEVICE_1_ID,
|
||||
config_entry=config_entry,
|
||||
)
|
||||
assert old_entry.unique_id == DEVICE_1_ID
|
||||
|
||||
with patch("homeassistant.components.sense.PLATFORMS", [Platform.BINARY_SENSOR]):
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
migrated = entity_registry.async_get(old_entry.entity_id)
|
||||
assert migrated.unique_id == f"{MONITOR_ID}-{DEVICE_1_ID}"
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"""Test the Sense config flow."""
|
||||
|
||||
from collections.abc import Iterator
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
from sense_energy import (
|
||||
@@ -22,9 +21,9 @@ from .const import MOCK_CONFIG
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
@pytest.fixture(name="mock_flow_sense")
|
||||
def mock_flow_sense_fixture() -> Iterator[MagicMock]:
|
||||
"""Mock Sense object for authentication."""
|
||||
@pytest.fixture(name="mock_sense")
|
||||
def mock_sense():
|
||||
"""Mock Sense object for authenticatation."""
|
||||
with patch(
|
||||
"homeassistant.components.sense.config_flow.ASyncSenseable"
|
||||
) as mock_sense:
|
||||
@@ -38,196 +37,259 @@ def mock_flow_sense_fixture() -> Iterator[MagicMock]:
|
||||
yield mock_sense
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_flow_sense")
|
||||
async def test_form(
|
||||
hass: HomeAssistant,
|
||||
mock_setup_entry: AsyncMock,
|
||||
) -> None:
|
||||
async def test_form(hass: HomeAssistant, mock_sense) -> None:
|
||||
"""Test we get the form."""
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {}
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{"timeout": "6", "email": "test-email", "password": "test-password"},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
with patch(
|
||||
"homeassistant.components.sense.async_setup_entry",
|
||||
return_value=True,
|
||||
) as mock_setup_entry:
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{"timeout": "6", "email": "test-email", "password": "test-password"},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == "test-email"
|
||||
assert result["data"] == MOCK_CONFIG
|
||||
mock_setup_entry.assert_called_once()
|
||||
assert result2["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result2["title"] == "test-email"
|
||||
assert result2["data"] == MOCK_CONFIG
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("exception", "error"),
|
||||
[
|
||||
(SenseAuthenticationException(), "invalid_auth"),
|
||||
(SenseAPITimeoutException(), "cannot_connect"),
|
||||
(SenseAPIException(), "cannot_connect"),
|
||||
(Exception("unknown exception"), "unknown"),
|
||||
],
|
||||
)
|
||||
@pytest.mark.usefixtures("mock_setup_entry")
|
||||
async def test_form_exceptions(
|
||||
hass: HomeAssistant,
|
||||
mock_flow_sense: MagicMock,
|
||||
exception: Exception,
|
||||
error: str,
|
||||
) -> None:
|
||||
"""Test we handle all exceptions in the user flow and can recover."""
|
||||
mock_flow_sense.return_value.authenticate.side_effect = exception
|
||||
|
||||
async def test_form_invalid_auth(hass: HomeAssistant) -> None:
|
||||
"""Test we handle invalid auth."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{"timeout": "6", "email": "test-email", "password": "test-password"},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {"base": error}
|
||||
with patch(
|
||||
"sense_energy.ASyncSenseable.authenticate",
|
||||
side_effect=SenseAuthenticationException,
|
||||
):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{"timeout": "6", "email": "test-email", "password": "test-password"},
|
||||
)
|
||||
|
||||
# Verify recovery: clear the error and complete the flow successfully
|
||||
mock_flow_sense.return_value.authenticate.side_effect = None
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{"timeout": "6", "email": "test-email", "password": "test-password"},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result2["type"] is FlowResultType.FORM
|
||||
assert result2["errors"] == {"base": "invalid_auth"}
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_setup_entry")
|
||||
async def test_form_mfa_required(
|
||||
hass: HomeAssistant,
|
||||
mock_flow_sense: MagicMock,
|
||||
) -> None:
|
||||
"""Test we handle the MFA flow."""
|
||||
mock_flow_sense.return_value.authenticate.side_effect = SenseMFARequiredException()
|
||||
|
||||
async def test_form_mfa_required(hass: HomeAssistant, mock_sense) -> None:
|
||||
"""Test we handle invalid auth."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
|
||||
mock_sense.return_value.authenticate.side_effect = SenseMFARequiredException
|
||||
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{"timeout": "6", "email": "test-email", "password": "test-password"},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "validation"
|
||||
assert result2["type"] is FlowResultType.FORM
|
||||
assert result2["step_id"] == "validation"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
mock_sense.return_value.validate_mfa.side_effect = None
|
||||
result3 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{CONF_CODE: "012345"},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == "test-email"
|
||||
assert result["data"] == MOCK_CONFIG
|
||||
assert result3["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result3["title"] == "test-email"
|
||||
assert result3["data"] == MOCK_CONFIG
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("exception", "error"),
|
||||
[
|
||||
(SenseAuthenticationException(), "invalid_auth"),
|
||||
(SenseAPITimeoutException(), "cannot_connect"),
|
||||
(SenseAPIException(), "cannot_connect"),
|
||||
(Exception("Unknown exception"), "unknown"),
|
||||
],
|
||||
)
|
||||
@pytest.mark.usefixtures("mock_setup_entry")
|
||||
async def test_form_mfa_exceptions(
|
||||
hass: HomeAssistant,
|
||||
mock_flow_sense: MagicMock,
|
||||
exception: Exception,
|
||||
error: str,
|
||||
) -> None:
|
||||
"""Test we handle all MFA validation exceptions and can recover."""
|
||||
mock_flow_sense.return_value.authenticate.side_effect = SenseMFARequiredException()
|
||||
|
||||
async def test_form_mfa_required_wrong(hass: HomeAssistant, mock_sense) -> None:
|
||||
"""Test we handle invalid auth."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
|
||||
mock_sense.return_value.authenticate.side_effect = SenseMFARequiredException
|
||||
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{"timeout": "6", "email": "test-email", "password": "test-password"},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "validation"
|
||||
assert result2["type"] is FlowResultType.FORM
|
||||
assert result2["step_id"] == "validation"
|
||||
|
||||
mock_flow_sense.return_value.validate_mfa.side_effect = exception
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
mock_sense.return_value.validate_mfa.side_effect = SenseAuthenticationException
|
||||
# Try with the WRONG verification code give us the form back again
|
||||
result3 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{CONF_CODE: "000000"},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {"base": error}
|
||||
assert result["step_id"] == "validation"
|
||||
assert result3["type"] is FlowResultType.FORM
|
||||
assert result3["errors"] == {"base": "invalid_auth"}
|
||||
assert result3["step_id"] == "validation"
|
||||
|
||||
# Verify recovery: clear the error and complete MFA successfully
|
||||
mock_flow_sense.return_value.validate_mfa.side_effect = None
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{CONF_CODE: "012345"},
|
||||
|
||||
async def test_form_mfa_required_timeout(hass: HomeAssistant, mock_sense) -> None:
|
||||
"""Test we handle invalid auth."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == "test-email"
|
||||
assert result["data"] == MOCK_CONFIG
|
||||
mock_sense.return_value.authenticate.side_effect = SenseMFARequiredException
|
||||
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{"timeout": "6", "email": "test-email", "password": "test-password"},
|
||||
)
|
||||
|
||||
assert result2["type"] is FlowResultType.FORM
|
||||
assert result2["step_id"] == "validation"
|
||||
|
||||
mock_sense.return_value.validate_mfa.side_effect = SenseAPITimeoutException
|
||||
result3 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{CONF_CODE: "000000"},
|
||||
)
|
||||
|
||||
assert result3["type"] is FlowResultType.FORM
|
||||
assert result3["errors"] == {"base": "cannot_connect"}
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_flow_sense")
|
||||
async def test_reauth_no_form(
|
||||
hass: HomeAssistant,
|
||||
mock_setup_entry: AsyncMock,
|
||||
) -> None:
|
||||
async def test_form_mfa_required_exception(hass: HomeAssistant, mock_sense) -> None:
|
||||
"""Test we handle invalid auth."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
mock_sense.return_value.authenticate.side_effect = SenseMFARequiredException
|
||||
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{"timeout": "6", "email": "test-email", "password": "test-password"},
|
||||
)
|
||||
|
||||
assert result2["type"] is FlowResultType.FORM
|
||||
assert result2["step_id"] == "validation"
|
||||
|
||||
mock_sense.return_value.validate_mfa.side_effect = Exception
|
||||
result3 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{CONF_CODE: "000000"},
|
||||
)
|
||||
|
||||
assert result3["type"] is FlowResultType.FORM
|
||||
assert result3["errors"] == {"base": "unknown"}
|
||||
|
||||
|
||||
async def test_form_timeout(hass: HomeAssistant) -> None:
|
||||
"""Test we handle cannot connect error."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
with patch(
|
||||
"sense_energy.ASyncSenseable.authenticate",
|
||||
side_effect=SenseAPITimeoutException,
|
||||
):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{"timeout": "6", "email": "test-email", "password": "test-password"},
|
||||
)
|
||||
|
||||
assert result2["type"] is FlowResultType.FORM
|
||||
assert result2["errors"] == {"base": "cannot_connect"}
|
||||
|
||||
|
||||
async def test_form_cannot_connect(hass: HomeAssistant) -> None:
|
||||
"""Test we handle cannot connect error."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
with patch(
|
||||
"sense_energy.ASyncSenseable.authenticate",
|
||||
side_effect=SenseAPIException,
|
||||
):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{"timeout": "6", "email": "test-email", "password": "test-password"},
|
||||
)
|
||||
|
||||
assert result2["type"] is FlowResultType.FORM
|
||||
assert result2["errors"] == {"base": "cannot_connect"}
|
||||
|
||||
|
||||
async def test_form_unknown_exception(hass: HomeAssistant) -> None:
|
||||
"""Test we handle unknown error."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
with patch(
|
||||
"sense_energy.ASyncSenseable.authenticate",
|
||||
side_effect=Exception,
|
||||
):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{"timeout": "6", "email": "test-email", "password": "test-password"},
|
||||
)
|
||||
|
||||
assert result2["type"] is FlowResultType.FORM
|
||||
assert result2["errors"] == {"base": "unknown"}
|
||||
|
||||
|
||||
async def test_reauth_no_form(hass: HomeAssistant, mock_sense) -> None:
|
||||
"""Test reauth where no form needed."""
|
||||
|
||||
# set up initially
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data=MOCK_CONFIG,
|
||||
unique_id="test-email",
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
result = await entry.start_reauth_flow(hass)
|
||||
await hass.async_block_till_done()
|
||||
with patch(
|
||||
"homeassistant.config_entries.ConfigEntries.async_reload",
|
||||
return_value=True,
|
||||
):
|
||||
result = await entry.start_reauth_flow(hass)
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "reauth_successful"
|
||||
mock_setup_entry.assert_called_once()
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_setup_entry")
|
||||
async def test_reauth_password(
|
||||
hass: HomeAssistant,
|
||||
mock_flow_sense: MagicMock,
|
||||
) -> None:
|
||||
async def test_reauth_password(hass: HomeAssistant, mock_sense) -> None:
|
||||
"""Test reauth form."""
|
||||
|
||||
# set up initially
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data=MOCK_CONFIG,
|
||||
unique_id="test-email",
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
mock_flow_sense.return_value.authenticate.side_effect = SenseAuthenticationException
|
||||
mock_sense.return_value.authenticate.side_effect = SenseAuthenticationException
|
||||
|
||||
# Reauth success without user input
|
||||
result = await entry.start_reauth_flow(hass)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
|
||||
mock_flow_sense.return_value.authenticate.side_effect = None
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{"password": "test-password"},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
mock_sense.return_value.authenticate.side_effect = None
|
||||
with patch(
|
||||
"homeassistant.components.sense.async_setup_entry",
|
||||
return_value=True,
|
||||
):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{"password": "test-password"},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "reauth_successful"
|
||||
assert result2["type"] is FlowResultType.ABORT
|
||||
assert result2["reason"] == "reauth_successful"
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
"""Tests for the Sense coordinators."""
|
||||
|
||||
from datetime import timedelta
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
import pytest
|
||||
from sense_energy import SenseAuthenticationException, SenseMFARequiredException
|
||||
|
||||
from homeassistant.components.sense.const import DOMAIN, TREND_UPDATE_RATE
|
||||
from homeassistant.config_entries import SOURCE_REAUTH
|
||||
from homeassistant.const import STATE_UNAVAILABLE, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from . import setup_platform
|
||||
from .const import MONITOR_ID
|
||||
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"exception",
|
||||
[
|
||||
SenseAuthenticationException("auth expired"),
|
||||
SenseMFARequiredException("auth expired"),
|
||||
],
|
||||
)
|
||||
async def test_trend_coordinator_auth_failure(
|
||||
hass: HomeAssistant,
|
||||
mock_sense: MagicMock,
|
||||
config_entry: MockConfigEntry,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
exception: Exception,
|
||||
) -> None:
|
||||
"""Test that auth errors from the trend coordinator start a reauth flow."""
|
||||
await setup_platform(hass, config_entry, Platform.SENSOR)
|
||||
|
||||
mock_sense.update_trend_data.side_effect = exception
|
||||
|
||||
freezer.tick(timedelta(seconds=TREND_UPDATE_RATE))
|
||||
async_fire_time_changed(hass, freezer())
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(f"sensor.sense_{MONITOR_ID}_daily_energy")
|
||||
assert state.state == STATE_UNAVAILABLE
|
||||
|
||||
flows = hass.config_entries.flow.async_progress()
|
||||
assert len(flows) == 1
|
||||
flow = flows[0]
|
||||
assert flow.get("step_id") == "reauth_validate"
|
||||
assert flow.get("handler") == DOMAIN
|
||||
assert flow["context"].get("source") == SOURCE_REAUTH
|
||||
assert flow["context"].get("entry_id") == config_entry.entry_id
|
||||
@@ -1,123 +0,0 @@
|
||||
"""Tests for the Sense integration setup."""
|
||||
|
||||
import socket
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
from sense_energy import (
|
||||
SenseAPIException,
|
||||
SenseAPITimeoutException,
|
||||
SenseAuthenticationException,
|
||||
SenseMFARequiredException,
|
||||
SenseWebsocketException,
|
||||
)
|
||||
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"exception",
|
||||
[
|
||||
SenseAPITimeoutException(),
|
||||
SenseWebsocketException(),
|
||||
],
|
||||
)
|
||||
async def test_setup_entry_exceptions(
|
||||
hass: HomeAssistant,
|
||||
mock_sense: MagicMock,
|
||||
config_entry: MockConfigEntry,
|
||||
exception: Exception,
|
||||
) -> None:
|
||||
"""Test we handle exceptions during async_setup_entry and can recover."""
|
||||
mock_sense.update_realtime.side_effect = exception
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
assert not await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert config_entry.state is ConfigEntryState.SETUP_RETRY
|
||||
|
||||
# Verify recovery: clear the error and reload the entry
|
||||
mock_sense.update_realtime.side_effect = None
|
||||
assert await hass.config_entries.async_reload(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"exception",
|
||||
[
|
||||
SenseAuthenticationException(),
|
||||
SenseMFARequiredException(),
|
||||
],
|
||||
)
|
||||
async def test_setup_get_monitor_data_auth_exceptions(
|
||||
hass: HomeAssistant,
|
||||
mock_sense: MagicMock,
|
||||
config_entry: MockConfigEntry,
|
||||
exception: Exception,
|
||||
) -> None:
|
||||
"""Test auth exceptions from get_monitor_data result in a failed entry."""
|
||||
mock_sense.get_monitor_data.side_effect = exception
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
assert not await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert config_entry.state is ConfigEntryState.SETUP_ERROR
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"exception",
|
||||
[
|
||||
SenseAPITimeoutException(),
|
||||
TimeoutError(),
|
||||
SenseAPIException("connect error"),
|
||||
socket.gaierror(),
|
||||
],
|
||||
)
|
||||
async def test_setup_get_monitor_data_retry_exceptions(
|
||||
hass: HomeAssistant,
|
||||
mock_sense: MagicMock,
|
||||
config_entry: MockConfigEntry,
|
||||
exception: Exception,
|
||||
) -> None:
|
||||
"""Test timeout and connect exceptions from get_monitor_data result in a retryable entry."""
|
||||
mock_sense.get_monitor_data.side_effect = exception
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
assert not await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert config_entry.state is ConfigEntryState.SETUP_RETRY
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"exception",
|
||||
[
|
||||
SenseAPITimeoutException(),
|
||||
TimeoutError(),
|
||||
SenseAPIException("connect error"),
|
||||
socket.gaierror(),
|
||||
SenseWebsocketException("ws error"),
|
||||
SenseAPIException(),
|
||||
],
|
||||
)
|
||||
async def test_setup_get_realtime_retry_exceptions(
|
||||
hass: HomeAssistant,
|
||||
mock_sense: MagicMock,
|
||||
config_entry: MockConfigEntry,
|
||||
exception: Exception,
|
||||
) -> None:
|
||||
"""Test timeout and connect exceptions from update_realtime result in a retryable entry."""
|
||||
mock_sense.update_realtime.side_effect = exception
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
assert not await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert config_entry.state is ConfigEntryState.SETUP_RETRY
|
||||
@@ -1,22 +1,16 @@
|
||||
"""The tests for Sense sensor platform."""
|
||||
|
||||
from datetime import timedelta
|
||||
import socket
|
||||
from unittest.mock import MagicMock, PropertyMock
|
||||
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
import pytest
|
||||
from sense_energy import (
|
||||
Scale,
|
||||
SenseAPIException,
|
||||
SenseAPITimeoutException,
|
||||
SenseWebsocketException,
|
||||
)
|
||||
from sense_energy import Scale
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.sense.const import ACTIVE_UPDATE_RATE, TREND_UPDATE_RATE
|
||||
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
|
||||
from homeassistant.const import STATE_UNAVAILABLE, Platform
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.util.dt import utcnow
|
||||
@@ -238,88 +232,3 @@ async def test_trend_energy_sensors(
|
||||
|
||||
state = hass.states.get(f"sensor.sense_{MONITOR_ID}_daily_net_production")
|
||||
assert state.state == "5000"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"exception",
|
||||
[
|
||||
SenseAPIException("api error"),
|
||||
SenseAPITimeoutException("timeout"),
|
||||
TimeoutError("timeout"),
|
||||
socket.gaierror("addr info error"),
|
||||
],
|
||||
)
|
||||
async def test_trend_coordinator_update_failure(
|
||||
hass: HomeAssistant,
|
||||
mock_sense: MagicMock,
|
||||
config_entry: MockConfigEntry,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
exception: Exception,
|
||||
) -> None:
|
||||
"""Test that connection errors from the trend coordinator mark entities unavailable."""
|
||||
await setup_platform(hass, config_entry, Platform.SENSOR)
|
||||
|
||||
state = hass.states.get(f"sensor.sense_{MONITOR_ID}_daily_energy")
|
||||
assert state is not None
|
||||
assert state.state != STATE_UNAVAILABLE
|
||||
|
||||
mock_sense.update_trend_data.side_effect = exception
|
||||
|
||||
freezer.tick(timedelta(seconds=TREND_UPDATE_RATE))
|
||||
async_fire_time_changed(hass, freezer())
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(f"sensor.sense_{MONITOR_ID}_daily_energy")
|
||||
assert state.state == STATE_UNAVAILABLE
|
||||
|
||||
mock_sense.update_trend_data.side_effect = None
|
||||
|
||||
freezer.tick(timedelta(seconds=TREND_UPDATE_RATE))
|
||||
async_fire_time_changed(hass, freezer())
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(f"sensor.sense_{MONITOR_ID}_daily_energy")
|
||||
assert state.state != STATE_UNAVAILABLE
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"exception",
|
||||
[
|
||||
SenseAPIException("api error"),
|
||||
SenseAPITimeoutException("timeout"),
|
||||
TimeoutError("timeout"),
|
||||
SenseWebsocketException("ws error"),
|
||||
socket.gaierror("addr info error"),
|
||||
],
|
||||
)
|
||||
async def test_realtime_coordinator_update_failure(
|
||||
hass: HomeAssistant,
|
||||
mock_sense: MagicMock,
|
||||
config_entry: MockConfigEntry,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
exception: Exception,
|
||||
) -> None:
|
||||
"""Test that errors from the realtime coordinator mark entities unavailable."""
|
||||
await setup_platform(hass, config_entry, Platform.SENSOR)
|
||||
|
||||
state = hass.states.get(f"sensor.sense_{MONITOR_ID}_energy")
|
||||
assert state is not None
|
||||
assert state.state != STATE_UNAVAILABLE
|
||||
|
||||
mock_sense.update_realtime.side_effect = exception
|
||||
|
||||
freezer.tick(timedelta(seconds=ACTIVE_UPDATE_RATE))
|
||||
async_fire_time_changed(hass, freezer())
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(f"sensor.sense_{MONITOR_ID}_energy")
|
||||
assert state.state == STATE_UNAVAILABLE
|
||||
|
||||
mock_sense.update_realtime.side_effect = None
|
||||
|
||||
freezer.tick(timedelta(seconds=ACTIVE_UPDATE_RATE))
|
||||
async_fire_time_changed(hass, freezer())
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(f"sensor.sense_{MONITOR_ID}_energy")
|
||||
assert state.state != STATE_UNAVAILABLE
|
||||
|
||||
@@ -39,7 +39,6 @@ DEVICE_FIXTURES = [
|
||||
"copper_water_meter_v03",
|
||||
"base_electric_meter",
|
||||
"smart_plug",
|
||||
"fibaro_dimmer_2",
|
||||
"vd_stv_2017_k",
|
||||
"c2c_arlo_pro_3_switch",
|
||||
"yale_push_button_deadbolt_lock",
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
{
|
||||
"components": {
|
||||
"button2": {
|
||||
"button": {
|
||||
"button": {
|
||||
"value": null
|
||||
},
|
||||
"numberOfButtons": {
|
||||
"value": null
|
||||
},
|
||||
"supportedButtonValues": {
|
||||
"value": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"main": {
|
||||
"powerMeter": {
|
||||
"power": {
|
||||
"value": 0.0,
|
||||
"unit": "W",
|
||||
"timestamp": "2026-05-28T12:21:31.639Z"
|
||||
}
|
||||
},
|
||||
"energyMeter": {
|
||||
"energy": {
|
||||
"value": 36.89,
|
||||
"unit": "kWh",
|
||||
"timestamp": "2026-05-28T12:32:57.766Z"
|
||||
}
|
||||
},
|
||||
"switchLevel": {
|
||||
"levelRange": {
|
||||
"value": null
|
||||
},
|
||||
"level": {
|
||||
"value": 0,
|
||||
"unit": "%",
|
||||
"timestamp": "2026-05-28T10:20:37.674Z"
|
||||
}
|
||||
},
|
||||
"legendabsolute60149.forcedOnLevel": {
|
||||
"forcedOnLevel": {
|
||||
"value": 5,
|
||||
"unit": "%",
|
||||
"timestamp": "2026-05-21T15:10:35.371Z"
|
||||
}
|
||||
},
|
||||
"refresh": {},
|
||||
"switch": {
|
||||
"switch": {
|
||||
"value": "off",
|
||||
"timestamp": "2026-05-28T10:20:37.616Z"
|
||||
}
|
||||
}
|
||||
},
|
||||
"button1": {
|
||||
"button": {
|
||||
"button": {
|
||||
"value": null
|
||||
},
|
||||
"numberOfButtons": {
|
||||
"value": null
|
||||
},
|
||||
"supportedButtonValues": {
|
||||
"value": null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"deviceId": "df9f5405-f930-46aa-9693-14c570b35c83",
|
||||
"name": "fibaro-dimmer-2",
|
||||
"label": "Dimmer entr\u00e9 1 1",
|
||||
"manufacturerName": "SmartThingsCommunity",
|
||||
"presentationId": "b1b79065-1923-3e7a-b016-9abcc3d1d416",
|
||||
"deviceManufacturerCode": "010F-0102-1001",
|
||||
"locationId": "c85a9f8a-5d2e-4cdd-8bdb-bc49ba4a3544",
|
||||
"ownerId": "7b68139b-d068-45d8-bf27-961320350024",
|
||||
"roomId": "f4084bf6-2985-47cc-b3c9-3907d098ec0c",
|
||||
"components": [
|
||||
{
|
||||
"id": "main",
|
||||
"label": "main",
|
||||
"capabilities": [
|
||||
{
|
||||
"id": "switch",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "switchLevel",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "powerMeter",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "energyMeter",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "legendabsolute60149.forcedOnLevel",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "refresh",
|
||||
"version": 1
|
||||
}
|
||||
],
|
||||
"categories": [
|
||||
{
|
||||
"name": "Switch",
|
||||
"categoryType": "manufacturer"
|
||||
}
|
||||
],
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"id": "button1",
|
||||
"label": "button1",
|
||||
"capabilities": [
|
||||
{
|
||||
"id": "button",
|
||||
"version": 1
|
||||
}
|
||||
],
|
||||
"categories": [
|
||||
{
|
||||
"name": "RemoteController",
|
||||
"categoryType": "manufacturer"
|
||||
}
|
||||
],
|
||||
"optional": false
|
||||
},
|
||||
{
|
||||
"id": "button2",
|
||||
"label": "button2",
|
||||
"capabilities": [
|
||||
{
|
||||
"id": "button",
|
||||
"version": 1
|
||||
}
|
||||
],
|
||||
"categories": [
|
||||
{
|
||||
"name": "RemoteController",
|
||||
"categoryType": "manufacturer"
|
||||
}
|
||||
],
|
||||
"optional": false
|
||||
}
|
||||
],
|
||||
"createTime": "2023-04-23T06:35:16.202Z",
|
||||
"parentDeviceId": "4869d882-e898-40c3-a198-7611b72187a5",
|
||||
"profile": {
|
||||
"id": "c6edf569-c7bb-38e7-a442-fe397ebe0cae"
|
||||
},
|
||||
"zwave": {
|
||||
"networkId": "0A",
|
||||
"driverId": "17c05c19-f008-42e2-aa12-f12f1aae5612",
|
||||
"executingLocally": true,
|
||||
"hubId": "4869d882-e898-40c3-a198-7611b72187a5",
|
||||
"networkSecurityLevel": "ZWAVE_S0_LEGACY",
|
||||
"provisioningState": "NONFUNCTIONAL",
|
||||
"manufacturerId": 271,
|
||||
"productType": 258,
|
||||
"productId": 4097
|
||||
},
|
||||
"type": "ZWAVE",
|
||||
"restrictionTier": 0,
|
||||
"allowed": null,
|
||||
"executionContext": "LOCAL",
|
||||
"relationships": []
|
||||
}
|
||||
],
|
||||
"_links": {}
|
||||
}
|
||||
@@ -1,114 +1,4 @@
|
||||
# serializer version: 1
|
||||
# name: test_all_entities[fibaro_dimmer_2][event.dimmer_entre_1_1_button1-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'event_types': None,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'event',
|
||||
'entity_category': None,
|
||||
'entity_id': 'event.dimmer_entre_1_1_button1',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'button1',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <EventDeviceClass.BUTTON: 'button'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'button1',
|
||||
'platform': 'smartthings',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'button',
|
||||
'unique_id': 'df9f5405-f930-46aa-9693-14c570b35c83_button1_button',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[fibaro_dimmer_2][event.dimmer_entre_1_1_button1-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'button',
|
||||
'event_type': None,
|
||||
'event_types': None,
|
||||
'friendly_name': 'Dimmer entré 1 1 button1',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'event.dimmer_entre_1_1_button1',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'unknown',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[fibaro_dimmer_2][event.dimmer_entre_1_1_button2-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'event_types': None,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'event',
|
||||
'entity_category': None,
|
||||
'entity_id': 'event.dimmer_entre_1_1_button2',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'button2',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <EventDeviceClass.BUTTON: 'button'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'button2',
|
||||
'platform': 'smartthings',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'button',
|
||||
'unique_id': 'df9f5405-f930-46aa-9693-14c570b35c83_button2_button',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[fibaro_dimmer_2][event.dimmer_entre_1_1_button2-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'button',
|
||||
'event_type': None,
|
||||
'event_types': None,
|
||||
'friendly_name': 'Dimmer entré 1 1 button2',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'event.dimmer_entre_1_1_button2',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'unknown',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[heatit_zpushwall][event.livingroom_smart_switch_button1-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
|
||||
@@ -1828,37 +1828,6 @@
|
||||
'via_device_id': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_devices[fibaro_dimmer_2]
|
||||
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',
|
||||
'df9f5405-f930-46aa-9693-14c570b35c83',
|
||||
),
|
||||
}),
|
||||
'labels': set({
|
||||
}),
|
||||
'manufacturer': None,
|
||||
'model': None,
|
||||
'model_id': None,
|
||||
'name': 'Dimmer entré 1 1',
|
||||
'name_by_user': None,
|
||||
'primary_config_entry': <ANY>,
|
||||
'serial_number': None,
|
||||
'sw_version': None,
|
||||
'via_device_id': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_devices[gas_detector]
|
||||
DeviceRegistryEntrySnapshot({
|
||||
'area_id': None,
|
||||
|
||||
@@ -542,66 +542,6 @@
|
||||
'state': 'on',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[fibaro_dimmer_2][light.dimmer_entre_1_1-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'supported_color_modes': list([
|
||||
<ColorMode.BRIGHTNESS: 'brightness'>,
|
||||
]),
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'light',
|
||||
'entity_category': None,
|
||||
'entity_id': 'light.dimmer_entre_1_1',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': None,
|
||||
'platform': 'smartthings',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': <LightEntityFeature: 32>,
|
||||
'translation_key': None,
|
||||
'unique_id': 'df9f5405-f930-46aa-9693-14c570b35c83_main',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[fibaro_dimmer_2][light.dimmer_entre_1_1-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'brightness': None,
|
||||
'color_mode': None,
|
||||
'friendly_name': 'Dimmer entré 1 1',
|
||||
'supported_color_modes': list([
|
||||
<ColorMode.BRIGHTNESS: 'brightness'>,
|
||||
]),
|
||||
'supported_features': <LightEntityFeature: 32>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'light.dimmer_entre_1_1',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'off',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[ge_in_wall_smart_dimmer][light.theater_basement_exit_light-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
|
||||
@@ -19725,122 +19725,6 @@
|
||||
'state': 'unknown',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[fibaro_dimmer_2][sensor.dimmer_entre_1_1_energy-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
|
||||
}),
|
||||
'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.dimmer_entre_1_1_energy',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Energy',
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 2,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.ENERGY: 'energy'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Energy',
|
||||
'platform': 'smartthings',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': None,
|
||||
'unique_id': 'df9f5405-f930-46aa-9693-14c570b35c83_main_energyMeter_energy_energy',
|
||||
'unit_of_measurement': 'kWh',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[fibaro_dimmer_2][sensor.dimmer_entre_1_1_energy-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'energy',
|
||||
'friendly_name': 'Dimmer entré 1 1 Energy',
|
||||
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
|
||||
'unit_of_measurement': 'kWh',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.dimmer_entre_1_1_energy',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '36.89',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[fibaro_dimmer_2][sensor.dimmer_entre_1_1_power-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'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.dimmer_entre_1_1_power',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Power',
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 0,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.POWER: 'power'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Power',
|
||||
'platform': 'smartthings',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': None,
|
||||
'unique_id': 'df9f5405-f930-46aa-9693-14c570b35c83_main_powerMeter_power_power',
|
||||
'unit_of_measurement': 'W',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[fibaro_dimmer_2][sensor.dimmer_entre_1_1_power-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'power',
|
||||
'friendly_name': 'Dimmer entré 1 1 Power',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': 'W',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.dimmer_entre_1_1_power',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '0.0',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[gas_detector][sensor.gas_detector_link_quality-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
|
||||
@@ -1378,7 +1378,6 @@ async def test_async_setup_entry_failed(
|
||||
mock_bot.side_effect = InvalidToken("mock invalid token error")
|
||||
|
||||
with pytest.raises(ConfigEntryAuthFailed) as err:
|
||||
# pylint: disable-next=home-assistant-tests-direct-async-setup-entry
|
||||
await async_setup_entry(hass, mock_broadcast_config_entry)
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
@@ -229,7 +229,6 @@ async def test_thermopro_restores_entities_on_restart_behavior(
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Manually set up sensor platform with our callback
|
||||
# pylint: disable-next=home-assistant-tests-direct-platform-async-setup-entry
|
||||
await thermopro_sensor.async_setup_entry(hass, entry1, add_entities_first)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
@@ -243,7 +242,6 @@ async def test_thermopro_restores_entities_on_restart_behavior(
|
||||
assert await hass.config_entries.async_setup(entry2.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# pylint: disable-next=home-assistant-tests-direct-platform-async-setup-entry
|
||||
await thermopro_sensor.async_setup_entry(hass, entry2, add_entities_second)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
||||
@@ -96,5 +96,4 @@ async def test_setup_requires_data_api_reauth(hass: HomeAssistant) -> None:
|
||||
)
|
||||
|
||||
with pytest.raises(ConfigEntryAuthFailed):
|
||||
# pylint: disable-next=home-assistant-tests-direct-async-setup-entry
|
||||
await async_setup_entry(hass, entry)
|
||||
|
||||
@@ -54,7 +54,6 @@ async def test_async_setup_entry__no_devices(
|
||||
) -> None:
|
||||
"""Test setup connects to vesync and creates empty config when no devices."""
|
||||
with patch.object(hass.config_entries, "async_forward_entry_setups") as setups_mock:
|
||||
# pylint: disable-next=home-assistant-tests-direct-async-setup-entry
|
||||
assert await async_setup_entry(hass, config_entry)
|
||||
# Assert platforms loaded
|
||||
await hass.async_block_till_done()
|
||||
@@ -82,7 +81,6 @@ async def test_async_setup_entry__loads_fans(
|
||||
manager._dev_list["fans"].append(fan)
|
||||
|
||||
with patch.object(hass.config_entries, "async_forward_entry_setups") as setups_mock:
|
||||
# pylint: disable-next=home-assistant-tests-direct-async-setup-entry
|
||||
assert await async_setup_entry(hass, config_entry)
|
||||
# Assert platforms loaded
|
||||
await hass.async_block_till_done()
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
"""Test Volvo locks."""
|
||||
|
||||
from collections.abc import Awaitable, Callable
|
||||
from datetime import timedelta
|
||||
from unittest.mock import patch
|
||||
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
from volvocarsapi.api import VolvoCarsApi
|
||||
@@ -14,7 +16,8 @@ from homeassistant.components.lock import (
|
||||
SERVICE_UNLOCK,
|
||||
LockState,
|
||||
)
|
||||
from homeassistant.const import ATTR_ENTITY_ID, Platform
|
||||
from homeassistant.components.volvo.coordinator import FAST_INTERVAL
|
||||
from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
@@ -22,7 +25,7 @@ from homeassistant.helpers import entity_registry as er
|
||||
from . import configure_mock
|
||||
from .const import DEFAULT_VIN
|
||||
|
||||
from tests.common import MockConfigEntry, snapshot_platform
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_api", "full_model")
|
||||
@@ -134,3 +137,28 @@ async def test_unlock_failure(
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert hass.states.get(entity_id).state == LockState.LOCKED
|
||||
|
||||
|
||||
@pytest.mark.freeze_time("2025-05-31T10:00:00+00:00")
|
||||
@pytest.mark.usefixtures("full_model")
|
||||
async def test_lock_unavailable_when_api_field_missing(
|
||||
hass: HomeAssistant,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
setup_integration: Callable[[], Awaitable[bool]],
|
||||
mock_api: VolvoCarsApi,
|
||||
) -> None:
|
||||
"""Test lock becomes unavailable when centralLock is missing from API response."""
|
||||
|
||||
with patch("homeassistant.components.volvo.PLATFORMS", [Platform.LOCK]):
|
||||
assert await setup_integration()
|
||||
|
||||
entity_id = "lock.volvo_xc40_lock"
|
||||
assert hass.states.get(entity_id).state == LockState.LOCKED
|
||||
|
||||
# Simulate API returning doors data without centralLock
|
||||
configure_mock(mock_api.async_get_doors_status, return_value={})
|
||||
freezer.tick(timedelta(minutes=FAST_INTERVAL))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
assert hass.states.get(entity_id).state == STATE_UNAVAILABLE
|
||||
|
||||
@@ -31,8 +31,6 @@ from homeassistant.util.dt import utc_from_timestamp, utcnow
|
||||
from tests.common import (
|
||||
ANY,
|
||||
MockConfigEntry,
|
||||
MockEntity,
|
||||
MockEntityPlatform,
|
||||
RegistryEntryWithDefaults,
|
||||
async_capture_events,
|
||||
async_fire_time_changed,
|
||||
@@ -3653,43 +3651,21 @@ async def test_unique_id_non_string(
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("create_kwargs", "migrate_kwargs", "new_subentry_id", "match"),
|
||||
("create_kwargs", "migrate_kwargs", "new_subentry_id"),
|
||||
[
|
||||
(
|
||||
{},
|
||||
{},
|
||||
None,
|
||||
"Unique id '1234' is already in use by 'light.light'",
|
||||
),
|
||||
(
|
||||
{"config_subentry_id": None},
|
||||
{},
|
||||
None,
|
||||
"Unique id '1234' is already in use by 'light.light'",
|
||||
),
|
||||
(
|
||||
{},
|
||||
{"new_config_subentry_id": None},
|
||||
None,
|
||||
"Unique id '1234' is already in use by 'light.light'",
|
||||
),
|
||||
(
|
||||
{},
|
||||
{"new_config_subentry_id": "mock-subentry-id-2"},
|
||||
"mock-subentry-id-2",
|
||||
"Can't change config entry without changing subentry",
|
||||
),
|
||||
({}, {}, None),
|
||||
({"config_subentry_id": None}, {}, None),
|
||||
({}, {"new_config_subentry_id": None}, None),
|
||||
({}, {"new_config_subentry_id": "mock-subentry-id-2"}, "mock-subentry-id-2"),
|
||||
(
|
||||
{"config_subentry_id": "mock-subentry-id-1"},
|
||||
{"new_config_subentry_id": None},
|
||||
None,
|
||||
"Unique id '1234' is already in use by 'light.light'",
|
||||
),
|
||||
(
|
||||
{"config_subentry_id": "mock-subentry-id-1"},
|
||||
{"new_config_subentry_id": "mock-subentry-id-2"},
|
||||
"mock-subentry-id-2",
|
||||
"Can't change config entry without changing subentry",
|
||||
),
|
||||
],
|
||||
)
|
||||
@@ -3699,7 +3675,6 @@ def test_migrate_entity_to_new_platform(
|
||||
create_kwargs: dict,
|
||||
migrate_kwargs: dict,
|
||||
new_subentry_id: str | None,
|
||||
match: str,
|
||||
) -> None:
|
||||
"""Test migrate_entity_to_new_platform."""
|
||||
orig_config_entry = MockConfigEntry(
|
||||
@@ -3772,7 +3747,7 @@ def test_migrate_entity_to_new_platform(
|
||||
assert new_entry.platform == "hue2"
|
||||
|
||||
# Test nonexisting entity
|
||||
with pytest.raises(KeyError, match="'light.not_a_real_light'"):
|
||||
with pytest.raises(KeyError):
|
||||
entity_registry.async_update_entity_platform(
|
||||
"light.not_a_real_light",
|
||||
"hue2",
|
||||
@@ -3781,16 +3756,15 @@ def test_migrate_entity_to_new_platform(
|
||||
)
|
||||
|
||||
# Test migrate entity without new config entry ID
|
||||
with pytest.raises(
|
||||
ValueError,
|
||||
match="new_config_entry_id required because light.light is already linked to a config entry",
|
||||
):
|
||||
with pytest.raises(ValueError):
|
||||
entity_registry.async_update_entity_platform(
|
||||
"light.light",
|
||||
"hue3",
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match=match):
|
||||
# Test entity with a state
|
||||
hass.states.async_set("light.light", "on")
|
||||
with pytest.raises(ValueError):
|
||||
entity_registry.async_update_entity_platform(
|
||||
"light.light",
|
||||
"hue2",
|
||||
@@ -3799,14 +3773,13 @@ def test_migrate_entity_to_new_platform(
|
||||
)
|
||||
|
||||
|
||||
async def test_migrate_entity_to_new_platform_error_handling(
|
||||
def test_migrate_entity_to_new_platform_error_handling(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test migrate_entity_to_new_platform."""
|
||||
platform = MockEntityPlatform(hass, domain="light", platform_name="hue")
|
||||
orig_config_entry = MockConfigEntry(
|
||||
domain="hue",
|
||||
domain="light",
|
||||
subentries_data=[
|
||||
config_entries.ConfigSubentryData(
|
||||
data={},
|
||||
@@ -3818,14 +3791,25 @@ async def test_migrate_entity_to_new_platform_error_handling(
|
||||
],
|
||||
)
|
||||
orig_config_entry.add_to_hass(hass)
|
||||
platform.config_entry = orig_config_entry
|
||||
entity = MockEntity(name="Light entity", entity_id="light.light", unique_id="5678")
|
||||
await platform.async_add_entities([entity], config_subentry_id="mock-subentry-id-1")
|
||||
orig_unique_id = "5678"
|
||||
|
||||
assert entity_registry.async_get("light.light") is not None
|
||||
orig_entry = entity_registry.async_get_or_create(
|
||||
"light",
|
||||
"hue",
|
||||
orig_unique_id,
|
||||
suggested_object_id="light",
|
||||
config_entry=orig_config_entry,
|
||||
config_subentry_id="mock-subentry-id-1",
|
||||
disabled_by=er.RegistryEntryDisabler.USER,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
original_device_class="mock-device-class",
|
||||
original_icon="initial-original_icon",
|
||||
original_name="initial-original_name",
|
||||
)
|
||||
assert entity_registry.async_get("light.light") is orig_entry
|
||||
|
||||
new_config_entry = MockConfigEntry(
|
||||
domain="hue2",
|
||||
domain="light",
|
||||
subentries_data=[
|
||||
config_entries.ConfigSubentryData(
|
||||
data={},
|
||||
@@ -3848,18 +3832,6 @@ async def test_migrate_entity_to_new_platform_error_handling(
|
||||
new_config_entry_id=new_config_entry.entry_id,
|
||||
)
|
||||
|
||||
with pytest.raises(
|
||||
ValueError, match="Only entities that haven't been loaded can be migrated"
|
||||
):
|
||||
entity_registry.async_update_entity_platform(
|
||||
"light.light",
|
||||
"hue2",
|
||||
new_unique_id=new_unique_id,
|
||||
new_config_entry_id=new_config_entry.entry_id,
|
||||
)
|
||||
|
||||
await platform.async_reset()
|
||||
|
||||
# Test migrate entity without new config entry ID
|
||||
with pytest.raises(
|
||||
ValueError,
|
||||
@@ -3871,17 +3843,29 @@ async def test_migrate_entity_to_new_platform_error_handling(
|
||||
):
|
||||
entity_registry.async_update_entity_platform(
|
||||
"light.light",
|
||||
"hue2",
|
||||
"hue3",
|
||||
)
|
||||
|
||||
# Test migrate entity without new config subentry ID
|
||||
with pytest.raises(
|
||||
ValueError,
|
||||
match="Can't change config entry without changing subentry",
|
||||
):
|
||||
entity_registry.async_update_entity_platform(
|
||||
"light.light",
|
||||
"hue3",
|
||||
new_config_entry_id=new_config_entry.entry_id,
|
||||
)
|
||||
|
||||
# Test entity with a state
|
||||
hass.states.async_set("light.light", "on")
|
||||
with pytest.raises(
|
||||
ValueError, match="Only entities that haven't been loaded can be migrated"
|
||||
):
|
||||
entity_registry.async_update_entity_platform(
|
||||
"light.light",
|
||||
"hue2",
|
||||
new_unique_id=new_unique_id,
|
||||
new_config_entry_id=new_config_entry.entry_id,
|
||||
)
|
||||
|
||||
|
||||
@@ -1,170 +0,0 @@
|
||||
"""Tests for the direct async_setup_entry checker."""
|
||||
|
||||
import astroid
|
||||
from pylint.testutils import UnittestLinter
|
||||
from pylint.utils.ast_walker import ASTWalker
|
||||
from pylint_home_assistant.checkers.tests.direct_async_setup_entry import (
|
||||
DirectAsyncSetupEntry,
|
||||
)
|
||||
import pytest
|
||||
|
||||
from tests.pylint import assert_no_messages
|
||||
|
||||
# Pre-load so astroid can resolve ``async_setup_entry`` in parsed snippets.
|
||||
astroid.MANAGER.ast_from_module_name("homeassistant.components.sun")
|
||||
astroid.MANAGER.ast_from_module_name("homeassistant.components.sun.sensor")
|
||||
|
||||
|
||||
@pytest.fixture(name="checker")
|
||||
def checker_fixture(linter: UnittestLinter) -> DirectAsyncSetupEntry:
|
||||
"""Fixture to provide a direct async_setup_entry checker."""
|
||||
return DirectAsyncSetupEntry(linter)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("code", "module_name"),
|
||||
[
|
||||
pytest.param(
|
||||
"""
|
||||
async def test_setup(hass):
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
""",
|
||||
"tests.components.sun.test_init",
|
||||
id="proper_setup_call",
|
||||
),
|
||||
pytest.param(
|
||||
"""
|
||||
from homeassistant.components.sun import async_setup_entry
|
||||
|
||||
async def test_setup(hass, mock_config_entry):
|
||||
await async_setup_entry(hass, mock_config_entry)
|
||||
""",
|
||||
"homeassistant.components.sun",
|
||||
id="not_a_test_module",
|
||||
),
|
||||
pytest.param(
|
||||
"""
|
||||
async def test_setup(hass, mock_config_entry):
|
||||
await some_local.async_setup_entry(hass, mock_config_entry)
|
||||
""",
|
||||
"tests.components.sun.test_init",
|
||||
id="unresolved_attribute_call",
|
||||
),
|
||||
pytest.param(
|
||||
"""
|
||||
async def async_setup_entry(hass, entry):
|
||||
return True
|
||||
|
||||
async def test_setup(hass, entry):
|
||||
await async_setup_entry(hass, entry)
|
||||
""",
|
||||
"tests.components.sun.test_init",
|
||||
id="local_async_setup_entry_not_an_integration",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_no_warning(
|
||||
linter: UnittestLinter,
|
||||
checker: DirectAsyncSetupEntry,
|
||||
code: str,
|
||||
module_name: str,
|
||||
) -> None:
|
||||
"""Test cases that should not trigger a warning."""
|
||||
root_node = astroid.parse(code, module_name)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(checker)
|
||||
|
||||
with assert_no_messages(linter):
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("code", "module_name", "expected_msg"),
|
||||
[
|
||||
pytest.param(
|
||||
"""
|
||||
from homeassistant.components.sun import async_setup_entry
|
||||
|
||||
async def test_setup(hass, mock_config_entry):
|
||||
await async_setup_entry(hass, mock_config_entry)
|
||||
""",
|
||||
"tests.components.sun.test_init",
|
||||
"home-assistant-tests-direct-async-setup-entry",
|
||||
id="direct_name_call_from_init",
|
||||
),
|
||||
pytest.param(
|
||||
"""
|
||||
from homeassistant.components import sun
|
||||
|
||||
async def test_setup(hass, mock_config_entry):
|
||||
await sun.async_setup_entry(hass, mock_config_entry)
|
||||
""",
|
||||
"tests.components.sun.test_init",
|
||||
"home-assistant-tests-direct-async-setup-entry",
|
||||
id="attribute_call_from_init",
|
||||
),
|
||||
pytest.param(
|
||||
"""
|
||||
from homeassistant.components.sun.sensor import async_setup_entry
|
||||
|
||||
async def test_setup(hass, mock_config_entry, add_entities):
|
||||
await async_setup_entry(hass, mock_config_entry, add_entities)
|
||||
""",
|
||||
"tests.components.sun.test_sensor",
|
||||
"home-assistant-tests-direct-platform-async-setup-entry",
|
||||
id="direct_call_from_platform",
|
||||
),
|
||||
pytest.param(
|
||||
"""
|
||||
from homeassistant.components.sun import sensor
|
||||
|
||||
async def test_setup(hass, mock_config_entry, add_entities):
|
||||
await sensor.async_setup_entry(hass, mock_config_entry, add_entities)
|
||||
""",
|
||||
"tests.components.sun.test_sensor",
|
||||
"home-assistant-tests-direct-platform-async-setup-entry",
|
||||
id="attribute_call_from_platform",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_warning(
|
||||
linter: UnittestLinter,
|
||||
checker: DirectAsyncSetupEntry,
|
||||
code: str,
|
||||
module_name: str,
|
||||
expected_msg: str,
|
||||
) -> None:
|
||||
"""Test cases that should trigger a warning."""
|
||||
root_node = astroid.parse(code, module_name)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(checker)
|
||||
walker.walk(root_node)
|
||||
|
||||
messages = linter.release_messages()
|
||||
assert len(messages) == 1
|
||||
assert messages[0].msg_id == expected_msg
|
||||
|
||||
|
||||
def test_multiple_calls_each_flagged(
|
||||
linter: UnittestLinter,
|
||||
checker: DirectAsyncSetupEntry,
|
||||
) -> None:
|
||||
"""Test that multiple direct calls are each flagged."""
|
||||
root_node = astroid.parse(
|
||||
"""
|
||||
from homeassistant.components.sun import async_setup_entry
|
||||
|
||||
async def test_a(hass, mock_config_entry):
|
||||
await async_setup_entry(hass, mock_config_entry)
|
||||
|
||||
async def test_b(hass, mock_config_entry):
|
||||
await async_setup_entry(hass, mock_config_entry)
|
||||
""",
|
||||
"tests.components.sun.test_init",
|
||||
)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(checker)
|
||||
walker.walk(root_node)
|
||||
|
||||
messages = linter.release_messages()
|
||||
assert len(messages) == 2
|
||||
Reference in New Issue
Block a user