Compare commits

...

12 Commits

Author SHA1 Message Date
J. Nick Koston 582761a6ec Merge branch 'dev' into ingress_dropping_close 2025-03-26 10:51:06 -10:00
Andrew Sayre 6bfd39f094 Add play queue item to HEOS (#141480)
Add ability to play specific queue item
2025-03-26 15:47:10 -05:00
Robert Resch 002ca9611d Add test for invalid mean type in StatisticsMeta (#141475) 2025-03-26 21:40:02 +01:00
Joost Lekkerkerker 46ee3d2b26 Sort SmartThings devices to be created by parent device id (#141515) 2025-03-26 20:52:39 +01:00
Franck Nijhof eb901bcf3a Bump version to 2025.5.0dev0 (#141507) 2025-03-26 20:30:03 +01:00
Norbert Rittel 930b4a2c81 Capitalize "Ethernet" in roku sensor name (#141509)
* Capitalize "Ethernet" in `roku` sensor name

* Update test_binary_sensor.py
2025-03-26 21:18:52 +02:00
Robert Resch 22d1b8e1cd Bump deebot-client to 12.4.0 (#141501) 2025-03-26 19:36:04 +01:00
J. Nick Koston a8bdf80044 Merge branch 'dev' into ingress_dropping_close 2025-03-09 10:08:20 -10:00
J. Nick Koston 14b16e298a Merge branch 'dev' into ingress_dropping_close 2025-01-08 07:26:39 -10:00
J. Nick Koston 75cf02cad8 Merge branch 'dev' into ingress_dropping_close 2024-11-08 23:24:44 +00:00
J. Nick Koston b2a737cf56 Merge branch 'dev' into ingress_dropping_close 2024-11-03 00:50:32 -05:00
J. Nick Koston 98601da3ea Fix ingress websocket forward not closing the websocket
Becuase the async iterator was used, it would stop iteration as soon
as a close message was seen. This meant we would never send close to
the other side
2024-09-25 07:56:47 -05:00
14 changed files with 879 additions and 738 deletions
+1 -1
View File
@@ -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"]
}
+15 -6
View File
@@ -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")
+1 -1
View File
@@ -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 = {
+1 -1
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1
View File
@@ -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
+2 -2
View File
@@ -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