Compare commits

..

18 Commits

Author SHA1 Message Date
Claude
15094c0693 Remove volume_up/volume_down overrides from group media player
The base class already handles volume stepping with the default step
size of 0.1, making these overrides redundant. The previous
implementation also had a bug where iterating over entities and calling
async_set_volume_level would set all group members to the last entity's
adjusted volume rather than stepping each independently.

https://claude.ai/code/session_01Gn8AeZ8HvyyDw53e1rynUA
2026-02-27 19:34:45 -05:00
Franck Nijhof
7ef6c34149 Reject relative paths in SFTP storage backup location config flow (#164408) 2026-02-27 19:25:04 -05:00
Franck Nijhof
5b32e42b8c Add aioclient_mock to ssdp tests to prevent real HTTP requests (#164403) 2026-02-27 19:24:13 -05:00
Franck Nijhof
1be8b8e525 Add discovery mocks to tplink init tests (#164386) 2026-02-27 19:23:47 -05:00
Franck Nijhof
3fae15c430 Fix fixture ordering in esphome dashboard tests (#164367) 2026-02-27 19:23:13 -05:00
Franck Nijhof
c7e78568d0 Enable real sockets in default_config setup test (#164366) 2026-02-27 19:22:29 -05:00
Stefan Agner
492b542136 Fix Matter vacuum crash on nullable ServiceArea location info (#164411) 2026-02-28 00:11:32 +01:00
Franck Nijhof
0f4852d8c2 Enable sockets for http integration tests (#164404) 2026-02-27 22:22:15 +01:00
nopoz
737c0c1823 Google Cast: detect state and attributes when device is doing active non-media casting (#160819)
Co-authored-by: Erik Montnemery <erik@montnemery.com>
2026-02-27 22:07:09 +01:00
Petro31
5fadcb01e9 Fix int vs float template sensor issue (#164339) 2026-02-27 22:06:37 +01:00
TheJulianJES
2b4f46a739 Fix ZHA update entities not working after reload (#164290) 2026-02-27 22:04:51 +01:00
Franck Nijhof
44fe37da1f Mock ConnectionContextBuilder in homematicip_cloud tests (#164356) 2026-02-27 22:00:37 +01:00
Joost Lekkerkerker
abd4e89577 Sync SmartThings vacuum fixture (#164360) 2026-02-27 21:43:30 +01:00
Franck Nijhof
033798835a Refactor adguard tests to use proper fixtures for mocking (#164402)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-27 21:34:10 +01:00
Franck Nijhof
83c77957c1 Add missing mock fixtures to telegram_bot polling init test (#164398) 2026-02-27 21:29:10 +01:00
dependabot[bot]
b1bc1dc102 Bump actions/dependency-review-action from 4.8.2 to 4.8.3 (#164296)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-27 21:21:15 +01:00
Jason Hunter
40b8a2c380 Remove Duke Energy (#164282)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2026-02-27 20:19:03 +00:00
Glenn de Haan
fb23a6fbf8 Add HDFury audio offset numbers (#164315) 2026-02-27 21:02:34 +01:00
73 changed files with 1907 additions and 1066 deletions

View File

@@ -605,7 +605,7 @@ jobs:
with:
persist-credentials: false
- name: Dependency review
uses: actions/dependency-review-action@3c4e3dcb1aa7874d2c16be7d79418e9b7efd6261 # v4.8.2
uses: actions/dependency-review-action@05fe4576374b728f0c523d6a13d64c25081e0803 # v4.8.3
with:
license-check: false # We use our own license audit checks

2
CODEOWNERS generated
View File

@@ -401,8 +401,6 @@ build.json @home-assistant/supervisor
/tests/components/dsmr_reader/ @sorted-bits @glodenox @erwindouna
/homeassistant/components/duckdns/ @tr4nt0r
/tests/components/duckdns/ @tr4nt0r
/homeassistant/components/duke_energy/ @hunterjm
/tests/components/duke_energy/ @hunterjm
/homeassistant/components/duotecno/ @cereal2nd
/tests/components/duotecno/ @cereal2nd
/homeassistant/components/dwd_weather_warnings/ @runningman84 @stephan192

View File

@@ -807,6 +807,7 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity):
# The lovelace app loops media to prevent timing out, don't show that
if self.app_id == CAST_APP_ID_HOMEASSISTANT_LOVELACE:
return MediaPlayerState.PLAYING
if (media_status := self._media_status()[0]) is not None:
if media_status.player_state == MEDIA_PLAYER_STATE_PLAYING:
return MediaPlayerState.PLAYING
@@ -817,19 +818,19 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity):
if media_status.player_is_idle:
return MediaPlayerState.IDLE
if self._chromecast is not None and self._chromecast.is_idle:
# If library consider us idle, that is our off state
# it takes HDMI status into account for cast devices.
return MediaPlayerState.OFF
if self.app_id in APP_IDS_UNRELIABLE_MEDIA_INFO:
# Some apps don't report media status, show the player as playing
return MediaPlayerState.PLAYING
if self.app_id is not None:
if self.app_id is not None and self.app_id != pychromecast.config.APP_BACKDROP:
# We have an active app
return MediaPlayerState.IDLE
if self._chromecast is not None and self._chromecast.is_idle:
# If library consider us idle, that is our off state
# it takes HDMI status into account for cast devices.
return MediaPlayerState.OFF
return None
@property

View File

@@ -1,22 +0,0 @@
"""The Duke Energy integration."""
from __future__ import annotations
from homeassistant.core import HomeAssistant
from .coordinator import DukeEnergyConfigEntry, DukeEnergyCoordinator
async def async_setup_entry(hass: HomeAssistant, entry: DukeEnergyConfigEntry) -> bool:
"""Set up Duke Energy from a config entry."""
coordinator = DukeEnergyCoordinator(hass, entry)
await coordinator.async_config_entry_first_refresh()
entry.runtime_data = coordinator
return True
async def async_unload_entry(hass: HomeAssistant, entry: DukeEnergyConfigEntry) -> bool:
"""Unload a config entry."""
return True

View File

@@ -1,67 +0,0 @@
"""Config flow for Duke Energy integration."""
from __future__ import annotations
import logging
from typing import Any
from aiodukeenergy import DukeEnergy
from aiohttp import ClientError, ClientResponseError
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, CONF_USERNAME
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
STEP_USER_DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_USERNAME): str,
vol.Required(CONF_PASSWORD): str,
}
)
class DukeEnergyConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Duke Energy."""
VERSION = 1
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the initial step."""
errors: dict[str, str] = {}
if user_input is not None:
session = async_get_clientsession(self.hass)
api = DukeEnergy(
user_input[CONF_USERNAME], user_input[CONF_PASSWORD], session
)
try:
auth = await api.authenticate()
except ClientResponseError as e:
errors["base"] = "invalid_auth" if e.status == 404 else "cannot_connect"
except ClientError, TimeoutError:
errors["base"] = "cannot_connect"
except Exception:
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
username = auth["internalUserID"].lower()
await self.async_set_unique_id(username)
self._abort_if_unique_id_configured()
email = auth["loginEmailAddress"].lower()
data = {
CONF_EMAIL: email,
CONF_USERNAME: username,
CONF_PASSWORD: user_input[CONF_PASSWORD],
}
self._async_abort_entries_match(data)
return self.async_create_entry(title=email, data=data)
return self.async_show_form(
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
)

View File

@@ -1,3 +0,0 @@
"""Constants for the Duke Energy integration."""
DOMAIN = "duke_energy"

View File

@@ -1,222 +0,0 @@
"""Coordinator to handle Duke Energy connections."""
from datetime import datetime, timedelta
import logging
from typing import Any, cast
from aiodukeenergy import DukeEnergy
from aiohttp import ClientError
from homeassistant.components.recorder import get_instance
from homeassistant.components.recorder.models import (
StatisticData,
StatisticMeanType,
StatisticMetaData,
)
from homeassistant.components.recorder.statistics import (
async_add_external_statistics,
get_last_statistics,
statistics_during_period,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, UnitOfEnergy, UnitOfVolume
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from homeassistant.util import dt as dt_util
from homeassistant.util.unit_conversion import EnergyConverter
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
_SUPPORTED_METER_TYPES = ("ELECTRIC",)
type DukeEnergyConfigEntry = ConfigEntry[DukeEnergyCoordinator]
class DukeEnergyCoordinator(DataUpdateCoordinator[None]):
"""Handle inserting statistics."""
config_entry: DukeEnergyConfigEntry
def __init__(
self, hass: HomeAssistant, config_entry: DukeEnergyConfigEntry
) -> None:
"""Initialize the data handler."""
super().__init__(
hass,
_LOGGER,
config_entry=config_entry,
name="Duke Energy",
# Data is updated daily on Duke Energy.
# Refresh every 12h to be at most 12h behind.
update_interval=timedelta(hours=12),
)
self.api = DukeEnergy(
config_entry.data[CONF_USERNAME],
config_entry.data[CONF_PASSWORD],
async_get_clientsession(hass),
)
self._statistic_ids: set = set()
@callback
def _dummy_listener() -> None:
pass
# Force the coordinator to periodically update by registering at least one listener.
# Duke Energy does not provide forecast data, so all information is historical.
# This makes _async_update_data get periodically called so we can insert statistics.
self.async_add_listener(_dummy_listener)
self.config_entry.async_on_unload(self._clear_statistics)
def _clear_statistics(self) -> None:
"""Clear statistics."""
get_instance(self.hass).async_clear_statistics(list(self._statistic_ids))
async def _async_update_data(self) -> None:
"""Insert Duke Energy statistics."""
meters: dict[str, dict[str, Any]] = await self.api.get_meters()
for serial_number, meter in meters.items():
if (
not isinstance(meter["serviceType"], str)
or meter["serviceType"] not in _SUPPORTED_METER_TYPES
):
_LOGGER.debug(
"Skipping unsupported meter type %s", meter["serviceType"]
)
continue
id_prefix = f"{meter['serviceType'].lower()}_{serial_number}"
consumption_statistic_id = f"{DOMAIN}:{id_prefix}_energy_consumption"
self._statistic_ids.add(consumption_statistic_id)
_LOGGER.debug(
"Updating Statistics for %s",
consumption_statistic_id,
)
last_stat = await get_instance(self.hass).async_add_executor_job(
get_last_statistics, self.hass, 1, consumption_statistic_id, True, set()
)
if not last_stat:
_LOGGER.debug("Updating statistic for the first time")
usage = await self._async_get_energy_usage(meter)
consumption_sum = 0.0
last_stats_time = None
else:
usage = await self._async_get_energy_usage(
meter,
last_stat[consumption_statistic_id][0]["start"],
)
if not usage:
_LOGGER.debug("No recent usage data. Skipping update")
continue
stats = await get_instance(self.hass).async_add_executor_job(
statistics_during_period,
self.hass,
min(usage.keys()),
None,
{consumption_statistic_id},
"hour",
None,
{"sum"},
)
consumption_sum = cast(float, stats[consumption_statistic_id][0]["sum"])
last_stats_time = stats[consumption_statistic_id][0]["start"]
consumption_statistics = []
for start, data in usage.items():
if last_stats_time is not None and start.timestamp() <= last_stats_time:
continue
consumption_sum += data["energy"]
consumption_statistics.append(
StatisticData(
start=start, state=data["energy"], sum=consumption_sum
)
)
name_prefix = (
f"Duke Energy {meter['serviceType'].capitalize()} {serial_number}"
)
consumption_metadata = StatisticMetaData(
mean_type=StatisticMeanType.NONE,
has_sum=True,
name=f"{name_prefix} Consumption",
source=DOMAIN,
statistic_id=consumption_statistic_id,
unit_class=EnergyConverter.UNIT_CLASS,
unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR
if meter["serviceType"] == "ELECTRIC"
else UnitOfVolume.CENTUM_CUBIC_FEET,
)
_LOGGER.debug(
"Adding %s statistics for %s",
len(consumption_statistics),
consumption_statistic_id,
)
async_add_external_statistics(
self.hass, consumption_metadata, consumption_statistics
)
async def _async_get_energy_usage(
self, meter: dict[str, Any], start_time: float | None = None
) -> dict[datetime, dict[str, float | int]]:
"""Get energy usage.
If start_time is None, get usage since account activation (or as far back as possible),
otherwise since start_time - 30 days to allow corrections in data.
Duke Energy provides hourly data all the way back to ~3 years.
"""
# All of Duke Energy Service Areas are currently in America/New_York timezone
# May need to re-think this if that ever changes and determine timezone based
# on the service address somehow.
tz = await dt_util.async_get_time_zone("America/New_York")
lookback = timedelta(days=30)
one = timedelta(days=1)
if start_time is None:
# Max 3 years of data
start = dt_util.now(tz) - timedelta(days=3 * 365)
else:
start = datetime.fromtimestamp(start_time, tz=tz) - lookback
agreement_date = dt_util.parse_datetime(meter["agreementActiveDate"])
if agreement_date is not None:
start = max(agreement_date.replace(tzinfo=tz), start)
start = start.replace(hour=0, minute=0, second=0, microsecond=0)
end = dt_util.now(tz).replace(hour=0, minute=0, second=0, microsecond=0) - one
_LOGGER.debug("Data lookup range: %s - %s", start, end)
start_step = max(end - lookback, start)
end_step = end
usage: dict[datetime, dict[str, float | int]] = {}
while True:
_LOGGER.debug("Getting hourly usage: %s - %s", start_step, end_step)
try:
# Get data
results = await self.api.get_energy_usage(
meter["serialNum"], "HOURLY", "DAY", start_step, end_step
)
usage = {**results["data"], **usage}
for missing in results["missing"]:
_LOGGER.debug("Missing data: %s", missing)
# Set next range
end_step = start_step - one
start_step = max(start_step - lookback, start)
# Make sure we don't go back too far
if end_step < start:
break
except TimeoutError, ClientError:
# ClientError is raised when there is no more data for the range
break
_LOGGER.debug("Got %s meter usage reads", len(usage))
return usage

View File

@@ -1,11 +0,0 @@
{
"domain": "duke_energy",
"name": "Duke Energy",
"codeowners": ["@hunterjm"],
"config_flow": true,
"dependencies": ["recorder"],
"documentation": "https://www.home-assistant.io/integrations/duke_energy",
"integration_type": "service",
"iot_class": "cloud_polling",
"requirements": ["aiodukeenergy==0.3.0"]
}

View File

@@ -1,20 +0,0 @@
{
"config": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"step": {
"user": {
"data": {
"password": "[%key:common::config_flow::data::password%]",
"username": "[%key:common::config_flow::data::username%]"
}
}
}
}
}

View File

@@ -78,12 +78,6 @@ query ($owner: String!, $repository: String!) {
number
}
}
merged_pull_request: pullRequests(
first:1
states: MERGED
) {
total: totalCount
}
release: latestRelease {
name
url

View File

@@ -28,9 +28,6 @@
"latest_tag": {
"default": "mdi:tag"
},
"merged_pulls_count": {
"default": "mdi:source-merge"
},
"pulls_count": {
"default": "mdi:source-pull"
},

View File

@@ -75,13 +75,6 @@ SENSOR_DESCRIPTIONS: tuple[GitHubSensorEntityDescription, ...] = (
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda data: data["pull_request"]["total"],
),
GitHubSensorEntityDescription(
key="merged_pulls_count",
translation_key="merged_pulls_count",
entity_category=EntityCategory.DIAGNOSTIC,
state_class=SensorStateClass.TOTAL,
value_fn=lambda data: data["merged_pull_request"]["total"],
),
GitHubSensorEntityDescription(
key="latest_commit",
translation_key="latest_commit",

View File

@@ -48,10 +48,6 @@
"latest_tag": {
"name": "Latest tag"
},
"merged_pulls_count": {
"name": "Merged pull requests",
"unit_of_measurement": "pull requests"
},
"pulls_count": {
"name": "Pull requests",
"unit_of_measurement": "pull requests"

View File

@@ -423,20 +423,6 @@ class MediaPlayerGroup(MediaPlayerEntity):
context=self._context,
)
async def async_volume_up(self) -> None:
"""Turn volume up for media player(s)."""
for entity in self._features[KEY_VOLUME]:
volume_level = self.hass.states.get(entity).attributes["volume_level"] # type: ignore[union-attr]
if volume_level < 1:
await self.async_set_volume_level(min(1, volume_level + 0.1))
async def async_volume_down(self) -> None:
"""Turn volume down for media player(s)."""
for entity in self._features[KEY_VOLUME]:
volume_level = self.hass.states.get(entity).attributes["volume_level"] # type: ignore[union-attr]
if volume_level > 0:
await self.async_set_volume_level(max(0, volume_level - 0.1))
@callback
def async_update_group_state(self) -> None:
"""Query all members and determine the media group state."""

View File

@@ -6,6 +6,12 @@
}
},
"number": {
"audio_unmute": {
"default": "mdi:volume-high"
},
"earc_unmute": {
"default": "mdi:volume-high"
},
"oled_fade": {
"default": "mdi:cellphone-information"
},

View File

@@ -31,6 +31,32 @@ class HDFuryNumberEntityDescription(NumberEntityDescription):
NUMBERS: tuple[HDFuryNumberEntityDescription, ...] = (
HDFuryNumberEntityDescription(
key="unmutecnt",
translation_key="audio_unmute",
entity_registry_enabled_default=False,
mode=NumberMode.BOX,
native_min_value=50,
native_max_value=1000,
native_step=1,
device_class=NumberDeviceClass.DURATION,
native_unit_of_measurement=UnitOfTime.MILLISECONDS,
entity_category=EntityCategory.CONFIG,
set_value_fn=lambda client, value: client.set_audio_unmute(value),
),
HDFuryNumberEntityDescription(
key="earcunmutecnt",
translation_key="earc_unmute",
entity_registry_enabled_default=False,
mode=NumberMode.BOX,
native_min_value=0,
native_max_value=1000,
native_step=1,
device_class=NumberDeviceClass.DURATION,
native_unit_of_measurement=UnitOfTime.MILLISECONDS,
entity_category=EntityCategory.CONFIG,
set_value_fn=lambda client, value: client.set_earc_unmute(value),
),
HDFuryNumberEntityDescription(
key="oledfade",
translation_key="oled_fade",

View File

@@ -41,6 +41,12 @@
}
},
"number": {
"audio_unmute": {
"name": "Unmute delay"
},
"earc_unmute": {
"name": "eARC unmute delay"
},
"oled_fade": {
"name": "OLED fade timer"
},

View File

@@ -168,8 +168,9 @@ class MatterVacuum(MatterEntity, StateVacuumEntity):
segments: dict[str, Segment] = {}
for area in supported_areas:
area_name = None
if area.areaInfo and area.areaInfo.locationInfo:
area_name = area.areaInfo.locationInfo.locationName
location_info = area.areaInfo.locationInfo
if location_info not in (None, clusters.NullValue):
area_name = location_info.locationName
if area_name:
segment_id = str(area.areaID)

View File

@@ -124,6 +124,17 @@ class SFTPFlowHandler(ConfigFlow, domain=DOMAIN):
}
)
if not user_input[CONF_BACKUP_LOCATION].startswith("/"):
errors[CONF_BACKUP_LOCATION] = "backup_location_relative"
return self.async_show_form(
step_id=step_id,
data_schema=self.add_suggested_values_to_schema(
DATA_SCHEMA, user_input
),
description_placeholders=placeholders,
errors=errors,
)
try:
# Validate auth input and save uploaded key file if provided
user_input = await self._validate_auth_and_save_keyfile(user_input)

View File

@@ -4,6 +4,7 @@
"already_configured": "Integration already configured. Host with same address, port and backup location already exists."
},
"error": {
"backup_location_relative": "The remote path must be an absolute path (starting with `/`).",
"invalid_key": "Invalid key uploaded. Please make sure key corresponds to valid SSH key algorithm.",
"key_or_password_needed": "Please configure password or private key file location for SFTP Storage.",
"os_error": "{error_message}. Please check if host and/or port are correct.",

View File

@@ -257,6 +257,9 @@ class AbstractTemplateSensor(AbstractTemplateEntity, RestoreSensor):
) -> StateType | date | datetime | Decimal | None:
"""Validate the state."""
if self._numeric_state_expected:
if not isinstance(result, bool) and isinstance(result, (int, float)):
return result
return template_validators.number(self, CONF_STATE)(result)
if result is None or self.device_class not in (

View File

@@ -274,6 +274,9 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Unload ZHA config entry."""
if not await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS):
return False
ha_zha_data = get_zha_data(hass)
ha_zha_data.config_entry = None
@@ -281,6 +284,8 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
await ha_zha_data.gateway_proxy.shutdown()
ha_zha_data.gateway_proxy = None
ha_zha_data.update_coordinator = None
# clean up any remaining entity metadata
# (entities that have been discovered but not yet added to HA)
# suppress KeyError because we don't know what state we may
@@ -291,7 +296,7 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
websocket_api.async_unload_api(hass)
return await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS)
return True
async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:

View File

@@ -161,7 +161,6 @@ FLOWS = {
"dsmr",
"dsmr_reader",
"duckdns",
"duke_energy",
"dunehd",
"duotecno",
"dwd_weather_warnings",

View File

@@ -1497,12 +1497,6 @@
"config_flow": true,
"iot_class": "cloud_polling"
},
"duke_energy": {
"name": "Duke Energy",
"integration_type": "service",
"config_flow": true,
"iot_class": "cloud_polling"
},
"dunehd": {
"name": "Dune HD",
"integration_type": "device",

3
requirements_all.txt generated
View File

@@ -235,9 +235,6 @@ aiodiscover==2.7.1
# homeassistant.components.dnsip
aiodns==4.0.0
# homeassistant.components.duke_energy
aiodukeenergy==0.3.0
# homeassistant.components.eafm
aioeafm==0.1.2

View File

@@ -226,9 +226,6 @@ aiodiscover==2.7.1
# homeassistant.components.dnsip
aiodns==4.0.0
# homeassistant.components.duke_energy
aiodukeenergy==0.3.0
# homeassistant.components.eafm
aioeafm==0.1.2

View File

@@ -295,7 +295,6 @@ INTEGRATIONS_WITHOUT_QUALITY_SCALE_FILE = [
"dsmr",
"dsmr_reader",
"dublin_bus_transport",
"duke_energy",
"dunehd",
"duotecno",
"dwd_weather_warnings",
@@ -1273,7 +1272,6 @@ INTEGRATIONS_WITHOUT_SCALE = [
"dsmr",
"dsmr_reader",
"dublin_bus_transport",
"duke_energy",
"dunehd",
"duotecno",
"dwd_weather_warnings",

View File

@@ -1,23 +1 @@
"""Tests for the AdGuard Home integration."""
from collections.abc import AsyncGenerator
from unittest.mock import patch
from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry
async def setup_integration(
hass: HomeAssistant,
config_entry: MockConfigEntry,
adguard_mock: AsyncGenerator,
) -> None:
"""Fixture for setting up the component."""
config_entry.add_to_hass(hass)
with patch(
"homeassistant.components.adguard.AdGuardHome",
return_value=adguard_mock,
):
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()

View File

@@ -1,6 +1,7 @@
"""Common fixtures for the adguard tests."""
from unittest.mock import AsyncMock
from collections.abc import Generator
from unittest.mock import AsyncMock, patch
from adguardhome import AdGuardHome
from adguardhome.filtering import AdGuardHomeFiltering
@@ -12,7 +13,7 @@ from adguardhome.stats import AdGuardHomeStats
from adguardhome.update import AdGuardHomeAvailableUpdate, AdGuardHomeUpdate
import pytest
from homeassistant.components.adguard import DOMAIN
from homeassistant.components.adguard import DOMAIN, PLATFORMS
from homeassistant.const import (
CONF_HOST,
CONF_PASSWORD,
@@ -20,7 +21,9 @@ from homeassistant.const import (
CONF_SSL,
CONF_USERNAME,
CONF_VERIFY_SSL,
Platform,
)
from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry
@@ -43,8 +46,8 @@ def mock_config_entry() -> MockConfigEntry:
@pytest.fixture
async def mock_adguard() -> AsyncMock:
"""Fixture for setting up the component."""
def mock_adguard() -> Generator[AsyncMock]:
"""Return a mocked AdGuard Home client."""
adguard_mock = AsyncMock(spec=AdGuardHome)
adguard_mock.filtering = AsyncMock(spec=AdGuardHomeFiltering)
adguard_mock.parental = AsyncMock(spec=AdGuardHomeParental)
@@ -86,4 +89,31 @@ async def mock_adguard() -> AsyncMock:
)
)
return adguard_mock
with patch(
"homeassistant.components.adguard.AdGuardHome",
return_value=adguard_mock,
):
yield adguard_mock
@pytest.fixture
def platforms() -> list[Platform]:
"""Fixture to specify platforms to test."""
return PLATFORMS
@pytest.fixture
async def init_integration(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_adguard: AsyncMock,
platforms: list[Platform],
) -> MockConfigEntry:
"""Set up the AdGuard Home integration for testing."""
mock_config_entry.add_to_hass(hass)
with patch("homeassistant.components.adguard.PLATFORMS", platforms):
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
return mock_config_entry

View File

@@ -1,25 +1,28 @@
"""Tests for the AdGuard Home."""
from unittest.mock import AsyncMock, patch
from unittest.mock import AsyncMock
from adguardhome import AdGuardHomeConnectionError
import pytest
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from . import setup_integration
from tests.common import MockConfigEntry
@pytest.fixture
def platforms() -> list[Platform]:
"""Fixture to specify platforms to test."""
return []
@pytest.mark.usefixtures("init_integration")
async def test_setup(
hass: HomeAssistant,
mock_adguard: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test the adguard setup."""
with patch("homeassistant.components.adguard.PLATFORMS", []):
await setup_integration(hass, mock_config_entry, mock_adguard)
assert mock_config_entry.state is ConfigEntryState.LOADED
@@ -31,5 +34,8 @@ async def test_setup_failed(
"""Test the adguard setup failed."""
mock_adguard.version.side_effect = AdGuardHomeConnectionError("Connection error")
await setup_integration(hass, mock_config_entry, mock_adguard)
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.SETUP_RETRY

View File

@@ -1,7 +1,5 @@
"""Tests for the AdGuard Home sensor entities."""
from unittest.mock import AsyncMock, patch
import pytest
from syrupy.assertion import SnapshotAssertion
@@ -9,21 +7,21 @@ from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from . import setup_integration
from tests.common import MockConfigEntry, snapshot_platform
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
@pytest.fixture
def platforms() -> list[Platform]:
"""Fixture to specify platforms to test."""
return [Platform.SENSOR]
@pytest.mark.usefixtures("entity_registry_enabled_by_default", "init_integration")
async def test_sensors(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
snapshot: SnapshotAssertion,
mock_adguard: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test the adguard sensor platform."""
with patch("homeassistant.components.adguard.PLATFORMS", [Platform.SENSOR]):
await setup_integration(hass, mock_config_entry, mock_adguard)
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)

View File

@@ -2,7 +2,7 @@
from collections.abc import Callable
from typing import Any
from unittest.mock import AsyncMock, patch
from unittest.mock import AsyncMock
import pytest
@@ -14,22 +14,22 @@ from homeassistant.components.adguard.const import (
SERVICE_REFRESH,
SERVICE_REMOVE_URL,
)
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from . import setup_integration
pytestmark = pytest.mark.usefixtures("init_integration")
from tests.common import MockConfigEntry
@pytest.fixture
def platforms() -> list[Platform]:
"""Fixture to specify platforms to test."""
return []
async def test_service_registration(
hass: HomeAssistant,
mock_adguard: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test the adguard services be registered."""
with patch("homeassistant.components.adguard.PLATFORMS", []):
await setup_integration(hass, mock_config_entry, mock_adguard)
services = hass.services.async_services_for_domain(DOMAIN)
assert len(services) == 5
@@ -73,15 +73,11 @@ async def test_service_registration(
async def test_service(
hass: HomeAssistant,
mock_adguard: AsyncMock,
mock_config_entry: MockConfigEntry,
service: str,
service_call_data: dict,
call_assertion: Callable[[AsyncMock], Any],
) -> None:
"""Test the adguard services be unregistered with unloading last entry."""
with patch("homeassistant.components.adguard.PLATFORMS", []):
await setup_integration(hass, mock_config_entry, mock_adguard)
await hass.services.async_call(
DOMAIN,
service,

View File

@@ -3,7 +3,7 @@
from collections.abc import Callable
import logging
from typing import Any
from unittest.mock import AsyncMock, patch
from unittest.mock import AsyncMock
from adguardhome import AdGuardHomeError
import pytest
@@ -14,23 +14,26 @@ from homeassistant.const import ATTR_ENTITY_ID, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from . import setup_integration
from tests.common import MockConfigEntry, snapshot_platform
@pytest.fixture
def platforms() -> list[Platform]:
"""Fixture to specify platforms to test."""
return [Platform.SWITCH]
pytestmark = pytest.mark.usefixtures("init_integration")
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
async def test_switch(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
snapshot: SnapshotAssertion,
mock_adguard: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test the adguard switch platform."""
with patch("homeassistant.components.adguard.PLATFORMS", [Platform.SWITCH]):
await setup_integration(hass, mock_config_entry, mock_adguard)
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
@@ -103,15 +106,11 @@ async def test_switch(
async def test_switch_actions(
hass: HomeAssistant,
mock_adguard: AsyncMock,
mock_config_entry: MockConfigEntry,
switch_name: str,
service: str,
call_assertion: Callable[[AsyncMock], Any],
) -> None:
"""Test the adguard switch actions."""
with patch("homeassistant.components.adguard.PLATFORMS", [Platform.SWITCH]):
await setup_integration(hass, mock_config_entry, mock_adguard)
await hass.services.async_call(
"switch",
service,
@@ -138,7 +137,6 @@ async def test_switch_actions(
async def test_switch_action_failed(
hass: HomeAssistant,
mock_adguard: AsyncMock,
mock_config_entry: MockConfigEntry,
caplog: pytest.LogCaptureFixture,
service: str,
expected_message: str,
@@ -146,9 +144,6 @@ async def test_switch_action_failed(
"""Test the adguard switch actions."""
caplog.set_level(logging.ERROR)
with patch("homeassistant.components.adguard.PLATFORMS", [Platform.SWITCH]):
await setup_integration(hass, mock_config_entry, mock_adguard)
mock_adguard.enable_protection.side_effect = AdGuardHomeError("Boom")
mock_adguard.disable_protection.side_effect = AdGuardHomeError("Boom")

View File

@@ -12,22 +12,23 @@ from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_registry as er
from . import setup_integration
from tests.common import MockConfigEntry, snapshot_platform
@pytest.fixture
def platforms() -> list[Platform]:
"""Fixture to specify platforms to test."""
return [Platform.UPDATE]
@pytest.mark.usefixtures("init_integration")
async def test_update(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
snapshot: SnapshotAssertion,
mock_adguard: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test the adguard update platform."""
with patch("homeassistant.components.adguard.PLATFORMS", [Platform.UPDATE]):
await setup_integration(hass, mock_config_entry, mock_adguard)
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
@@ -41,41 +42,38 @@ async def test_update_disabled(
disabled=True,
)
mock_config_entry.add_to_hass(hass)
with patch("homeassistant.components.adguard.PLATFORMS", [Platform.UPDATE]):
await setup_integration(hass, mock_config_entry, mock_adguard)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
assert not hass.states.async_all()
@pytest.mark.usefixtures("init_integration")
async def test_update_install(
hass: HomeAssistant,
mock_adguard: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test the adguard update installation."""
with patch("homeassistant.components.adguard.PLATFORMS", [Platform.UPDATE]):
await setup_integration(hass, mock_config_entry, mock_adguard)
await hass.services.async_call(
"update",
"install",
{"entity_id": "update.adguard_home"},
blocking=True,
)
mock_adguard.update.begin_update.assert_called_once()
@pytest.mark.usefixtures("init_integration")
async def test_update_install_failed(
hass: HomeAssistant,
mock_adguard: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test the adguard update install failed."""
mock_adguard.update.begin_update.side_effect = AdGuardHomeError("boom")
with patch("homeassistant.components.adguard.PLATFORMS", [Platform.UPDATE]):
await setup_integration(hass, mock_config_entry, mock_adguard)
with pytest.raises(HomeAssistantError):
await hass.services.async_call(
"update",

View File

@@ -2385,3 +2385,42 @@ async def test_ha_cast(hass: HomeAssistant, ha_controller_mock) -> None:
chromecast.unregister_handler.reset_mock()
unregister_cb()
chromecast.unregister_handler.assert_not_called()
async def test_entity_media_states_active_app_reported_idle(
hass: HomeAssistant, entity_registry: er.EntityRegistry
) -> None:
"""Test entity state when app is active but device reports idle (fixes #160814)."""
entity_id = "media_player.speaker"
info = get_fake_chromecast_info()
chromecast, _ = await async_setup_media_player_cast(hass, info)
cast_status_cb, conn_status_cb, _ = get_status_callbacks(chromecast)
# Connect the device
connection_status = MagicMock()
connection_status.status = "CONNECTED"
conn_status_cb(connection_status)
await hass.async_block_till_done()
# Scenario: Custom App is running (e.g. DashCast), but device reports is_idle=True
chromecast.app_id = "84912283" # Example Custom App ID
chromecast.is_idle = True # Device thinks it's idle/standby
# Trigger a status update
cast_status = MagicMock()
cast_status_cb(cast_status)
await hass.async_block_till_done()
state = hass.states.get(entity_id)
assert state is not None
assert state.state == "idle"
# Scenario: Backdrop (Screensaver) is running. Should still be OFF.
chromecast.app_id = pychromecast.config.APP_BACKDROP
chromecast.is_idle = True
cast_status_cb(cast_status)
await hass.async_block_till_done()
state = hass.states.get(entity_id)
assert state.state == "off"

View File

@@ -30,7 +30,7 @@ def recorder_url_mock():
yield
@pytest.mark.usefixtures("mock_bluetooth", "mock_zeroconf")
@pytest.mark.usefixtures("mock_bluetooth", "mock_zeroconf", "socket_enabled")
async def test_setup(hass: HomeAssistant) -> None:
"""Test setup."""
recorder_helper.async_initialize_recorder(hass)

View File

@@ -1 +0,0 @@
"""Tests for the Duke Energy integration."""

View File

@@ -1,90 +0,0 @@
"""Common fixtures for the Duke Energy tests."""
from collections.abc import Generator
from unittest.mock import AsyncMock, patch
import pytest
from homeassistant.components.duke_energy.const import DOMAIN
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
from homeassistant.util import dt as dt_util
from tests.common import MockConfigEntry
from tests.typing import RecorderInstanceContextManager
@pytest.fixture
async def mock_recorder_before_hass(
async_test_recorder: RecorderInstanceContextManager,
) -> None:
"""Set up recorder."""
@pytest.fixture
def mock_setup_entry() -> Generator[AsyncMock]:
"""Override async_setup_entry."""
with patch(
"homeassistant.components.duke_energy.async_setup_entry",
return_value=True,
) as mock_setup_entry:
yield mock_setup_entry
@pytest.fixture
def mock_config_entry(hass: HomeAssistant) -> Generator[AsyncMock]:
"""Return the default mocked config entry."""
config_entry = MockConfigEntry(
domain=DOMAIN,
data={
CONF_EMAIL: "test@example.com",
CONF_USERNAME: "test-username",
CONF_PASSWORD: "test-password",
},
)
config_entry.add_to_hass(hass)
return config_entry
@pytest.fixture
def mock_api() -> Generator[AsyncMock]:
"""Mock a successful Duke Energy API."""
with (
patch(
"homeassistant.components.duke_energy.config_flow.DukeEnergy",
autospec=True,
) as mock_api,
patch(
"homeassistant.components.duke_energy.coordinator.DukeEnergy",
new=mock_api,
),
):
api = mock_api.return_value
api.authenticate.return_value = {
"loginEmailAddress": "TEST@EXAMPLE.COM",
"internalUserID": "test-username",
}
api.get_meters.return_value = {}
yield api
@pytest.fixture
def mock_api_with_meters(mock_api: AsyncMock) -> AsyncMock:
"""Mock a successful Duke Energy API with meters."""
mock_api.get_meters.return_value = {
"123": {
"serialNum": "123",
"serviceType": "ELECTRIC",
"agreementActiveDate": "2000-01-01",
},
}
mock_api.get_energy_usage.return_value = {
"data": {
dt_util.now(): {
"energy": 1.3,
"temperature": 70,
}
},
"missing": [],
}
return mock_api

View File

@@ -1,118 +0,0 @@
"""Test the Duke Energy config flow."""
from unittest.mock import AsyncMock, Mock
from aiohttp import ClientError, ClientResponseError
import pytest
from homeassistant import config_entries
from homeassistant.components.duke_energy.const import DOMAIN
from homeassistant.components.recorder import Recorder
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
async def test_user(
hass: HomeAssistant,
recorder_mock: Recorder,
mock_api: AsyncMock,
mock_setup_entry: AsyncMock,
) -> None:
"""Test user config."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result.get("type") is FlowResultType.FORM
assert result.get("step_id") == "user"
# test with all provided
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{CONF_USERNAME: "test-username", CONF_PASSWORD: "test-password"},
)
assert result.get("type") is FlowResultType.CREATE_ENTRY
assert result.get("title") == "test@example.com"
data = result.get("data")
assert data
assert data[CONF_USERNAME] == "test-username"
assert data[CONF_PASSWORD] == "test-password"
assert data[CONF_EMAIL] == "test@example.com"
async def test_abort_if_already_setup(
hass: HomeAssistant,
recorder_mock: Recorder,
mock_api: AsyncMock,
mock_config_entry: AsyncMock,
) -> None:
"""Test we abort if the email is already setup."""
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_USER},
data={
CONF_USERNAME: "test-username",
CONF_PASSWORD: "test-password",
},
)
assert result
assert result.get("type") is FlowResultType.ABORT
assert result.get("reason") == "already_configured"
async def test_abort_if_already_setup_alternate_username(
hass: HomeAssistant,
recorder_mock: Recorder,
mock_api: AsyncMock,
mock_config_entry: AsyncMock,
) -> None:
"""Test we abort if the email is already setup."""
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_USER},
data={
CONF_USERNAME: "test@example.com",
CONF_PASSWORD: "test-password",
},
)
assert result
assert result.get("type") is FlowResultType.ABORT
assert result.get("reason") == "already_configured"
@pytest.mark.parametrize(
("side_effect", "expected_error"),
[
(ClientResponseError(None, None, status=404), "invalid_auth"),
(ClientResponseError(None, None, status=500), "cannot_connect"),
(TimeoutError(), "cannot_connect"),
(ClientError(), "cannot_connect"),
(Exception(), "unknown"),
],
)
async def test_api_errors(
hass: HomeAssistant,
recorder_mock: Recorder,
mock_api: Mock,
side_effect,
expected_error,
) -> None:
"""Test the failure scenarios."""
mock_api.authenticate.side_effect = side_effect
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_USER},
data={CONF_USERNAME: "test-username", CONF_PASSWORD: "test-password"},
)
assert result.get("type") is FlowResultType.FORM
assert result.get("errors") == {"base": expected_error}
mock_api.authenticate.side_effect = None
# test with all provided
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{CONF_USERNAME: "test-username", CONF_PASSWORD: "test-password"},
)
assert result.get("type") is FlowResultType.CREATE_ENTRY

View File

@@ -1,44 +0,0 @@
"""Tests for the SolarEdge coordinator services."""
from datetime import timedelta
from unittest.mock import Mock, patch
from freezegun.api import FrozenDateTimeFactory
from homeassistant.components.recorder import Recorder
from homeassistant.core import HomeAssistant
from homeassistant.util import dt as dt_util
from tests.common import MockConfigEntry, async_fire_time_changed
async def test_update(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_api_with_meters: Mock,
freezer: FrozenDateTimeFactory,
recorder_mock: Recorder,
) -> None:
"""Test Coordinator."""
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
assert mock_api_with_meters.get_meters.call_count == 1
# 3 years of data
assert mock_api_with_meters.get_energy_usage.call_count == 37
with patch(
"homeassistant.components.duke_energy.coordinator.get_last_statistics",
return_value={
"duke_energy:electric_123_energy_consumption": [
{"start": dt_util.now().timestamp()}
]
},
):
freezer.tick(timedelta(hours=12))
async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True)
assert mock_api_with_meters.get_meters.call_count == 2
# Now have stats, so only one call
assert mock_api_with_meters.get_energy_usage.call_count == 38

View File

@@ -19,9 +19,11 @@ from .conftest import MockESPHomeDeviceType
from tests.common import MockConfigEntry
@pytest.mark.usefixtures("init_integration", "mock_dashboard")
@pytest.mark.usefixtures("mock_dashboard")
async def test_dashboard_storage(
hass: HomeAssistant,
mock_client: APIClient,
init_integration: MockConfigEntry,
hass_storage: dict[str, Any],
) -> None:
"""Test dashboard storage."""
@@ -129,6 +131,7 @@ async def test_setup_dashboard_fails(
async def test_setup_dashboard_fails_when_already_setup(
hass: HomeAssistant,
mock_client: APIClient,
mock_config_entry: MockConfigEntry,
hass_storage: dict[str, Any],
) -> None:
@@ -168,7 +171,9 @@ async def test_setup_dashboard_fails_when_already_setup(
@pytest.mark.usefixtures("mock_dashboard")
async def test_new_info_reload_config_entries(
hass: HomeAssistant, init_integration: MockConfigEntry
hass: HomeAssistant,
mock_client: APIClient,
init_integration: MockConfigEntry,
) -> None:
"""Test config entries are reloaded when new info is set."""
assert init_integration.state is ConfigEntryState.LOADED

View File

@@ -49,9 +49,6 @@
}
]
},
"merged_pull_request": {
"total": 42
},
"release": {
"name": "v1.0.0",
"url": "https://github.com/octocat/Hello-World/releases/v1.0.0",

View File

@@ -104,6 +104,8 @@ def mock_hdfury_client() -> Generator[AsyncMock]:
"relay": "0",
"macaddr": "c7:1c:df:9d:f6:40",
"reboottimer": "0",
"unmutecnt": "500",
"earcunmutecnt": "0",
"oled": "1",
"oledfade": "30",
}

View File

@@ -15,6 +15,7 @@
'cec1en': '1',
'cec2en': '1',
'cec3en': '1',
'earcunmutecnt': '0',
'htpcmode0': '0',
'htpcmode1': '0',
'htpcmode2': '0',
@@ -29,6 +30,7 @@
'relay': '0',
'tx0plus5': '1',
'tx1plus5': '1',
'unmutecnt': '500',
}),
'info': dict({
'AUD0': 'bitstream 48kHz',

View File

@@ -1,4 +1,64 @@
# serializer version: 1
# name: test_number_entities[number.hdfury_vrroom_02_earc_unmute_delay-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'max': 1000,
'min': 0,
'mode': <NumberMode.BOX: 'box'>,
'step': 1,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'number',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'number.hdfury_vrroom_02_earc_unmute_delay',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'eARC unmute delay',
'options': dict({
}),
'original_device_class': <NumberDeviceClass.DURATION: 'duration'>,
'original_icon': None,
'original_name': 'eARC unmute delay',
'platform': 'hdfury',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'earc_unmute',
'unique_id': '000123456789_earcunmutecnt',
'unit_of_measurement': <UnitOfTime.MILLISECONDS: 'ms'>,
})
# ---
# name: test_number_entities[number.hdfury_vrroom_02_earc_unmute_delay-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'duration',
'friendly_name': 'HDFury VRROOM-02 eARC unmute delay',
'max': 1000,
'min': 0,
'mode': <NumberMode.BOX: 'box'>,
'step': 1,
'unit_of_measurement': <UnitOfTime.MILLISECONDS: 'ms'>,
}),
'context': <ANY>,
'entity_id': 'number.hdfury_vrroom_02_earc_unmute_delay',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '0.0',
})
# ---
# name: test_number_entities[number.hdfury_vrroom_02_oled_fade_timer-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
@@ -119,3 +179,63 @@
'state': '0.0',
})
# ---
# name: test_number_entities[number.hdfury_vrroom_02_unmute_delay-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'max': 1000,
'min': 50,
'mode': <NumberMode.BOX: 'box'>,
'step': 1,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'number',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'number.hdfury_vrroom_02_unmute_delay',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Unmute delay',
'options': dict({
}),
'original_device_class': <NumberDeviceClass.DURATION: 'duration'>,
'original_icon': None,
'original_name': 'Unmute delay',
'platform': 'hdfury',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'audio_unmute',
'unique_id': '000123456789_unmutecnt',
'unit_of_measurement': <UnitOfTime.MILLISECONDS: 'ms'>,
})
# ---
# name: test_number_entities[number.hdfury_vrroom_02_unmute_delay-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'duration',
'friendly_name': 'HDFury VRROOM-02 Unmute delay',
'max': 1000,
'min': 50,
'mode': <NumberMode.BOX: 'box'>,
'step': 1,
'unit_of_measurement': <UnitOfTime.MILLISECONDS: 'ms'>,
}),
'context': <ANY>,
'entity_id': 'number.hdfury_vrroom_02_unmute_delay',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '500.0',
})
# ---

View File

@@ -23,6 +23,7 @@ from . import setup_integration
from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
async def test_number_entities(
hass: HomeAssistant,
snapshot: SnapshotAssertion,
@@ -40,8 +41,11 @@ async def test_number_entities(
[
("number.hdfury_vrroom_02_oled_fade_timer", "set_oled_fade"),
("number.hdfury_vrroom_02_restart_timer", "set_reboot_timer"),
("number.hdfury_vrroom_02_unmute_delay", "set_audio_unmute"),
("number.hdfury_vrroom_02_earc_unmute_delay", "set_earc_unmute"),
],
)
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
async def test_number_set_value(
hass: HomeAssistant,
mock_hdfury_client: AsyncMock,
@@ -68,8 +72,11 @@ async def test_number_set_value(
[
("number.hdfury_vrroom_02_oled_fade_timer", "set_oled_fade"),
("number.hdfury_vrroom_02_restart_timer", "set_reboot_timer"),
("number.hdfury_vrroom_02_unmute_delay", "set_audio_unmute"),
("number.hdfury_vrroom_02_earc_unmute_delay", "set_earc_unmute"),
],
)
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
async def test_number_error(
hass: HomeAssistant,
mock_hdfury_client: AsyncMock,
@@ -100,8 +107,11 @@ async def test_number_error(
[
("number.hdfury_vrroom_02_oled_fade_timer"),
("number.hdfury_vrroom_02_restart_timer"),
("number.hdfury_vrroom_02_unmute_delay"),
("number.hdfury_vrroom_02_earc_unmute_delay"),
],
)
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
async def test_number_entities_unavailable_on_error(
hass: HomeAssistant,
mock_hdfury_client: AsyncMock,

View File

@@ -131,10 +131,15 @@ def simple_mock_home_fixture():
get_current_state_async=AsyncMock(),
)
with patch(
"homeassistant.components.homematicip_cloud.hap.AsyncHome",
autospec=True,
return_value=mock_home,
with (
patch(
"homeassistant.components.homematicip_cloud.hap.AsyncHome",
autospec=True,
return_value=mock_home,
),
patch(
"homeassistant.components.homematicip_cloud.hap.ConnectionContextBuilder.build_context_async",
),
):
yield

View File

@@ -2,7 +2,6 @@
from unittest.mock import AsyncMock, Mock, patch
from homematicip.connection.connection_context import ConnectionContext
from homematicip.exceptions.connection_exceptions import HmipConnectionError
from homeassistant.components.homematicip_cloud.const import (
@@ -107,7 +106,6 @@ async def test_load_entry_fails_due_to_connection_error(
),
patch(
"homeassistant.components.homematicip_cloud.hap.ConnectionContextBuilder.build_context_async",
return_value=ConnectionContext(),
),
):
assert await async_setup_component(hass, DOMAIN, {})
@@ -127,6 +125,9 @@ async def test_load_entry_fails_due_to_generic_exception(
"homeassistant.components.homematicip_cloud.hap.AsyncHome.get_current_state_async",
side_effect=Exception,
),
patch(
"homeassistant.components.homematicip_cloud.hap.ConnectionContextBuilder.build_context_async",
),
):
assert await async_setup_component(hass, DOMAIN, {})

View File

@@ -27,7 +27,7 @@ from tests.typing import ClientSessionGenerator
@pytest.fixture(autouse=True)
def disable_http_server() -> None:
def disable_http_server(socket_enabled: None) -> None:
"""Override the global disable_http_server fixture with an empty fixture.
This allows the HTTP server to start in tests that need it.

View File

@@ -92,6 +92,7 @@ FIXTURES = [
"mock_window_covering_tilt",
"onoff_light_with_levelcontrol_present",
"resideo_x2s_thermostat",
"roborock_saros_10",
"secuyou_smart_lock",
"silabs_dishwasher",
"silabs_evse_charging",

View File

@@ -0,0 +1,540 @@
{
"node_id": 202,
"date_commissioned": "2025-01-01T00:00:00",
"last_interview": "2026-01-01T00:00:00",
"interview_version": 6,
"available": true,
"is_bridge": false,
"attributes": {
"0/29/0": [
{
"0": 22,
"1": 1
}
],
"0/29/1": [29, 31, 40, 48, 49, 50, 51, 60, 62, 63],
"0/29/2": [],
"0/29/3": [1],
"0/29/65533": 2,
"0/29/65532": 0,
"0/29/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533],
"0/29/65529": [],
"0/29/65528": [],
"0/31/0": [
{
"1": 5,
"2": 2,
"3": [112233],
"4": null,
"254": 2
}
],
"0/31/2": 4,
"0/31/3": 3,
"0/31/4": 4,
"0/31/65533": 2,
"0/31/65532": 0,
"0/31/65531": [0, 2, 3, 4, 65528, 65529, 65531, 65532, 65533],
"0/31/65529": [],
"0/31/65528": [],
"0/40/0": 18,
"0/40/1": "Roborock",
"0/40/2": 5248,
"0/40/3": "Robotic Vacuum Cleaner",
"0/40/4": 5,
"0/40/5": "",
"0/40/6": "**REDACTED**",
"0/40/7": 2,
"0/40/8": "1.4",
"0/40/9": 2,
"0/40/10": "1.4",
"0/40/13": "https://www.roborock.com",
"0/40/14": "Robotic Vacuum Cleaner",
"0/40/15": "RAPEED12345678",
"0/40/18": "12AB12AB12AB12AB",
"0/40/19": {
"0": 3,
"1": 65535
},
"0/40/21": 17039360,
"0/40/22": 1,
"0/40/24": 1,
"0/40/65533": 4,
"0/40/65532": 0,
"0/40/65531": [
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 13, 14, 15, 18, 19, 21, 22, 65528,
65529, 65531, 65532, 65533
],
"0/40/65529": [],
"0/40/65528": [],
"0/48/0": 0,
"0/48/1": {
"0": 60,
"1": 900
},
"0/48/2": 0,
"0/48/3": 2,
"0/48/4": true,
"0/48/65533": 2,
"0/48/65532": 0,
"0/48/65531": [0, 1, 2, 3, 4, 65528, 65529, 65531, 65532, 65533],
"0/48/65529": [0, 2, 4],
"0/48/65528": [1, 3, 5],
"0/49/0": 1,
"0/49/1": [],
"0/49/4": true,
"0/49/5": 0,
"0/49/6": null,
"0/49/7": null,
"0/49/2": 30,
"0/49/3": 60,
"0/49/8": [0],
"0/49/65533": 2,
"0/49/65532": 1,
"0/49/65531": [
0, 1, 2, 3, 4, 5, 6, 7, 8, 65528, 65529, 65531, 65532, 65533
],
"0/49/65529": [0, 2, 4, 6, 8],
"0/49/65528": [1, 5, 7],
"0/50/65533": 1,
"0/50/65532": 0,
"0/50/65531": [65528, 65529, 65531, 65532, 65533],
"0/50/65529": [0],
"0/50/65528": [1],
"0/51/0": [
{
"0": "ap0",
"1": false,
"2": null,
"3": null,
"4": "sko58laD",
"5": [],
"6": [],
"7": 0
},
{
"0": "wlan0",
"1": true,
"2": null,
"3": null,
"4": "sEo58laD",
"5": ["wKhQuQ=="],
"6": [
"/XqKrJXsABCySjn//vJWgw==",
"KgIBaTwJABCySjn//vJWgw==",
"/oAAAAAAAACySjn//vJWgw=="
],
"7": 0
},
{
"0": "sit0",
"1": false,
"2": null,
"3": null,
"4": "AAAAAAAA",
"5": [],
"6": [],
"7": 0
},
{
"0": "lo",
"1": true,
"2": null,
"3": null,
"4": "AAAAAAAA",
"5": ["fwAAAQ=="],
"6": ["AAAAAAAAAAAAAAAAAAAAAQ=="],
"7": 0
}
],
"0/51/1": 296,
"0/51/2": 8,
"0/51/3": 6328,
"0/51/8": false,
"0/51/65533": 2,
"0/51/65532": 0,
"0/51/65531": [0, 1, 2, 3, 8, 65528, 65529, 65531, 65532, 65533],
"0/51/65529": [0, 1],
"0/51/65528": [2],
"0/60/0": 0,
"0/60/1": null,
"0/60/2": null,
"0/60/65533": 1,
"0/60/65532": 0,
"0/60/65531": [0, 1, 2, 65528, 65529, 65531, 65532, 65533],
"0/60/65529": [0, 2],
"0/60/65528": [],
"0/62/0": [
{
"1": "FTABAQEkAgE3AyQTAhgmBIAigScmBYAlTTo3BiQVAiQRyhgkBwEkCAEwCUEEFn0vNfCOD0dTxJ+/vIAsLHsPottGgAzLEYjD0IZda+wcLI6otwL3l70MZK44UQact9g+kLna4RHtR2DtJjzi3DcKNQEoARgkAgE2AwQCBAEYMAQUfe7BMayXJA5FAhU93iHoPeGaicwwBRS9bdraaL8JLSNzrDNJcbicl5ghHRgwC0DAfR8r1sKukiqQw8dPHxQBsDVYjQ2jyerfvkYRSMQGIr9Pr594PCSUazATbDgxf9kvIT7cpAnWVjA1YaYLXSlVGA==",
"2": "FTABAQEkAgE3AyQUARgmBIAigScmBYAlTTo3BiQTAhgkBwEkCAEwCUEEYKwzNQoI9xg/J/BXjm//XmufngPSiphrXcf/ZbJxf7K3k8Xo7I77pwece9Uj8QnKrMMUdloy0sNyxbIPkTGpyjcKNQEpARgkAmAwBBS9bdraaL8JLSNzrDNJcbicl5ghHTAFFBfVqc98NGU0Xt+pmyNVXJvnhDlkGDALQKoRuyZfkC/AbH9qIIxjOhkfJB2ZS8sovhbN1fo+cvSfZXdBw255Ytf9nag0yY2maE5thqhIE4MgGV9jwQ2EPysY",
"254": 2
}
],
"0/62/1": [
{
"1": "BFhpm8fVgw4hzcuwFGwSe59XhvdUHtMntaUUbgCX0jqoaA1fjjcRYrZCA0PDImdLtZSkrUdug3S/euAVf4gvaKo=",
"2": 4939,
"3": 2,
"4": 202,
"5": "Home Assistant",
"254": 2
}
],
"0/62/2": 5,
"0/62/3": 2,
"0/62/4": [
"FTABAQEkAgE3AyQUARgmBIAigScmBYAlTTo3BiQUARgkBwEkCAEwCUEEZGswP7Cx5r/rggyFyL5F/W2s7jQv9jdnF/BtORJ5CJLHyNrJouomrpNPkewkATT25URTzakxfZ/BC2RRof3LQjcKNQEpARgkAmAwBBSwDB1/C2jgnr2LPAd9KH/07G7HSjAFFLAMHX8LaOCevYs8B30of/TsbsdKGDALQGEJod+l+O0QOa/rnbYaghE4QgquJyT9pviD3sP2+MbUXJj1br+dZLQ7CfeCKfbM8EO9iPAe1ULLveIFfHakCpAY",
"FTABAQEkAgE3AyQUARgmBIAigScmBYAlTTo3BiQUARgkBwEkCAEwCUEEDheXhz87ejqXrjJrfcRfXbv1Co84yVLcfxYr3Q4VM5Fx0JCbQDNTmqeZ/BC67MDnaqXhrPHz6tPXjC7kar6RLDcKNQEpARgkAmAwBBQX1anPfDRlNF7fqZsjVVyb54Q5ZDAFFBfVqc98NGU0Xt+pmyNVXJvnhDlkGDALQFQj3btpuzZU/TNTTTh2Q/bUE8TTOP7U4kV4J8VNyl/phUUHSfnTAnaTR/YcUehZcgPJqnW6433HWTjsa8lopVMY"
],
"0/62/5": 2,
"0/62/65533": 1,
"0/62/65532": 0,
"0/62/65531": [0, 1, 2, 3, 4, 5, 65528, 65529, 65531, 65532, 65533],
"0/62/65529": [0, 2, 4, 6, 7, 9, 10, 11],
"0/62/65528": [1, 3, 5, 8],
"0/63/0": [],
"0/63/1": [],
"0/63/2": 4,
"0/63/3": 3,
"0/63/65533": 2,
"0/63/65532": 0,
"0/63/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533],
"0/63/65529": [0, 1, 3, 4],
"0/63/65528": [2, 5],
"1/29/0": [
{
"0": 17,
"1": 1
},
{
"0": 116,
"1": 1
}
],
"1/29/1": [3, 29, 47, 84, 85, 97, 336],
"1/29/2": [],
"1/29/3": [],
"1/29/65533": 2,
"1/29/65532": 0,
"1/29/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533],
"1/29/65529": [],
"1/29/65528": [],
"1/3/0": 0,
"1/3/1": 3,
"1/3/65533": 5,
"1/3/65532": 0,
"1/3/65531": [0, 1, 65528, 65529, 65531, 65532, 65533],
"1/3/65529": [0],
"1/3/65528": [],
"1/336/0": [
{
"0": 1,
"1": 0,
"2": {
"0": {
"0": "Living room",
"1": null,
"2": 52
},
"1": null
}
},
{
"0": 2,
"1": 0,
"2": {
"0": {
"0": "Bathroom",
"1": null,
"2": 6
},
"1": null
}
},
{
"0": 3,
"1": 0,
"2": {
"0": {
"0": "Bedroom",
"1": null,
"2": 7
},
"1": null
}
},
{
"0": 4,
"1": 0,
"2": {
"0": {
"0": "Office",
"1": null,
"2": 88
},
"1": null
}
},
{
"0": 5,
"1": 0,
"2": {
"0": {
"0": "Corridor",
"1": null,
"2": 16
},
"1": null
}
},
{
"0": 6,
"1": 0,
"2": {
"0": null,
"1": {
"0": 17,
"1": 2
}
}
},
{
"0": 7,
"1": 0,
"2": {
"0": null,
"1": {
"0": 43,
"1": 2
}
}
}
],
"1/336/2": [],
"1/336/65533": 1,
"1/336/65532": 4,
"1/336/65531": [0, 1, 2, 65528, 65529, 65531, 65532, 65533],
"1/336/65529": [0],
"1/336/65528": [1],
"1/336/1": [
{
"0": 0,
"1": "Map-0"
}
],
"1/47/0": 1,
"1/47/1": 0,
"1/47/2": "Primary Battery",
"1/47/31": [],
"1/47/12": 200,
"1/47/14": 0,
"1/47/15": false,
"1/47/16": 3,
"1/47/17": true,
"1/47/26": 2,
"1/47/28": true,
"1/47/65533": 3,
"1/47/65532": 6,
"1/47/65531": [
0, 1, 2, 12, 14, 15, 16, 17, 26, 28, 31, 65528, 65529, 65531, 65532, 65533
],
"1/47/65529": [],
"1/47/65528": [],
"1/84/0": [
{
"label": "Idle",
"mode": 0,
"modeTags": [
{
"value": 16384
}
]
},
{
"label": "Cleaning",
"mode": 1,
"modeTags": [
{
"value": 16385
}
]
},
{
"label": "Mapping",
"mode": 2,
"modeTags": [
{
"value": 16386
}
]
}
],
"1/84/1": 0,
"1/84/65533": 3,
"1/84/65532": 0,
"1/84/65531": [0, 1, 65528, 65529, 65531, 65532, 65533],
"1/84/65529": [0],
"1/84/65528": [1],
"1/85/0": [
{
"label": "Quiet, Vacuum Only",
"mode": 1,
"modeTags": [
{
"value": 2
},
{
"value": 16385
}
]
},
{
"label": "Auto, Vacuum Only",
"mode": 2,
"modeTags": [
{
"value": 0
},
{
"value": 16385
}
]
},
{
"label": "Deep Clean, Vacuum Only",
"mode": 3,
"modeTags": [
{
"value": 16384
},
{
"value": 16385
}
]
},
{
"label": "Quiet, Mop Only",
"mode": 4,
"modeTags": [
{
"value": 2
},
{
"value": 16386
}
]
},
{
"label": "Auto, Mop Only",
"mode": 5,
"modeTags": [
{
"value": 0
},
{
"value": 16386
}
]
},
{
"label": "Deep Clean, Mop Only",
"mode": 6,
"modeTags": [
{
"value": 16384
},
{
"value": 16386
}
]
},
{
"label": "Quiet, Vacuum and Mop",
"mode": 7,
"modeTags": [
{
"value": 2
},
{
"value": 16385
},
{
"value": 16386
}
]
},
{
"label": "Auto, Vacuum and Mop",
"mode": 8,
"modeTags": [
{
"value": 0
},
{
"value": 16385
},
{
"value": 16386
}
]
},
{
"label": "Deep Clean, Vacuum and Mop",
"mode": 9,
"modeTags": [
{
"value": 16384
},
{
"value": 16385
},
{
"value": 16386
}
]
}
],
"1/85/1": 8,
"1/85/65533": 3,
"1/85/65532": 0,
"1/85/65531": [0, 1, 65528, 65529, 65531, 65532, 65533],
"1/85/65529": [0],
"1/85/65528": [1],
"1/97/0": null,
"1/97/1": null,
"1/97/3": [
{
"0": 0
},
{
"0": 1
},
{
"0": 2
},
{
"0": 3
},
{
"0": 64
},
{
"0": 65
},
{
"0": 66
}
],
"1/97/4": 66,
"1/97/5": {
"0": 0
},
"1/97/65533": 2,
"1/97/65532": 0,
"1/97/65531": [0, 1, 3, 4, 5, 65528, 65529, 65531, 65532, 65533],
"1/97/65529": [0, 3, 128],
"1/97/65528": [4]
},
"attribute_subscriptions": []
}

View File

@@ -3485,6 +3485,56 @@
'state': 'unknown',
})
# ---
# name: test_buttons[roborock_saros_10][button.robotic_vacuum_cleaner_identify-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'button',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'button.robotic_vacuum_cleaner_identify',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Identify',
'options': dict({
}),
'original_device_class': <ButtonDeviceClass.IDENTIFY: 'identify'>,
'original_icon': None,
'original_name': 'Identify',
'platform': 'matter',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '00000000000004D2-00000000000000CA-MatterNodeDevice-1-IdentifyButton-3-1',
'unit_of_measurement': None,
})
# ---
# name: test_buttons[roborock_saros_10][button.robotic_vacuum_cleaner_identify-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'identify',
'friendly_name': 'Robotic Vacuum Cleaner Identify',
}),
'context': <ANY>,
'entity_id': 'button.robotic_vacuum_cleaner_identify',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_buttons[secuyou_smart_lock][button.secuyou_smart_lock_identify-entry]
EntityRegistryEntrySnapshot({
'aliases': set({

View File

@@ -3995,6 +3995,78 @@
'state': 'previous',
})
# ---
# name: test_selects[roborock_saros_10][select.robotic_vacuum_cleaner_clean_mode-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'options': list([
'Quiet, Vacuum Only',
'Auto, Vacuum Only',
'Deep Clean, Vacuum Only',
'Quiet, Mop Only',
'Auto, Mop Only',
'Deep Clean, Mop Only',
'Quiet, Vacuum and Mop',
'Auto, Vacuum and Mop',
'Deep Clean, Vacuum and Mop',
]),
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'select',
'entity_category': None,
'entity_id': 'select.robotic_vacuum_cleaner_clean_mode',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Clean mode',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Clean mode',
'platform': 'matter',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'clean_mode',
'unique_id': '00000000000004D2-00000000000000CA-MatterNodeDevice-1-MatterRvcCleanMode-85-1',
'unit_of_measurement': None,
})
# ---
# name: test_selects[roborock_saros_10][select.robotic_vacuum_cleaner_clean_mode-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Robotic Vacuum Cleaner Clean mode',
'options': list([
'Quiet, Vacuum Only',
'Auto, Vacuum Only',
'Deep Clean, Vacuum Only',
'Quiet, Mop Only',
'Auto, Mop Only',
'Deep Clean, Mop Only',
'Quiet, Vacuum and Mop',
'Auto, Vacuum and Mop',
'Deep Clean, Vacuum and Mop',
]),
}),
'context': <ANY>,
'entity_id': 'select.robotic_vacuum_cleaner_clean_mode',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'Auto, Vacuum and Mop',
})
# ---
# name: test_selects[secuyou_smart_lock][select.secuyou_smart_lock_operating_mode-entry]
EntityRegistryEntrySnapshot({
'aliases': set({

View File

@@ -11473,6 +11473,283 @@
'state': '20.55',
})
# ---
# name: test_sensors[roborock_saros_10][sensor.robotic_vacuum_cleaner_battery-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.robotic_vacuum_cleaner_battery',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Battery',
'options': dict({
}),
'original_device_class': <SensorDeviceClass.BATTERY: 'battery'>,
'original_icon': None,
'original_name': 'Battery',
'platform': 'matter',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '00000000000004D2-00000000000000CA-MatterNodeDevice-1-PowerSource-47-12',
'unit_of_measurement': '%',
})
# ---
# name: test_sensors[roborock_saros_10][sensor.robotic_vacuum_cleaner_battery-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'battery',
'friendly_name': 'Robotic Vacuum Cleaner Battery',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': '%',
}),
'context': <ANY>,
'entity_id': 'sensor.robotic_vacuum_cleaner_battery',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '100',
})
# ---
# name: test_sensors[roborock_saros_10][sensor.robotic_vacuum_cleaner_battery_charge_state-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'options': list([
'not_charging',
'charging',
'full_charge',
]),
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.robotic_vacuum_cleaner_battery_charge_state',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Battery charge state',
'options': dict({
}),
'original_device_class': <SensorDeviceClass.ENUM: 'enum'>,
'original_icon': None,
'original_name': 'Battery charge state',
'platform': 'matter',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'battery_charge_state',
'unique_id': '00000000000004D2-00000000000000CA-MatterNodeDevice-1-PowerSourceBatChargeState-47-26',
'unit_of_measurement': None,
})
# ---
# name: test_sensors[roborock_saros_10][sensor.robotic_vacuum_cleaner_battery_charge_state-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'enum',
'friendly_name': 'Robotic Vacuum Cleaner Battery charge state',
'options': list([
'not_charging',
'charging',
'full_charge',
]),
}),
'context': <ANY>,
'entity_id': 'sensor.robotic_vacuum_cleaner_battery_charge_state',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'full_charge',
})
# ---
# name: test_sensors[roborock_saros_10][sensor.robotic_vacuum_cleaner_operational_error-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'options': list([
'no_error',
'unable_to_start_or_resume',
'unable_to_complete_operation',
'command_invalid_in_state',
'failed_to_find_charging_dock',
'stuck',
'dust_bin_missing',
'dust_bin_full',
'water_tank_empty',
'water_tank_missing',
'water_tank_lid_open',
'mop_cleaning_pad_missing',
'low_battery',
'cannot_reach_target_area',
'dirty_water_tank_full',
'dirty_water_tank_missing',
'wheels_jammed',
'brush_jammed',
'navigation_sensor_obscured',
]),
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.robotic_vacuum_cleaner_operational_error',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Operational error',
'options': dict({
}),
'original_device_class': <SensorDeviceClass.ENUM: 'enum'>,
'original_icon': None,
'original_name': 'Operational error',
'platform': 'matter',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'operational_error',
'unique_id': '00000000000004D2-00000000000000CA-MatterNodeDevice-1-RvcOperationalStateOperationalError-97-5',
'unit_of_measurement': None,
})
# ---
# name: test_sensors[roborock_saros_10][sensor.robotic_vacuum_cleaner_operational_error-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'enum',
'friendly_name': 'Robotic Vacuum Cleaner Operational error',
'options': list([
'no_error',
'unable_to_start_or_resume',
'unable_to_complete_operation',
'command_invalid_in_state',
'failed_to_find_charging_dock',
'stuck',
'dust_bin_missing',
'dust_bin_full',
'water_tank_empty',
'water_tank_missing',
'water_tank_lid_open',
'mop_cleaning_pad_missing',
'low_battery',
'cannot_reach_target_area',
'dirty_water_tank_full',
'dirty_water_tank_missing',
'wheels_jammed',
'brush_jammed',
'navigation_sensor_obscured',
]),
}),
'context': <ANY>,
'entity_id': 'sensor.robotic_vacuum_cleaner_operational_error',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'no_error',
})
# ---
# name: test_sensors[roborock_saros_10][sensor.robotic_vacuum_cleaner_operational_state-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'options': list([
'stopped',
'running',
'paused',
'error',
'seeking_charger',
'charging',
'docked',
]),
}),
'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.robotic_vacuum_cleaner_operational_state',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Operational state',
'options': dict({
}),
'original_device_class': <SensorDeviceClass.ENUM: 'enum'>,
'original_icon': None,
'original_name': 'Operational state',
'platform': 'matter',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'operational_state',
'unique_id': '00000000000004D2-00000000000000CA-MatterNodeDevice-1-RvcOperationalState-97-4',
'unit_of_measurement': None,
})
# ---
# name: test_sensors[roborock_saros_10][sensor.robotic_vacuum_cleaner_operational_state-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'enum',
'friendly_name': 'Robotic Vacuum Cleaner Operational state',
'options': list([
'stopped',
'running',
'paused',
'error',
'seeking_charger',
'charging',
'docked',
]),
}),
'context': <ANY>,
'entity_id': 'sensor.robotic_vacuum_cleaner_operational_state',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'docked',
})
# ---
# name: test_sensors[silabs_dishwasher][sensor.dishwasher_effective_current-entry]
EntityRegistryEntrySnapshot({
'aliases': set({

View File

@@ -149,6 +149,56 @@
'state': 'idle',
})
# ---
# name: test_vacuum[roborock_saros_10][vacuum.robotic_vacuum_cleaner-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'vacuum',
'entity_category': None,
'entity_id': 'vacuum.robotic_vacuum_cleaner',
'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': 'matter',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': <VacuumEntityFeature: 29212>,
'translation_key': 'vacuum',
'unique_id': '00000000000004D2-00000000000000CA-MatterNodeDevice-1-MatterVacuumCleaner-84-1',
'unit_of_measurement': None,
})
# ---
# name: test_vacuum[roborock_saros_10][vacuum.robotic_vacuum_cleaner-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Robotic Vacuum Cleaner',
'supported_features': <VacuumEntityFeature: 29212>,
}),
'context': <ANY>,
'entity_id': 'vacuum.robotic_vacuum_cleaner',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'docked',
})
# ---
# name: test_vacuum[switchbot_k11_plus][vacuum.k11-entry]
EntityRegistryEntrySnapshot({
'aliases': set({

View File

@@ -338,6 +338,38 @@ async def test_vacuum_get_segments(
assert segments[2] == {"id": "2290649224", "name": "My Location C", "group": None}
@pytest.mark.parametrize("node_fixture", ["roborock_saros_10"])
async def test_vacuum_get_segments_nullable_location_info(
hass: HomeAssistant,
matter_node: MatterNode,
hass_ws_client: WebSocketGenerator,
) -> None:
"""Test vacuum get_segments handles nullable ServiceArea location info."""
await async_setup_component(hass, "homeassistant", {})
assert matter_node
entity_ids = [state.entity_id for state in hass.states.async_all("vacuum")]
assert len(entity_ids) == 1
entity_id = entity_ids[0]
state = hass.states.get(entity_id)
assert state
client = await hass_ws_client(hass)
await client.send_json_auto_id(
{"type": "vacuum/get_segments", "entity_id": entity_id}
)
msg = await client.receive_json()
assert msg["success"]
assert msg["result"]["segments"] == [
{"id": "1", "name": "Living room", "group": None},
{"id": "2", "name": "Bathroom", "group": None},
{"id": "3", "name": "Bedroom", "group": None},
{"id": "4", "name": "Office", "group": None},
{"id": "5", "name": "Corridor", "group": None},
]
@pytest.mark.parametrize("node_fixture", ["mock_vacuum_cleaner"])
async def test_vacuum_clean_area(
hass: HomeAssistant,

View File

@@ -31,7 +31,7 @@ from tests.common import MockConfigEntry
type ComponentSetup = Callable[[], Awaitable[None]]
BACKUP_METADATA = {
"file_path": "backup_location/backup.tar",
"file_path": "/backup_location/backup.tar",
"metadata": {
"addons": [{"name": "Test", "slug": "test", "version": "1.0.0"}],
"backup_id": "test-backup",
@@ -60,7 +60,7 @@ USER_INPUT = {
CONF_USERNAME: "username",
CONF_PASSWORD: "password",
CONF_PRIVATE_KEY_FILE: PRIVATE_KEY_FILE_UUID,
CONF_BACKUP_LOCATION: "backup_location",
CONF_BACKUP_LOCATION: "/backup_location",
}
TEST_AGENT_ID = ulid()
@@ -118,7 +118,7 @@ def mock_config_entry(hass: HomeAssistant) -> MockConfigEntry:
CONF_USERNAME: "username",
CONF_PASSWORD: "password",
CONF_PRIVATE_KEY_FILE: str(private_key),
CONF_BACKUP_LOCATION: "backup_location",
CONF_BACKUP_LOCATION: "/backup_location",
},
)

View File

@@ -151,7 +151,7 @@ async def test_agents_list_backups_include_bad_metadata(
# Called two times, one for bad backup metadata and once for good
assert mock_ssh_connection._sftp._mock_open._mock_read.call_count == 2
assert (
"Failed to load backup metadata from file: backup_location/invalid.metadata.json. Expecting value: line 1 column 1 (char 0)"
"Failed to load backup metadata from file: /backup_location/invalid.metadata.json. Expecting value: line 1 column 1 (char 0)"
in caplog.messages
)

View File

@@ -15,6 +15,7 @@ from homeassistant.components.sftp_storage.config_flow import (
SFTPStorageMissingPasswordOrPkey,
)
from homeassistant.components.sftp_storage.const import (
CONF_BACKUP_LOCATION,
CONF_HOST,
CONF_PASSWORD,
CONF_PRIVATE_KEY_FILE,
@@ -194,3 +195,35 @@ async def test_config_entry_error(hass: HomeAssistant) -> None:
result["flow_id"], user_input
)
assert "errors" in result and result["errors"]["base"] == "key_or_password_needed"
@pytest.mark.usefixtures("current_request_with_host")
@pytest.mark.usefixtures("mock_process_uploaded_file")
@pytest.mark.usefixtures("mock_ssh_connection")
async def test_relative_backup_location_rejected(
hass: HomeAssistant,
) -> None:
"""Test that a relative backup location path is rejected."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
assert result["step_id"] == "user"
user_input = USER_INPUT.copy()
user_input[CONF_BACKUP_LOCATION] = "backups"
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input
)
assert result["type"] is FlowResultType.FORM
assert result["errors"] == {CONF_BACKUP_LOCATION: "backup_location_relative"}
# Fix the path and verify the flow succeeds
user_input[CONF_BACKUP_LOCATION] = "/backups"
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input
)
assert result["type"] is FlowResultType.CREATE_ENTRY

View File

@@ -1,15 +1,15 @@
{
"items": [
{
"deviceId": "05accb39-2017-c98b-a5ab-04a81f4d3d9a",
"deviceId": "01b28624-5907-c8bc-0325-8ad23f03a637",
"name": "[robot vacuum] Samsung",
"label": "Robot vacuum",
"label": "Robot Vacuum",
"manufacturerName": "Samsung Electronics",
"presentationId": "DA-RVC-MAP-01011",
"deviceManufacturerCode": "Samsung Electronics",
"locationId": "d31d0982-9bf9-4f0c-afd4-ad3d78842541",
"ownerId": "85532262-6537-54d9-179a-333db98dbcc0",
"roomId": "572f5713-53a9-4fb8-85fd-60515e44f1ed",
"locationId": "4647a408-2d4f-44a8-8ee6-f64328a0e480",
"ownerId": "8157695b-6c2f-4de5-98cb-bacaf51b8b2d",
"roomId": "9b0f3cf5-56b5-45fa-9bb8-81014bd63715",
"deviceTypeName": "Samsung OCF Robot Vacuum",
"components": [
{
@@ -132,10 +132,22 @@
"id": "samsungce.microphoneSettings",
"version": 1
},
{
"id": "samsungce.notification",
"version": 1
},
{
"id": "samsungce.objectDetection",
"version": 1
},
{
"id": "samsungce.softwareUpdate",
"version": 1
},
{
"id": "samsungce.sabbathMode",
"version": 1
},
{
"id": "samsungce.musicPlaylist",
"version": 1
@@ -320,24 +332,24 @@
"optional": false
}
],
"createTime": "2025-06-20T14:12:56.260Z",
"createTime": "2026-02-27T10:49:02.683Z",
"profile": {
"id": "5d345d41-a497-3fc7-84fe-eaaee50f0509"
"id": "0b3bf610-5ec4-3eeb-9e50-1038099f6904"
},
"ocf": {
"ocfDeviceType": "oic.d.robotcleaner",
"name": "[robot vacuum] Samsung",
"specVersion": "core.1.1.0",
"verticalDomainSpecVersion": "res.1.1.0,sh.1.1.0",
"verticalDomainSpecVersion": "1.2.1",
"manufacturerName": "Samsung Electronics",
"modelNumber": "JETBOT_COMBO_9X00_24K|50029141|80010b0002d8411f0100000000000000",
"modelNumber": "JETBOT_COMBOT_9X00_24K|50029141|80010a0002d8411f0100000000000000",
"platformVersion": "1.0",
"platformOS": "Tizen",
"hwVersion": "",
"firmwareVersion": "20250123.105306",
"firmwareVersion": "20260120.215157",
"vendorId": "DA-RVC-MAP-01011",
"vendorResourceClientServerVersion": "4.0.38",
"lastSignupTime": "2025-06-20T14:12:56.202953160Z",
"vendorResourceClientServerVersion": "4.0.40",
"lastSignupTime": "2026-02-27T12:08:52.022059763Z",
"transferCandidate": false,
"additionalAuthCodeRequired": false,
"modelCode": "NONE"

View File

@@ -947,19 +947,19 @@
'identifiers': set({
tuple(
'smartthings',
'05accb39-2017-c98b-a5ab-04a81f4d3d9a',
'01b28624-5907-c8bc-0325-8ad23f03a637',
),
}),
'labels': set({
}),
'manufacturer': 'Samsung Electronics',
'model': 'JETBOT_COMBO_9X00_24K',
'model': 'JETBOT_COMBOT_9X00_24K',
'model_id': None,
'name': 'Robot vacuum',
'name': 'Robot Vacuum',
'name_by_user': None,
'primary_config_entry': <ANY>,
'serial_number': None,
'sw_version': '20250123.105306',
'sw_version': '20260120.215157',
'via_device_id': None,
})
# ---

View File

@@ -1,4 +1,58 @@
# serializer version: 1
# name: test_all_entities[da_rvc_map_01011][media_player.robot_vacuum-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'media_player',
'entity_category': None,
'entity_id': 'media_player.robot_vacuum',
'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': <MediaPlayerEntityFeature: 284045>,
'translation_key': None,
'unique_id': '01b28624-5907-c8bc-0325-8ad23f03a637_main',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[da_rvc_map_01011][media_player.robot_vacuum-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Robot Vacuum',
'is_volume_muted': False,
'repeat': <RepeatMode.ALL: 'all'>,
'supported_features': <MediaPlayerEntityFeature: 284045>,
'volume_level': 0.2,
}),
'context': <ANY>,
'entity_id': 'media_player.robot_vacuum',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'on',
})
# ---
# name: test_all_entities[hw_q80r_soundbar][media_player.soundbar-entry]
EntityRegistryEntrySnapshot({
'aliases': set({

View File

@@ -452,14 +452,14 @@
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'lamp',
'unique_id': '05accb39-2017-c98b-a5ab-04a81f4d3d9a_main_samsungce.lamp_brightnessLevel_brightnessLevel',
'unique_id': '01b28624-5907-c8bc-0325-8ad23f03a637_main_samsungce.lamp_brightnessLevel_brightnessLevel',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[da_rvc_map_01011][select.robot_vacuum_lamp-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Robot vacuum Lamp',
'friendly_name': 'Robot Vacuum Lamp',
'options': list([
'on',
'off',

View File

@@ -10506,7 +10506,7 @@
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '05accb39-2017-c98b-a5ab-04a81f4d3d9a_main_battery_battery_battery',
'unique_id': '01b28624-5907-c8bc-0325-8ad23f03a637_main_battery_battery_battery',
'unit_of_measurement': '%',
})
# ---
@@ -10514,7 +10514,7 @@
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'battery',
'friendly_name': 'Robot vacuum Battery',
'friendly_name': 'Robot Vacuum Battery',
'unit_of_measurement': '%',
}),
'context': <ANY>,
@@ -10522,7 +10522,7 @@
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '59',
'state': '99',
})
# ---
# name: test_all_entities[da_rvc_map_01011][sensor.robot_vacuum_cleaning_mode-entry]
@@ -10566,7 +10566,7 @@
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'robot_cleaner_cleaning_mode',
'unique_id': '05accb39-2017-c98b-a5ab-04a81f4d3d9a_main_robotCleanerCleaningMode_robotCleanerCleaningMode_robotCleanerCleaningMode',
'unique_id': '01b28624-5907-c8bc-0325-8ad23f03a637_main_robotCleanerCleaningMode_robotCleanerCleaningMode_robotCleanerCleaningMode',
'unit_of_measurement': None,
})
# ---
@@ -10574,7 +10574,7 @@
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'enum',
'friendly_name': 'Robot vacuum Cleaning mode',
'friendly_name': 'Robot Vacuum Cleaning mode',
'options': list([
'auto',
'part',
@@ -10629,7 +10629,7 @@
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '05accb39-2017-c98b-a5ab-04a81f4d3d9a_main_powerConsumptionReport_powerConsumption_energy_meter',
'unique_id': '01b28624-5907-c8bc-0325-8ad23f03a637_main_powerConsumptionReport_powerConsumption_energy_meter',
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
})
# ---
@@ -10637,7 +10637,7 @@
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'energy',
'friendly_name': 'Robot vacuum Energy',
'friendly_name': 'Robot Vacuum Energy',
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
@@ -10646,7 +10646,7 @@
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '0.981',
'state': '0.335',
})
# ---
# name: test_all_entities[da_rvc_map_01011][sensor.robot_vacuum_energy_difference-entry]
@@ -10686,7 +10686,7 @@
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'energy_difference',
'unique_id': '05accb39-2017-c98b-a5ab-04a81f4d3d9a_main_powerConsumptionReport_powerConsumption_deltaEnergy_meter',
'unique_id': '01b28624-5907-c8bc-0325-8ad23f03a637_main_powerConsumptionReport_powerConsumption_deltaEnergy_meter',
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
})
# ---
@@ -10694,7 +10694,7 @@
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'energy',
'friendly_name': 'Robot vacuum Energy difference',
'friendly_name': 'Robot Vacuum Energy difference',
'state_class': <SensorStateClass.TOTAL: 'total'>,
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
@@ -10703,7 +10703,7 @@
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '0.021',
'state': '0.003',
})
# ---
# name: test_all_entities[da_rvc_map_01011][sensor.robot_vacuum_energy_saved-entry]
@@ -10743,7 +10743,7 @@
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'energy_saved',
'unique_id': '05accb39-2017-c98b-a5ab-04a81f4d3d9a_main_powerConsumptionReport_powerConsumption_energySaved_meter',
'unique_id': '01b28624-5907-c8bc-0325-8ad23f03a637_main_powerConsumptionReport_powerConsumption_energySaved_meter',
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
})
# ---
@@ -10751,7 +10751,7 @@
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'energy',
'friendly_name': 'Robot vacuum Energy saved',
'friendly_name': 'Robot Vacuum Energy saved',
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
@@ -10809,7 +10809,7 @@
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'robot_cleaner_movement',
'unique_id': '05accb39-2017-c98b-a5ab-04a81f4d3d9a_main_robotCleanerMovement_robotCleanerMovement_robotCleanerMovement',
'unique_id': '01b28624-5907-c8bc-0325-8ad23f03a637_main_robotCleanerMovement_robotCleanerMovement_robotCleanerMovement',
'unit_of_measurement': None,
})
# ---
@@ -10817,7 +10817,7 @@
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'enum',
'friendly_name': 'Robot vacuum Movement',
'friendly_name': 'Robot Vacuum Movement',
'options': list([
'homing',
'idle',
@@ -10837,7 +10837,7 @@
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'cleaning',
'state': 'charging',
})
# ---
# name: test_all_entities[da_rvc_map_01011][sensor.robot_vacuum_power-entry]
@@ -10877,7 +10877,7 @@
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '05accb39-2017-c98b-a5ab-04a81f4d3d9a_main_powerConsumptionReport_powerConsumption_power_meter',
'unique_id': '01b28624-5907-c8bc-0325-8ad23f03a637_main_powerConsumptionReport_powerConsumption_power_meter',
'unit_of_measurement': <UnitOfPower.WATT: 'W'>,
})
# ---
@@ -10885,9 +10885,9 @@
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'power',
'friendly_name': 'Robot vacuum Power',
'power_consumption_end': '2025-07-10T11:20:22Z',
'power_consumption_start': '2025-07-10T11:11:22Z',
'friendly_name': 'Robot Vacuum Power',
'power_consumption_end': '2026-02-27T17:02:44Z',
'power_consumption_start': '2026-02-27T16:52:44Z',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfPower.WATT: 'W'>,
}),
@@ -10936,7 +10936,7 @@
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'power_energy',
'unique_id': '05accb39-2017-c98b-a5ab-04a81f4d3d9a_main_powerConsumptionReport_powerConsumption_powerEnergy_meter',
'unique_id': '01b28624-5907-c8bc-0325-8ad23f03a637_main_powerConsumptionReport_powerConsumption_powerEnergy_meter',
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
})
# ---
@@ -10944,7 +10944,7 @@
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'energy',
'friendly_name': 'Robot vacuum Power energy',
'friendly_name': 'Robot Vacuum Power energy',
'state_class': <SensorStateClass.TOTAL: 'total'>,
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
@@ -10995,7 +10995,7 @@
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'robot_cleaner_turbo_mode',
'unique_id': '05accb39-2017-c98b-a5ab-04a81f4d3d9a_main_robotCleanerTurboMode_robotCleanerTurboMode_robotCleanerTurboMode',
'unique_id': '01b28624-5907-c8bc-0325-8ad23f03a637_main_robotCleanerTurboMode_robotCleanerTurboMode_robotCleanerTurboMode',
'unit_of_measurement': None,
})
# ---
@@ -11003,7 +11003,7 @@
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'enum',
'friendly_name': 'Robot vacuum Turbo mode',
'friendly_name': 'Robot Vacuum Turbo mode',
'options': list([
'on',
'off',
@@ -11016,7 +11016,7 @@
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'extra_silence',
'state': 'off',
})
# ---
# name: test_all_entities[da_rvc_normal_000001][sensor.robot_vacuum_battery-entry]

View File

@@ -979,55 +979,6 @@
'state': 'off',
})
# ---
# name: test_all_entities[da_rvc_map_01011][switch.robot_vacuum-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'switch',
'entity_category': None,
'entity_id': 'switch.robot_vacuum',
'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': 0,
'translation_key': None,
'unique_id': '05accb39-2017-c98b-a5ab-04a81f4d3d9a_main_switch_switch_switch',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[da_rvc_map_01011][switch.robot_vacuum-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Robot vacuum',
}),
'context': <ANY>,
'entity_id': 'switch.robot_vacuum',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'on',
})
# ---
# name: test_all_entities[da_rvc_map_01011][switch.robot_vacuum_do_not_disturb-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
@@ -1060,21 +1011,21 @@
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'do_not_disturb',
'unique_id': '05accb39-2017-c98b-a5ab-04a81f4d3d9a_main_custom.doNotDisturbMode_doNotDisturb_doNotDisturb',
'unique_id': '01b28624-5907-c8bc-0325-8ad23f03a637_main_custom.doNotDisturbMode_doNotDisturb_doNotDisturb',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[da_rvc_map_01011][switch.robot_vacuum_do_not_disturb-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Robot vacuum Do not disturb',
'friendly_name': 'Robot Vacuum Do not disturb',
}),
'context': <ANY>,
'entity_id': 'switch.robot_vacuum_do_not_disturb',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
'state': 'on',
})
# ---
# name: test_all_entities[da_rvc_normal_000001][switch.robot_vacuum-entry]

View File

@@ -31,14 +31,14 @@
'suggested_object_id': None,
'supported_features': <VacuumEntityFeature: 12308>,
'translation_key': None,
'unique_id': '05accb39-2017-c98b-a5ab-04a81f4d3d9a_main',
'unique_id': '01b28624-5907-c8bc-0325-8ad23f03a637_main',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[da_rvc_map_01011][vacuum.robot_vacuum-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Robot vacuum',
'friendly_name': 'Robot Vacuum',
'supported_features': <VacuumEntityFeature: 12308>,
}),
'context': <ANY>,

View File

@@ -68,7 +68,7 @@ async def test_vacuum_actions(
blocking=True,
)
devices.execute_device_command.assert_called_once_with(
"05accb39-2017-c98b-a5ab-04a81f4d3d9a",
"01b28624-5907-c8bc-0325-8ad23f03a637",
Capability.SAMSUNG_CE_ROBOT_CLEANER_OPERATING_STATE,
command,
MAIN,
@@ -89,7 +89,7 @@ async def test_state_update(
await trigger_update(
hass,
devices,
"05accb39-2017-c98b-a5ab-04a81f4d3d9a",
"01b28624-5907-c8bc-0325-8ad23f03a637",
Capability.SAMSUNG_CE_ROBOT_CLEANER_OPERATING_STATE,
Attribute.OPERATING_STATE,
"error",
@@ -110,13 +110,13 @@ async def test_availability(
assert hass.states.get("vacuum.robot_vacuum").state == VacuumActivity.DOCKED
await trigger_health_update(
hass, devices, "05accb39-2017-c98b-a5ab-04a81f4d3d9a", HealthStatus.OFFLINE
hass, devices, "01b28624-5907-c8bc-0325-8ad23f03a637", HealthStatus.OFFLINE
)
assert hass.states.get("vacuum.robot_vacuum").state == STATE_UNAVAILABLE
await trigger_health_update(
hass, devices, "05accb39-2017-c98b-a5ab-04a81f4d3d9a", HealthStatus.ONLINE
hass, devices, "01b28624-5907-c8bc-0325-8ad23f03a637", HealthStatus.ONLINE
)
assert hass.states.get("vacuum.robot_vacuum").state == VacuumActivity.DOCKED

View File

@@ -41,7 +41,11 @@ from tests.test_util.aiohttp import AiohttpClientMocker
return_value={"mock-domain": [{"st": "mock-st"}]},
)
async def test_ssdp_flow_dispatched_on_st(
mock_get_ssdp, hass: HomeAssistant, caplog: pytest.LogCaptureFixture, mock_flow_init
mock_get_ssdp,
hass: HomeAssistant,
caplog: pytest.LogCaptureFixture,
mock_flow_init,
aioclient_mock: AiohttpClientMocker,
) -> None:
"""Test matching based on ST."""
mock_ssdp_search_response = _ssdp_headers(
@@ -84,7 +88,11 @@ async def test_ssdp_flow_dispatched_on_st(
return_value={"mock-domain": [{"manufacturerURL": "mock-url"}]},
)
async def test_ssdp_flow_dispatched_on_manufacturer_url(
mock_get_ssdp, hass: HomeAssistant, caplog: pytest.LogCaptureFixture, mock_flow_init
mock_get_ssdp,
hass: HomeAssistant,
caplog: pytest.LogCaptureFixture,
mock_flow_init,
aioclient_mock: AiohttpClientMocker,
) -> None:
"""Test matching based on manufacturerURL."""
mock_ssdp_search_response = _ssdp_headers(
@@ -1038,6 +1046,7 @@ async def test_ssdp_rediscover(
async def test_ssdp_rediscover_no_match(
mock_get_ssdp,
hass: HomeAssistant,
aioclient_mock: AiohttpClientMocker,
mock_flow_init,
entry_domain: str,
entry_discovery_keys: dict[str, tuple[DiscoveryKey, ...]],

View File

@@ -119,8 +119,10 @@ async def test_webhook_platform_init(hass: HomeAssistant, webhook_bot) -> None:
assert hass.services.has_service(DOMAIN, SERVICE_SEND_MESSAGE) is True
@pytest.mark.usefixtures("mock_external_calls", "mock_polling_calls")
async def test_polling_platform_init(
hass: HomeAssistant, mock_polling_config_entry: MockConfigEntry
hass: HomeAssistant,
mock_polling_config_entry: MockConfigEntry,
) -> None:
"""Test initialization of the polling platform."""
mock_polling_config_entry.add_to_hass(hass)

View File

@@ -684,7 +684,7 @@ async def test_sun_renders_once_per_sensor(hass: HomeAssistant) -> None:
def _record_async_render(self, *args, **kwargs):
"""Catch async_render."""
async_render_calls.append(self.template)
return "75"
return 75
later = dt_util.utcnow()
@@ -692,7 +692,7 @@ async def test_sun_renders_once_per_sensor(hass: HomeAssistant) -> None:
hass.states.async_set("sun.sun", {"elevation": 50, "next_rising": later})
await hass.async_block_till_done()
assert hass.states.get("sensor.solar_angle").state == "75.0"
assert hass.states.get("sensor.solar_angle").state == "75"
assert hass.states.get("sensor.sunrise").state == "75"
assert len(async_render_calls) == 2
@@ -1524,7 +1524,7 @@ async def test_last_reset(hass: HomeAssistant, expected: str) -> None:
state = hass.states.get(TEST_SENSOR.entity_id)
assert state is not None
assert state.state == "0.0"
assert state.state == "0"
assert state.attributes["state_class"] == "total"
assert state.attributes["last_reset"] == expected
@@ -1553,7 +1553,7 @@ async def test_invalid_last_reset(
state = hass.states.get(TEST_SENSOR.entity_id)
assert state is not None
assert state.state == "0.0"
assert state.state == "0"
assert state.attributes.get("last_reset") is None
err = "Received invalid sensor last_reset: not a datetime for entity"
@@ -1993,3 +1993,47 @@ async def test_numeric_sensor_recovers_from_exception(hass: HomeAssistant) -> No
):
await async_trigger(hass, TEST_STATE_SENSOR, set_state)
assert hass.states.get(TEST_SENSOR.entity_id).state == expected_state
@pytest.mark.parametrize(
("count", "config"),
[
(
1,
{
"device_class": "temperature",
"state_class": "measurement",
"unit_of_measurement": "°C",
},
)
],
)
@pytest.mark.parametrize(
"style", [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER]
)
@pytest.mark.parametrize(
("state_template", "expected_state"),
[
("{{ '1.0' }}", "1.0"),
("{{ '1' }}", "1"),
("{{ 1.0 }}", "1.0"),
("{{ 1 }}", "1"),
("{{ '0.0' }}", "0.0"),
("{{ '0' }}", "0"),
("{{ 0.0 }}", "0.0"),
("{{ 0 }}", "0"),
("{{ '10021452' }}", "10021452"),
("{{ 10021452 }}", "10021452"),
("{{ '1002.1452' }}", "1002.1452"),
("{{ 1002.1452 }}", "1002.1452"),
("{{ True }}", STATE_UNKNOWN),
("{{ False }}", STATE_UNKNOWN),
],
)
@pytest.mark.usefixtures("setup_state_sensor")
async def test_numeric_sensor_int_float(
hass: HomeAssistant, expected_state: str
) -> None:
"""Test sensor properly stores int or float for state."""
await async_trigger(hass, TEST_STATE_SENSOR, "anything")
assert hass.states.get(TEST_SENSOR.entity_id).state == expected_state

View File

@@ -545,7 +545,12 @@ async def test_unlink_devices(
}
assert device_entries[0].identifiers == set(test_identifiers)
with patch("homeassistant.components.tplink.CONF_CONFIG_ENTRY_MINOR_VERSION", 3):
with (
patch("homeassistant.components.tplink.CONF_CONFIG_ENTRY_MINOR_VERSION", 3),
_patch_discovery(),
_patch_single_discovery(),
_patch_connect(),
):
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
@@ -596,6 +601,8 @@ async def test_move_credentials_hash(
patch("homeassistant.components.tplink.Device.connect", new=_connect),
patch("homeassistant.components.tplink.PLATFORMS", []),
patch("homeassistant.components.tplink.CONF_CONFIG_ENTRY_MINOR_VERSION", 4),
_patch_discovery(),
_patch_single_discovery(),
):
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
@@ -640,6 +647,8 @@ async def test_move_credentials_hash_auth_error(
),
patch("homeassistant.components.tplink.PLATFORMS", []),
patch("homeassistant.components.tplink.CONF_CONFIG_ENTRY_MINOR_VERSION", 4),
_patch_discovery(),
_patch_single_discovery(),
):
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
@@ -682,6 +691,8 @@ async def test_move_credentials_hash_other_error(
),
patch("homeassistant.components.tplink.PLATFORMS", []),
patch("homeassistant.components.tplink.CONF_CONFIG_ENTRY_MINOR_VERSION", 4),
_patch_discovery(),
_patch_single_discovery(),
):
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
@@ -717,6 +728,8 @@ async def test_credentials_hash(
with (
patch("homeassistant.components.tplink.PLATFORMS", []),
patch("homeassistant.components.tplink.Device.connect", new=_connect),
_patch_discovery(),
_patch_single_discovery(),
):
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
@@ -753,6 +766,8 @@ async def test_credentials_hash_auth_error(
"homeassistant.components.tplink.Device.connect",
side_effect=AuthenticationError,
) as connect_mock,
_patch_discovery(),
_patch_single_discovery(),
):
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
@@ -782,6 +797,7 @@ async def test_credentials_hash_auth_error(
async def test_migrate_remove_device_config(
hass: HomeAssistant,
mock_connect: AsyncMock,
mock_discovery: AsyncMock,
caplog: pytest.LogCaptureFixture,
device_config: DeviceConfig,
expected_entry_data: dict[str, Any],

View File

@@ -33,6 +33,7 @@ from homeassistant.components.update import (
from homeassistant.components.zha.helpers import (
ZHADeviceProxy,
ZHAGatewayProxy,
get_zha_data,
get_zha_gateway,
get_zha_gateway_proxy,
)
@@ -55,6 +56,7 @@ from homeassistant.setup import async_setup_component
from .common import find_entity_id, update_attribute_cache
from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_PROFILE, SIG_EP_TYPE
from tests.common import MockConfigEntry
from tests.typing import WebSocketGenerator
@@ -267,6 +269,43 @@ async def test_firmware_update_notification_from_service_call(
)
async def test_firmware_update_poll_after_reload(
hass: HomeAssistant,
setup_zha: Callable[..., Coroutine[None]],
config_entry: MockConfigEntry,
zigpy_device_mock: Callable[..., Device],
) -> None:
"""Test polling a ZHA update entity still works after reloading ZHA."""
await setup_zha()
await async_setup_component(hass, HA_DOMAIN, {})
zha_data = get_zha_data(hass)
coordinator_before = zha_data.update_coordinator
assert coordinator_before is not None
assert await hass.config_entries.async_reload(config_entry.entry_id)
await hass.async_block_till_done()
coordinator_after = get_zha_data(hass).update_coordinator
assert coordinator_after is not None
assert coordinator_after is not coordinator_before
zha_device, _, _, _ = await setup_test_data(hass, zigpy_device_mock)
entity_id = find_entity_id(Platform.UPDATE, zha_device, hass)
assert entity_id is not None
with patch("zigpy.ota.OTA.broadcast_notify") as mock_broadcast_notify:
await hass.services.async_call(
HA_DOMAIN,
SERVICE_UPDATE_ENTITY,
service_data={ATTR_ENTITY_ID: entity_id},
blocking=True,
)
assert mock_broadcast_notify.await_count == 1
assert mock_broadcast_notify.call_args_list[0] == call(jitter=100)
def make_packet(zigpy_device, cluster, cmd_name: str, **kwargs):
"""Make a zigpy packet."""
req_hdr, req_cmd = cluster._create_request(