mirror of
https://github.com/home-assistant/core.git
synced 2026-05-29 23:46:24 +00:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 582761a6ec | |||
| 6bfd39f094 | |||
| 002ca9611d | |||
| 46ee3d2b26 | |||
| eb901bcf3a | |||
| 930b4a2c81 | |||
| 22d1b8e1cd | |||
| a8bdf80044 | |||
| 14b16e298a | |||
| 75cf02cad8 | |||
| b2a737cf56 | |||
| 98601da3ea |
@@ -40,7 +40,7 @@ env:
|
||||
CACHE_VERSION: 12
|
||||
UV_CACHE_VERSION: 1
|
||||
MYPY_CACHE_VERSION: 9
|
||||
HA_SHORT_VERSION: "2025.4"
|
||||
HA_SHORT_VERSION: "2025.5"
|
||||
DEFAULT_PYTHON: "3.13"
|
||||
ALL_PYTHON_VERSIONS: "['3.13']"
|
||||
# 10.3 is the oldest supported version
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/ecovacs",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["sleekxmppfs", "sucks", "deebot_client"],
|
||||
"requirements": ["py-sucks==0.9.10", "deebot-client==12.3.1"]
|
||||
"requirements": ["py-sucks==0.9.10", "deebot-client==12.4.0"]
|
||||
}
|
||||
|
||||
@@ -284,23 +284,32 @@ def _is_websocket(request: web.Request) -> bool:
|
||||
)
|
||||
|
||||
|
||||
_CLOSE_TYPES = {
|
||||
aiohttp.WSMsgType.CLOSE,
|
||||
aiohttp.WSMsgType.CLOSING,
|
||||
aiohttp.WSMsgType.CLOSED,
|
||||
}
|
||||
|
||||
|
||||
async def _websocket_forward(
|
||||
ws_from: web.WebSocketResponse | ClientWebSocketResponse,
|
||||
ws_to: web.WebSocketResponse | ClientWebSocketResponse,
|
||||
) -> None:
|
||||
"""Handle websocket message directly."""
|
||||
try:
|
||||
async for msg in ws_from:
|
||||
if msg.type is aiohttp.WSMsgType.TEXT:
|
||||
while msg := await ws_from.receive():
|
||||
msg_type = msg.type
|
||||
if msg_type is aiohttp.WSMsgType.TEXT:
|
||||
await ws_to.send_str(msg.data)
|
||||
elif msg.type is aiohttp.WSMsgType.BINARY:
|
||||
elif msg_type is aiohttp.WSMsgType.BINARY:
|
||||
await ws_to.send_bytes(msg.data)
|
||||
elif msg.type is aiohttp.WSMsgType.PING:
|
||||
elif msg_type is aiohttp.WSMsgType.PING:
|
||||
await ws_to.ping()
|
||||
elif msg.type is aiohttp.WSMsgType.PONG:
|
||||
elif msg_type is aiohttp.WSMsgType.PONG:
|
||||
await ws_to.pong()
|
||||
elif ws_to.closed:
|
||||
elif msg_type in _CLOSE_TYPES:
|
||||
await ws_to.close(code=ws_to.close_code, message=msg.extra) # type: ignore[arg-type]
|
||||
break
|
||||
except RuntimeError:
|
||||
_LOGGER.debug("Ingress Websocket runtime error")
|
||||
except ConnectionResetError:
|
||||
|
||||
@@ -387,6 +387,15 @@ class HeosMediaPlayer(CoordinatorEntity[HeosCoordinator], MediaPlayerEntity):
|
||||
await self._player.play_preset_station(index)
|
||||
return
|
||||
|
||||
if media_type == "queue":
|
||||
# media_id must be an int
|
||||
try:
|
||||
queue_id = int(media_id)
|
||||
except ValueError:
|
||||
raise ValueError(f"Invalid queue id '{media_id}'") from None
|
||||
await self._player.play_queue(queue_id)
|
||||
return
|
||||
|
||||
raise ValueError(f"Unsupported media type '{media_type}'")
|
||||
|
||||
@catch_action_error("select source")
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
"name": "Supports AirPlay"
|
||||
},
|
||||
"supports_ethernet": {
|
||||
"name": "Supports ethernet"
|
||||
"name": "Supports Ethernet"
|
||||
},
|
||||
"supports_find_remote": {
|
||||
"name": "Supports find remote"
|
||||
|
||||
@@ -410,7 +410,9 @@ def create_devices(
|
||||
rooms: dict[str, str],
|
||||
) -> None:
|
||||
"""Create devices in the device registry."""
|
||||
for device in devices.values():
|
||||
for device in sorted(
|
||||
devices.values(), key=lambda d: d.device.parent_device_id or ""
|
||||
):
|
||||
kwargs: dict[str, Any] = {}
|
||||
if device.device.hub is not None:
|
||||
kwargs = {
|
||||
|
||||
@@ -24,7 +24,7 @@ if TYPE_CHECKING:
|
||||
|
||||
APPLICATION_NAME: Final = "HomeAssistant"
|
||||
MAJOR_VERSION: Final = 2025
|
||||
MINOR_VERSION: Final = 4
|
||||
MINOR_VERSION: Final = 5
|
||||
PATCH_VERSION: Final = "0.dev0"
|
||||
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
||||
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
|
||||
|
||||
+714
-723
File diff suppressed because it is too large
Load Diff
Generated
+1
-1
@@ -758,7 +758,7 @@ debugpy==1.8.13
|
||||
# decora==0.6
|
||||
|
||||
# homeassistant.components.ecovacs
|
||||
deebot-client==12.3.1
|
||||
deebot-client==12.4.0
|
||||
|
||||
# homeassistant.components.ihc
|
||||
# homeassistant.components.namecheapdns
|
||||
|
||||
Generated
+1
-1
@@ -649,7 +649,7 @@ dbus-fast==2.43.0
|
||||
debugpy==1.8.13
|
||||
|
||||
# homeassistant.components.ecovacs
|
||||
deebot-client==12.3.1
|
||||
deebot-client==12.4.0
|
||||
|
||||
# homeassistant.components.ihc
|
||||
# homeassistant.components.namecheapdns
|
||||
|
||||
@@ -41,6 +41,7 @@ class MockHeos(Heos):
|
||||
self.player_get_quick_selects: AsyncMock = AsyncMock()
|
||||
self.player_play_next: AsyncMock = AsyncMock()
|
||||
self.player_play_previous: AsyncMock = AsyncMock()
|
||||
self.player_play_queue: AsyncMock = AsyncMock()
|
||||
self.player_play_quick_select: AsyncMock = AsyncMock()
|
||||
self.player_set_mute: AsyncMock = AsyncMock()
|
||||
self.player_set_play_mode: AsyncMock = AsyncMock()
|
||||
|
||||
@@ -1321,6 +1321,51 @@ async def test_play_media_music_source_url(
|
||||
controller.play_url.assert_called_once()
|
||||
|
||||
|
||||
async def test_play_media_queue(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
controller: MockHeos,
|
||||
) -> None:
|
||||
"""Test the play media service with type queue."""
|
||||
config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
|
||||
await hass.services.async_call(
|
||||
MEDIA_PLAYER_DOMAIN,
|
||||
SERVICE_PLAY_MEDIA,
|
||||
{
|
||||
ATTR_ENTITY_ID: "media_player.test_player",
|
||||
ATTR_MEDIA_CONTENT_TYPE: "queue",
|
||||
ATTR_MEDIA_CONTENT_ID: "2",
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
controller.player_play_queue.assert_called_once_with(1, 2)
|
||||
|
||||
|
||||
async def test_play_media_queue_invalid(
|
||||
hass: HomeAssistant, config_entry: MockConfigEntry, controller: MockHeos
|
||||
) -> None:
|
||||
"""Test the play media service with an invalid queue id."""
|
||||
config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
with pytest.raises(
|
||||
HomeAssistantError,
|
||||
match=re.escape("Unable to play media: Invalid queue id 'Invalid'"),
|
||||
):
|
||||
await hass.services.async_call(
|
||||
MEDIA_PLAYER_DOMAIN,
|
||||
SERVICE_PLAY_MEDIA,
|
||||
{
|
||||
ATTR_ENTITY_ID: "media_player.test_player",
|
||||
ATTR_MEDIA_CONTENT_TYPE: "queue",
|
||||
ATTR_MEDIA_CONTENT_ID: "Invalid",
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
assert controller.player_play_queue.call_count == 0
|
||||
|
||||
|
||||
async def test_browse_media_root(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
|
||||
@@ -2,10 +2,19 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import threading
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components import recorder
|
||||
from homeassistant.components.recorder.db_schema import StatisticsMeta
|
||||
from homeassistant.components.recorder.models import (
|
||||
StatisticMeanType,
|
||||
StatisticMetaData,
|
||||
)
|
||||
from homeassistant.components.recorder.util import session_scope
|
||||
from homeassistant.const import DEGREE
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.typing import RecorderInstanceGenerator
|
||||
@@ -55,3 +64,78 @@ async def test_unsafe_calls_to_statistics_meta_manager(
|
||||
session,
|
||||
statistic_ids=["light.kitchen"],
|
||||
)
|
||||
|
||||
|
||||
async def test_invalid_mean_types(
|
||||
async_setup_recorder_instance: RecorderInstanceGenerator,
|
||||
hass: HomeAssistant,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test passing invalid mean types will be skipped and logged."""
|
||||
instance = await async_setup_recorder_instance(
|
||||
hass, {recorder.CONF_COMMIT_INTERVAL: 0}
|
||||
)
|
||||
instance.recorder_and_worker_thread_ids.add(threading.get_ident())
|
||||
|
||||
valid_metadata: dict[str, tuple[int, StatisticMetaData]] = {
|
||||
"sensor.energy": (
|
||||
1,
|
||||
{
|
||||
"mean_type": StatisticMeanType.NONE,
|
||||
"has_mean": False,
|
||||
"has_sum": True,
|
||||
"name": "Total imported energy",
|
||||
"source": "recorder",
|
||||
"statistic_id": "sensor.energy",
|
||||
"unit_of_measurement": "kWh",
|
||||
},
|
||||
),
|
||||
"sensor.wind_direction": (
|
||||
2,
|
||||
{
|
||||
"mean_type": StatisticMeanType.CIRCULAR,
|
||||
"has_mean": False,
|
||||
"has_sum": False,
|
||||
"name": "Wind direction",
|
||||
"source": "recorder",
|
||||
"statistic_id": "sensor.wind_direction",
|
||||
"unit_of_measurement": DEGREE,
|
||||
},
|
||||
),
|
||||
"sensor.wind_speed": (
|
||||
3,
|
||||
{
|
||||
"mean_type": StatisticMeanType.ARITHMETIC,
|
||||
"has_mean": True,
|
||||
"has_sum": False,
|
||||
"name": "Wind speed",
|
||||
"source": "recorder",
|
||||
"statistic_id": "sensor.wind_speed",
|
||||
"unit_of_measurement": "km/h",
|
||||
},
|
||||
),
|
||||
}
|
||||
manager = instance.statistics_meta_manager
|
||||
with instance.get_session() as session:
|
||||
for _, metadata in valid_metadata.values():
|
||||
session.add(StatisticsMeta.from_meta(metadata))
|
||||
|
||||
# Add invalid mean type
|
||||
session.add(
|
||||
StatisticsMeta(
|
||||
statistic_id="sensor.invalid",
|
||||
source="recorder",
|
||||
has_sum=False,
|
||||
name="Invalid",
|
||||
mean_type=12345,
|
||||
)
|
||||
)
|
||||
session.commit()
|
||||
|
||||
# Check that the invalid mean type was skipped
|
||||
assert manager.get_many(session) == valid_metadata
|
||||
assert (
|
||||
"homeassistant.components.recorder.table_managers.statistics_meta",
|
||||
logging.WARNING,
|
||||
"Invalid mean type found for statistic_id: sensor.invalid, mean_type: 12345. Skipping",
|
||||
) in caplog.record_tuples
|
||||
|
||||
@@ -50,7 +50,7 @@ async def test_roku_binary_sensors(
|
||||
assert entry.unique_id == f"{UPNP_SERIAL}_supports_ethernet"
|
||||
assert entry.entity_category == EntityCategory.DIAGNOSTIC
|
||||
assert state.state == STATE_ON
|
||||
assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Roku 3 Supports ethernet"
|
||||
assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Roku 3 Supports Ethernet"
|
||||
assert ATTR_DEVICE_CLASS not in state.attributes
|
||||
|
||||
state = hass.states.get("binary_sensor.my_roku_3_supports_find_remote")
|
||||
@@ -125,7 +125,7 @@ async def test_rokutv_binary_sensors(
|
||||
assert entry.entity_category == EntityCategory.DIAGNOSTIC
|
||||
assert state.state == STATE_ON
|
||||
assert (
|
||||
state.attributes.get(ATTR_FRIENDLY_NAME) == '58" Onn Roku TV Supports ethernet'
|
||||
state.attributes.get(ATTR_FRIENDLY_NAME) == '58" Onn Roku TV Supports Ethernet'
|
||||
)
|
||||
assert ATTR_DEVICE_CLASS not in state.attributes
|
||||
|
||||
|
||||
Reference in New Issue
Block a user