Compare commits

...

22 Commits

Author SHA1 Message Date
Franck Nijhof
28027ddca4 2026.2.1 (#162450) 2026-02-06 22:44:07 +01:00
Franck Nijhof
fe0d7b3cca Bump version to 2026.2.1 2026-02-06 20:49:26 +00:00
jameson_uk
0dcc4e9527 dep: bump aioamazondevices to 11.1.3 (#162437) 2026-02-06 20:47:38 +00:00
Artur Pragacz
b13b189703 Make bad entity ID detection more lenient (#162425) 2026-02-06 20:47:37 +00:00
epenet
150829f599 Fix invalid yardian snaphots (#162422) 2026-02-06 20:47:36 +00:00
Joost Lekkerkerker
57dd9d9c23 Remove double unit of measurement for yardian (#162412) 2026-02-06 20:47:34 +00:00
Sab44
e2056cb12c Bump librehardwaremonitor-api to version 1.9.1 (#162409) 2026-02-06 20:47:33 +00:00
Joost Lekkerkerker
fa2c8992cf Remove entity id overwrite for ambient station (#162403) 2026-02-06 20:47:32 +00:00
Matt Zimmerman
ddf5c7fe3a Add missing config flow strings to SmartTub (#162375)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-06 20:47:31 +00:00
Matt Zimmerman
7034ed6d3f Bump python-smarttub to 0.0.47 (#162367)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-06 20:47:29 +00:00
Aaron Godfrey
9015b53c1b Fix conversion of data for todo.* actions (#162366) 2026-02-06 20:47:28 +00:00
Jordan Harvey
1cfa6561f7 Update pynintendoparental requirement to version 2.3.2.1 (#162362) 2026-02-06 20:47:27 +00:00
Shay Levy
eead02dcca Fix Shelly Linkedgo Thermostat status update (#162339) 2026-02-06 20:47:26 +00:00
Arie Catsman
456e51a221 Bump pyenphase to 2.4.5 (#162324) 2026-02-06 20:47:25 +00:00
Luo Chen
5d984ce186 Fix unicode escaping in MCP server tool response (#162319)
Co-authored-by: Franck Nijhof <git@frenck.dev>
Co-authored-by: Franck Nijhof <frenck@frenck.nl>
Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
2026-02-06 20:47:24 +00:00
Oliver
61f45489ac Add mapping for stopped state to denonavr media player (#162283) 2026-02-06 20:47:23 +00:00
Tomás Correia
f72c643b38 Fix multipart upload to use consistent part sizes for R2/S3 (#162278) 2026-02-06 20:47:22 +00:00
Oliver
27bc26e886 Bump denonavr to 1.3.2 (#162271) 2026-02-06 20:47:20 +00:00
Thomas55555
0e9f03cbc1 Bump google_air_quality_api to 3.0.1 (#162233) 2026-02-06 20:47:19 +00:00
David Bonnes
9480c33fb0 Bump evohome-async to 1.1.3 (#162232) 2026-02-06 20:47:18 +00:00
Jonathan
3e6b8663e8 Fix device_class of backup reserve sensor (#161178) 2026-02-06 20:47:17 +00:00
epenet
1c69a83793 Fix redundant off preset in Tuya climate (#161040) 2026-02-06 20:47:16 +00:00
35 changed files with 353 additions and 155 deletions

View File

@@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["aioamazondevices"],
"quality_scale": "platinum",
"requirements": ["aioamazondevices==11.1.1"]
"requirements": ["aioamazondevices==11.1.3"]
}

View File

@@ -26,10 +26,9 @@ from homeassistant.const import (
UnitOfVolumetricFlux,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity import EntityDescription
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import AmbientStation, AmbientStationConfigEntry
from . import AmbientStationConfigEntry
from .const import ATTR_LAST_DATA, TYPE_SOLARRADIATION, TYPE_SOLARRADIATION_LX
from .entity import AmbientWeatherEntity
@@ -683,22 +682,6 @@ async def async_setup_entry(
class AmbientWeatherSensor(AmbientWeatherEntity, SensorEntity):
"""Define an Ambient sensor."""
def __init__(
self,
ambient: AmbientStation,
mac_address: str,
station_name: str,
description: EntityDescription,
) -> None:
"""Initialize the sensor."""
super().__init__(ambient, mac_address, station_name, description)
if description.key == TYPE_SOLARRADIATION_LX:
# Since TYPE_SOLARRADIATION and TYPE_SOLARRADIATION_LX will have the same
# name in the UI, we influence the entity ID of TYPE_SOLARRADIATION_LX here
# to differentiate them:
self.entity_id = f"sensor.{station_name}_solar_rad_lx"
@callback
def update_from_latest_data(self) -> None:
"""Fetch new state data for the sensor."""

View File

@@ -196,44 +196,46 @@ class R2BackupAgent(BackupAgent):
)
upload_id = multipart_upload["UploadId"]
try:
parts = []
parts: list[dict[str, Any]] = []
part_number = 1
buffer_size = 0 # bytes
buffer: list[bytes] = []
buffer = bytearray() # bytes buffer to store the data
stream = await open_stream()
async for chunk in stream:
buffer_size += len(chunk)
buffer.append(chunk)
buffer.extend(chunk)
# upload parts of exactly MULTIPART_MIN_PART_SIZE_BYTES to ensure
# all non-trailing parts have the same size (required by S3/R2)
while len(buffer) >= MULTIPART_MIN_PART_SIZE_BYTES:
part_data = bytes(buffer[:MULTIPART_MIN_PART_SIZE_BYTES])
del buffer[:MULTIPART_MIN_PART_SIZE_BYTES]
# If buffer size meets minimum part size, upload it as a part
if buffer_size >= MULTIPART_MIN_PART_SIZE_BYTES:
_LOGGER.debug(
"Uploading part number %d, size %d", part_number, buffer_size
"Uploading part number %d, size %d",
part_number,
len(part_data),
)
part = await self._client.upload_part(
Bucket=self._bucket,
Key=self._with_prefix(tar_filename),
PartNumber=part_number,
UploadId=upload_id,
Body=b"".join(buffer),
Body=part_data,
)
parts.append({"PartNumber": part_number, "ETag": part["ETag"]})
part_number += 1
buffer_size = 0
buffer = []
# Upload the final buffer as the last part (no minimum size requirement)
if buffer:
_LOGGER.debug(
"Uploading final part number %d, size %d", part_number, buffer_size
"Uploading final part number %d, size %d", part_number, len(buffer)
)
part = await self._client.upload_part(
Bucket=self._bucket,
Key=self._with_prefix(tar_filename),
PartNumber=part_number,
UploadId=upload_id,
Body=b"".join(buffer),
Body=bytes(buffer),
)
parts.append({"PartNumber": part_number, "ETag": part["ETag"]})

View File

@@ -7,7 +7,7 @@
"integration_type": "device",
"iot_class": "local_push",
"loggers": ["denonavr"],
"requirements": ["denonavr==1.3.1"],
"requirements": ["denonavr==1.3.2"],
"ssdp": [
{
"deviceType": "urn:schemas-upnp-org:device:MediaRenderer:1",

View File

@@ -17,6 +17,7 @@ from denonavr.const import (
STATE_ON,
STATE_PAUSED,
STATE_PLAYING,
STATE_STOPPED,
)
from denonavr.exceptions import (
AvrCommandError,
@@ -103,6 +104,7 @@ DENON_STATE_MAPPING = {
STATE_OFF: MediaPlayerState.OFF,
STATE_PLAYING: MediaPlayerState.PLAYING,
STATE_PAUSED: MediaPlayerState.PAUSED,
STATE_STOPPED: MediaPlayerState.IDLE,
}

View File

@@ -8,7 +8,7 @@
"iot_class": "local_polling",
"loggers": ["pyenphase"],
"quality_scale": "platinum",
"requirements": ["pyenphase==2.4.3"],
"requirements": ["pyenphase==2.4.5"],
"zeroconf": [
{
"type": "_enphase-envoy._tcp.local."

View File

@@ -4,7 +4,7 @@
"codeowners": ["@zxdavb"],
"documentation": "https://www.home-assistant.io/integrations/evohome",
"iot_class": "cloud_polling",
"loggers": ["evohome", "evohomeasync", "evohomeasync2"],
"loggers": ["evohomeasync", "evohomeasync2"],
"quality_scale": "legacy",
"requirements": ["evohome-async==1.0.6"]
"requirements": ["evohome-async==1.1.3"]
}

View File

@@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["google_air_quality_api"],
"quality_scale": "bronze",
"requirements": ["google_air_quality_api==3.0.0"]
"requirements": ["google_air_quality_api==3.0.1"]
}

View File

@@ -7,5 +7,5 @@
"integration_type": "device",
"iot_class": "local_polling",
"quality_scale": "silver",
"requirements": ["librehardwaremonitor-api==1.8.4"]
"requirements": ["librehardwaremonitor-api==1.9.1"]
}

View File

@@ -110,7 +110,7 @@ async def create_server(
return [
types.TextContent(
type="text",
text=json.dumps(tool_response),
text=json.dumps(tool_response, ensure_ascii=False),
)
]

View File

@@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["pynintendoauth", "pynintendoparental"],
"quality_scale": "bronze",
"requirements": ["pynintendoauth==1.0.2", "pynintendoparental==2.3.2"]
"requirements": ["pynintendoauth==1.0.2", "pynintendoparental==2.3.2.1"]
}

View File

@@ -104,7 +104,6 @@ class RpcLinkedgoThermostatClimate(ShellyRpcAttributeEntity, ClimateEntity):
)
config = coordinator.device.config
self._status = coordinator.device.status
self._attr_min_temp = config[key]["min"]
self._attr_max_temp = config[key]["max"]
@@ -142,6 +141,11 @@ class RpcLinkedgoThermostatClimate(ShellyRpcAttributeEntity, ClimateEntity):
THERMOSTAT_TO_HA_MODE[mode] for mode in modes
]
@property
def _status(self) -> dict[str, Any]:
"""Return the full device status."""
return self.coordinator.device.status
@property
def current_humidity(self) -> float | None:
"""Return the current humidity."""

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/smarttub",
"iot_class": "cloud_polling",
"loggers": ["smarttub"],
"requirements": ["python-smarttub==0.0.46"]
"requirements": ["python-smarttub==0.0.47"]
}

View File

@@ -9,6 +9,14 @@
},
"step": {
"reauth_confirm": {
"data": {
"email": "[%key:common::config_flow::data::email%]",
"password": "[%key:common::config_flow::data::password%]"
},
"data_description": {
"email": "[%key:component::smarttub::config::step::user::data_description::email%]",
"password": "[%key:component::smarttub::config::step::user::data_description::password%]"
},
"description": "The SmartTub integration needs to re-authenticate your account",
"title": "[%key:common::config_flow::title::reauth%]"
},
@@ -17,6 +25,10 @@
"email": "[%key:common::config_flow::data::email%]",
"password": "[%key:common::config_flow::data::password%]"
},
"data_description": {
"email": "The email address associated with your SmartTub account",
"password": "The password for your SmartTub account"
},
"description": "Enter your SmartTub email address and password to log in",
"title": "Login"
}

View File

@@ -436,7 +436,6 @@ ENERGY_INFO_DESCRIPTIONS: tuple[SensorEntityDescription, ...] = (
SensorEntityDescription(
key="vpp_backup_reserve_percent",
entity_category=EntityCategory.DIAGNOSTIC,
device_class=SensorDeviceClass.BATTERY,
native_unit_of_measurement=PERCENTAGE,
),
SensorEntityDescription(key="version"),

View File

@@ -45,9 +45,9 @@ def _task_api_data(item: TodoItem, api_data: Task | None = None) -> dict[str, An
}
if due := item.due:
if isinstance(due, datetime.datetime):
item_data["due_datetime"] = due.isoformat()
item_data["due_datetime"] = due
else:
item_data["due_date"] = due.isoformat()
item_data["due_date"] = due
# In order to not lose any recurrence metadata for the task, we need to
# ensure that we send the `due_string` param if the task has it set.
# NOTE: It's ok to send stale data for non-recurring tasks. Any provided

View File

@@ -48,6 +48,7 @@ TUYA_HVAC_TO_HA = {
"heat": HVACMode.HEAT,
"hot": HVACMode.HEAT,
"manual": HVACMode.HEAT_COOL,
"off": HVACMode.OFF,
"wet": HVACMode.DRY,
"wind": HVACMode.FAN_ONLY,
}
@@ -442,7 +443,9 @@ class TuyaClimateEntity(TuyaEntity, ClimateEntity):
if hvac_mode_wrapper:
self._attr_hvac_modes = [HVACMode.OFF]
for mode in hvac_mode_wrapper.options:
self._attr_hvac_modes.append(HVACMode(mode))
if mode != HVACMode.OFF:
# OFF is always added first
self._attr_hvac_modes.append(HVACMode(mode))
elif switch_wrapper:
self._attr_hvac_modes = [

View File

@@ -61,7 +61,6 @@ SENSOR_DESCRIPTIONS: tuple[YardianSensorEntityDescription, ...] = (
YardianSensorEntityDescription(
key="active_zone_count",
translation_key="active_zone_count",
native_unit_of_measurement="zones",
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda coordinator: len(coordinator.data.active_zones),
),

View File

@@ -38,7 +38,7 @@
"sensor": {
"active_zone_count": {
"name": "Active zones",
"unit_of_measurement": "Zones"
"unit_of_measurement": "zones"
},
"rain_delay": {
"name": "Rain delay"

View File

@@ -17,7 +17,7 @@ if TYPE_CHECKING:
APPLICATION_NAME: Final = "HomeAssistant"
MAJOR_VERSION: Final = 2026
MINOR_VERSION: Final = 2
PATCH_VERSION: Final = "0"
PATCH_VERSION: Final = "1"
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 13, 2)

View File

@@ -42,6 +42,7 @@ from . import device_registry as dr, entity_registry as er, service, translation
from .deprecation import deprecated_function
from .entity_registry import EntityRegistry, RegistryEntryDisabler, RegistryEntryHider
from .event import async_call_later
from .frame import report_usage
from .issue_registry import IssueSeverity, async_create_issue
from .typing import UNDEFINED, ConfigType, DiscoveryInfoType, VolDictType, VolSchemaType
@@ -822,13 +823,28 @@ class EntityPlatform:
# An entity may suggest the entity_id by setting entity_id itself
if not hasattr(entity, "internal_integration_suggested_object_id"):
if entity.entity_id is not None and not valid_entity_id(entity.entity_id):
if entity.unique_id is not None:
report_usage(
f"sets an invalid entity ID: '{entity.entity_id}'. "
"In most cases, entities should not set entity_id,"
" but if they do, it should be a valid entity ID.",
integration_domain=self.platform_name,
breaks_in_ha_version="2027.2.0",
)
else:
entity.add_to_platform_abort()
raise HomeAssistantError(f"Invalid entity ID: {entity.entity_id}")
try:
entity.internal_integration_suggested_object_id = (
split_entity_id(entity.entity_id)[1]
if entity.entity_id is not None
else None
)
except ValueError:
entity.add_to_platform_abort()
raise HomeAssistantError(f"Invalid entity ID: {entity.entity_id}")
entity.internal_integration_suggested_object_id = (
split_entity_id(entity.entity_id)[1]
if entity.entity_id is not None
else None
)
raise HomeAssistantError(
f"Invalid entity ID: {entity.entity_id}"
) from None
# Get entity_id from unique ID registration
if entity.unique_id is not None:

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "homeassistant"
version = "2026.2.0"
version = "2026.2.1"
license = "Apache-2.0"
license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"]
description = "Open-source home automation platform running on Python 3."

16
requirements_all.txt generated
View File

@@ -190,7 +190,7 @@ aioairzone-cloud==0.7.2
aioairzone==1.0.5
# homeassistant.components.alexa_devices
aioamazondevices==11.1.1
aioamazondevices==11.1.3
# homeassistant.components.ambient_network
# homeassistant.components.ambient_station
@@ -797,7 +797,7 @@ deluge-client==1.10.2
demetriek==1.3.0
# homeassistant.components.denonavr
denonavr==1.3.1
denonavr==1.3.2
# homeassistant.components.devialet
devialet==1.5.7
@@ -938,7 +938,7 @@ eufylife-ble-client==0.1.8
# evdev==1.6.1
# homeassistant.components.evohome
evohome-async==1.0.6
evohome-async==1.1.3
# homeassistant.components.bryant_evolution
evolutionhttp==0.0.18
@@ -1105,7 +1105,7 @@ google-nest-sdm==9.1.2
google-photos-library-api==0.12.1
# homeassistant.components.google_air_quality
google_air_quality_api==3.0.0
google_air_quality_api==3.0.1
# homeassistant.components.slide
# homeassistant.components.slide_local
@@ -1397,7 +1397,7 @@ libpyfoscamcgi==0.0.9
libpyvivotek==0.6.1
# homeassistant.components.libre_hardware_monitor
librehardwaremonitor-api==1.8.4
librehardwaremonitor-api==1.9.1
# homeassistant.components.mikrotik
librouteros==3.2.0
@@ -2032,7 +2032,7 @@ pyegps==0.2.5
pyemoncms==0.1.3
# homeassistant.components.enphase_envoy
pyenphase==2.4.3
pyenphase==2.4.5
# homeassistant.components.envisalink
pyenvisalink==4.7
@@ -2251,7 +2251,7 @@ pynina==1.0.2
pynintendoauth==1.0.2
# homeassistant.components.nintendo_parental_controls
pynintendoparental==2.3.2
pynintendoparental==2.3.2.1
# homeassistant.components.nobo_hub
pynobo==1.8.1
@@ -2597,7 +2597,7 @@ python-ripple-api==0.0.3
python-roborock==4.8.0
# homeassistant.components.smarttub
python-smarttub==0.0.46
python-smarttub==0.0.47
# homeassistant.components.snoo
python-snoo==0.8.3

View File

@@ -181,7 +181,7 @@ aioairzone-cloud==0.7.2
aioairzone==1.0.5
# homeassistant.components.alexa_devices
aioamazondevices==11.1.1
aioamazondevices==11.1.3
# homeassistant.components.ambient_network
# homeassistant.components.ambient_station
@@ -706,7 +706,7 @@ deluge-client==1.10.2
demetriek==1.3.0
# homeassistant.components.denonavr
denonavr==1.3.1
denonavr==1.3.2
# homeassistant.components.devialet
devialet==1.5.7
@@ -826,7 +826,7 @@ eternalegypt==0.0.18
eufylife-ble-client==0.1.8
# homeassistant.components.evohome
evohome-async==1.0.6
evohome-async==1.1.3
# homeassistant.components.bryant_evolution
evolutionhttp==0.0.18
@@ -981,7 +981,7 @@ google-nest-sdm==9.1.2
google-photos-library-api==0.12.1
# homeassistant.components.google_air_quality
google_air_quality_api==3.0.0
google_air_quality_api==3.0.1
# homeassistant.components.slide
# homeassistant.components.slide_local
@@ -1228,7 +1228,7 @@ libpyfoscamcgi==0.0.9
libpyvivotek==0.6.1
# homeassistant.components.libre_hardware_monitor
librehardwaremonitor-api==1.8.4
librehardwaremonitor-api==1.9.1
# homeassistant.components.mikrotik
librouteros==3.2.0
@@ -1730,7 +1730,7 @@ pyegps==0.2.5
pyemoncms==0.1.3
# homeassistant.components.enphase_envoy
pyenphase==2.4.3
pyenphase==2.4.5
# homeassistant.components.everlights
pyeverlights==0.1.0
@@ -1907,7 +1907,7 @@ pynina==1.0.2
pynintendoauth==1.0.2
# homeassistant.components.nintendo_parental_controls
pynintendoparental==2.3.2
pynintendoparental==2.3.2.1
# homeassistant.components.nobo_hub
pynobo==1.8.1
@@ -2187,7 +2187,7 @@ python-rabbitair==0.0.8
python-roborock==4.8.0
# homeassistant.components.smarttub
python-smarttub==0.0.46
python-smarttub==0.0.47
# homeassistant.components.snoo
python-snoo==0.8.3

View File

@@ -367,6 +367,57 @@ async def test_agents_upload_network_failure(
assert "Upload failed for cloudflare_r2" in caplog.text
async def test_multipart_upload_consistent_part_sizes(
hass: HomeAssistant,
mock_client: MagicMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test that multipart upload uses consistent part sizes.
S3/R2 requires all non-trailing parts to have the same size. This test
verifies that varying chunk sizes still result in consistent part sizes.
"""
agent = R2BackupAgent(hass, mock_config_entry)
# simulate varying chunk data sizes
# total data: 12 + 12 + 10 + 12 + 5 = 51 MiB
chunk_sizes = [12, 12, 10, 12, 5] # in units of 1 MiB
mib = 2**20
async def mock_stream():
for size in chunk_sizes:
yield b"x" * (size * mib)
async def open_stream():
return mock_stream()
# Record the sizes of each uploaded part
uploaded_part_sizes: list[int] = []
async def record_upload_part(**kwargs):
body = kwargs.get("Body", b"")
uploaded_part_sizes.append(len(body))
return {"ETag": f"etag-{len(uploaded_part_sizes)}"}
mock_client.upload_part.side_effect = record_upload_part
await agent._upload_multipart("test.tar", open_stream)
# Verify that all non-trailing parts have the same size
assert len(uploaded_part_sizes) >= 2, "Expected at least 2 parts"
non_trailing_parts = uploaded_part_sizes[:-1]
assert all(size == MULTIPART_MIN_PART_SIZE_BYTES for size in non_trailing_parts), (
f"All non-trailing parts should be {MULTIPART_MIN_PART_SIZE_BYTES} bytes, got {non_trailing_parts}"
)
# Verify the trailing part contains the remainder
total_data = sum(chunk_sizes) * mib
expected_trailing = total_data % MULTIPART_MIN_PART_SIZE_BYTES
if expected_trailing == 0:
expected_trailing = MULTIPART_MIN_PART_SIZE_BYTES
assert uploaded_part_sizes[-1] == expected_trailing
async def test_agents_download(
hass_client: ClientSessionGenerator,
mock_client: MagicMock,

View File

@@ -168,7 +168,7 @@ async def setup_evohome(
"evohomeasync2.auth.CredentialsManagerBase._post_request",
mock_post_request(install),
),
patch("evohome.auth.AbstractAuth._make_request", mock_make_request(install)),
patch("_evohome.auth.AbstractAuth._make_request", mock_make_request(install)),
):
evo: EvohomeClient | None = None

View File

@@ -31,13 +31,9 @@ _MSG_USR = (
"special characters accepted via the vendor's website are not valid here."
)
LOG_HINT_429_CREDS = ("evohome.credentials", logging.ERROR, _MSG_429)
LOG_HINT_OTH_CREDS = ("evohome.credentials", logging.ERROR, _MSG_OTH)
LOG_HINT_USR_CREDS = ("evohome.credentials", logging.ERROR, _MSG_USR)
LOG_HINT_429_AUTH = ("evohome.auth", logging.ERROR, _MSG_429)
LOG_HINT_OTH_AUTH = ("evohome.auth", logging.ERROR, _MSG_OTH)
LOG_HINT_USR_AUTH = ("evohome.auth", logging.ERROR, _MSG_USR)
LOG_HINT_429_AUTH = ("evohomeasync2.auth", logging.ERROR, _MSG_429)
LOG_HINT_OTH_AUTH = ("evohomeasync2.auth", logging.ERROR, _MSG_OTH)
LOG_HINT_USR_AUTH = ("evohomeasync2.auth", logging.ERROR, _MSG_USR)
LOG_FAIL_CONNECTION = (
"homeassistant.components.evohome",
@@ -110,10 +106,10 @@ EXC_BAD_GATEWAY = aiohttp.ClientResponseError(
)
AUTHENTICATION_TESTS: dict[Exception, list] = {
EXC_BAD_CONNECTION: [LOG_HINT_OTH_CREDS, LOG_FAIL_CONNECTION, LOG_SETUP_FAILED],
EXC_BAD_CREDENTIALS: [LOG_HINT_USR_CREDS, LOG_FAIL_CREDENTIALS, LOG_SETUP_FAILED],
EXC_BAD_GATEWAY: [LOG_HINT_OTH_CREDS, LOG_FAIL_GATEWAY, LOG_SETUP_FAILED],
EXC_TOO_MANY_REQUESTS: [LOG_HINT_429_CREDS, LOG_FAIL_TOO_MANY, LOG_SETUP_FAILED],
EXC_BAD_CONNECTION: [LOG_HINT_OTH_AUTH, LOG_FAIL_CONNECTION, LOG_SETUP_FAILED],
EXC_BAD_CREDENTIALS: [LOG_HINT_USR_AUTH, LOG_FAIL_CREDENTIALS, LOG_SETUP_FAILED],
EXC_BAD_GATEWAY: [LOG_HINT_OTH_AUTH, LOG_FAIL_GATEWAY, LOG_SETUP_FAILED],
EXC_TOO_MANY_REQUESTS: [LOG_HINT_429_AUTH, LOG_FAIL_TOO_MANY, LOG_SETUP_FAILED],
}
CLIENT_REQUEST_TESTS: dict[Exception, list] = {
@@ -137,7 +133,8 @@ async def test_authentication_failure_v2(
with (
patch(
"evohome.credentials.CredentialsManagerBase._request", side_effect=exception
"_evohome.credentials.CredentialsManagerBase._request",
side_effect=exception,
),
caplog.at_level(logging.WARNING),
):
@@ -165,7 +162,7 @@ async def test_client_request_failure_v2(
"evohomeasync2.auth.CredentialsManagerBase._post_request",
mock_post_request("default"),
),
patch("evohome.auth.AbstractAuth._request", side_effect=exception),
patch("_evohome.auth.AbstractAuth._request", side_effect=exception),
caplog.at_level(logging.WARNING),
):
result = await async_setup_component(hass, DOMAIN, {DOMAIN: config})

View File

@@ -439,7 +439,7 @@
'state': '5.030',
})
# ---
# name: test_sensors_are_created[sensor.gaming_pc_msi_mag_b650m_mortar_wifi_ms_7d76_cpu_fan_fan-entry]
# name: test_sensors_are_created[sensor.gaming_pc_msi_mag_b650m_mortar_wifi_ms_7d76_cpu_fan_speed-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
@@ -454,7 +454,7 @@
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.gaming_pc_msi_mag_b650m_mortar_wifi_ms_7d76_cpu_fan_fan',
'entity_id': 'sensor.gaming_pc_msi_mag_b650m_mortar_wifi_ms_7d76_cpu_fan_speed',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
@@ -462,12 +462,12 @@
'labels': set({
}),
'name': None,
'object_id_base': 'CPU Fan Fan',
'object_id_base': 'CPU Fan Speed',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'CPU Fan Fan',
'original_name': 'CPU Fan Speed',
'platform': 'libre_hardware_monitor',
'previous_unique_id': None,
'suggested_object_id': None,
@@ -477,17 +477,17 @@
'unit_of_measurement': 'RPM',
})
# ---
# name: test_sensors_are_created[sensor.gaming_pc_msi_mag_b650m_mortar_wifi_ms_7d76_cpu_fan_fan-state]
# name: test_sensors_are_created[sensor.gaming_pc_msi_mag_b650m_mortar_wifi_ms_7d76_cpu_fan_speed-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': '[GAMING-PC] MSI MAG B650M MORTAR WIFI (MS-7D76) CPU Fan Fan',
'friendly_name': '[GAMING-PC] MSI MAG B650M MORTAR WIFI (MS-7D76) CPU Fan Speed',
'max_value': '0',
'min_value': '0',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'RPM',
}),
'context': <ANY>,
'entity_id': 'sensor.gaming_pc_msi_mag_b650m_mortar_wifi_ms_7d76_cpu_fan_fan',
'entity_id': 'sensor.gaming_pc_msi_mag_b650m_mortar_wifi_ms_7d76_cpu_fan_speed',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
@@ -549,7 +549,7 @@
'state': '55.0',
})
# ---
# name: test_sensors_are_created[sensor.gaming_pc_msi_mag_b650m_mortar_wifi_ms_7d76_pump_fan_fan-entry]
# name: test_sensors_are_created[sensor.gaming_pc_msi_mag_b650m_mortar_wifi_ms_7d76_pump_fan_speed-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
@@ -564,7 +564,7 @@
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.gaming_pc_msi_mag_b650m_mortar_wifi_ms_7d76_pump_fan_fan',
'entity_id': 'sensor.gaming_pc_msi_mag_b650m_mortar_wifi_ms_7d76_pump_fan_speed',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
@@ -572,12 +572,12 @@
'labels': set({
}),
'name': None,
'object_id_base': 'Pump Fan Fan',
'object_id_base': 'Pump Fan Speed',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Pump Fan Fan',
'original_name': 'Pump Fan Speed',
'platform': 'libre_hardware_monitor',
'previous_unique_id': None,
'suggested_object_id': None,
@@ -587,24 +587,24 @@
'unit_of_measurement': 'RPM',
})
# ---
# name: test_sensors_are_created[sensor.gaming_pc_msi_mag_b650m_mortar_wifi_ms_7d76_pump_fan_fan-state]
# name: test_sensors_are_created[sensor.gaming_pc_msi_mag_b650m_mortar_wifi_ms_7d76_pump_fan_speed-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': '[GAMING-PC] MSI MAG B650M MORTAR WIFI (MS-7D76) Pump Fan Fan',
'friendly_name': '[GAMING-PC] MSI MAG B650M MORTAR WIFI (MS-7D76) Pump Fan Speed',
'max_value': '0',
'min_value': '0',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'RPM',
}),
'context': <ANY>,
'entity_id': 'sensor.gaming_pc_msi_mag_b650m_mortar_wifi_ms_7d76_pump_fan_fan',
'entity_id': 'sensor.gaming_pc_msi_mag_b650m_mortar_wifi_ms_7d76_pump_fan_speed',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '0',
})
# ---
# name: test_sensors_are_created[sensor.gaming_pc_msi_mag_b650m_mortar_wifi_ms_7d76_system_fan_1_fan-entry]
# name: test_sensors_are_created[sensor.gaming_pc_msi_mag_b650m_mortar_wifi_ms_7d76_system_fan_1_speed-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
@@ -619,7 +619,7 @@
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.gaming_pc_msi_mag_b650m_mortar_wifi_ms_7d76_system_fan_1_fan',
'entity_id': 'sensor.gaming_pc_msi_mag_b650m_mortar_wifi_ms_7d76_system_fan_1_speed',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
@@ -627,12 +627,12 @@
'labels': set({
}),
'name': None,
'object_id_base': 'System Fan #1 Fan',
'object_id_base': 'System Fan #1 Speed',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'System Fan #1 Fan',
'original_name': 'System Fan #1 Speed',
'platform': 'libre_hardware_monitor',
'previous_unique_id': None,
'suggested_object_id': None,
@@ -642,16 +642,16 @@
'unit_of_measurement': None,
})
# ---
# name: test_sensors_are_created[sensor.gaming_pc_msi_mag_b650m_mortar_wifi_ms_7d76_system_fan_1_fan-state]
# name: test_sensors_are_created[sensor.gaming_pc_msi_mag_b650m_mortar_wifi_ms_7d76_system_fan_1_speed-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': '[GAMING-PC] MSI MAG B650M MORTAR WIFI (MS-7D76) System Fan #1 Fan',
'friendly_name': '[GAMING-PC] MSI MAG B650M MORTAR WIFI (MS-7D76) System Fan #1 Speed',
'max_value': None,
'min_value': None,
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'context': <ANY>,
'entity_id': 'sensor.gaming_pc_msi_mag_b650m_mortar_wifi_ms_7d76_system_fan_1_fan',
'entity_id': 'sensor.gaming_pc_msi_mag_b650m_mortar_wifi_ms_7d76_system_fan_1_speed',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
@@ -933,7 +933,7 @@
'state': '36.0',
})
# ---
# name: test_sensors_are_created[sensor.gaming_pc_nvidia_geforce_rtx_4080_super_gpu_fan_1_fan-entry]
# name: test_sensors_are_created[sensor.gaming_pc_nvidia_geforce_rtx_4080_super_gpu_fan_1_speed-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
@@ -948,7 +948,7 @@
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.gaming_pc_nvidia_geforce_rtx_4080_super_gpu_fan_1_fan',
'entity_id': 'sensor.gaming_pc_nvidia_geforce_rtx_4080_super_gpu_fan_1_speed',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
@@ -956,12 +956,12 @@
'labels': set({
}),
'name': None,
'object_id_base': 'GPU Fan 1 Fan',
'object_id_base': 'GPU Fan 1 Speed',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'GPU Fan 1 Fan',
'original_name': 'GPU Fan 1 Speed',
'platform': 'libre_hardware_monitor',
'previous_unique_id': None,
'suggested_object_id': None,
@@ -971,24 +971,24 @@
'unit_of_measurement': 'RPM',
})
# ---
# name: test_sensors_are_created[sensor.gaming_pc_nvidia_geforce_rtx_4080_super_gpu_fan_1_fan-state]
# name: test_sensors_are_created[sensor.gaming_pc_nvidia_geforce_rtx_4080_super_gpu_fan_1_speed-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': '[GAMING-PC] NVIDIA GeForce RTX 4080 SUPER GPU Fan 1 Fan',
'friendly_name': '[GAMING-PC] NVIDIA GeForce RTX 4080 SUPER GPU Fan 1 Speed',
'max_value': '0',
'min_value': '0',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'RPM',
}),
'context': <ANY>,
'entity_id': 'sensor.gaming_pc_nvidia_geforce_rtx_4080_super_gpu_fan_1_fan',
'entity_id': 'sensor.gaming_pc_nvidia_geforce_rtx_4080_super_gpu_fan_1_speed',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '0',
})
# ---
# name: test_sensors_are_created[sensor.gaming_pc_nvidia_geforce_rtx_4080_super_gpu_fan_2_fan-entry]
# name: test_sensors_are_created[sensor.gaming_pc_nvidia_geforce_rtx_4080_super_gpu_fan_2_speed-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
@@ -1003,7 +1003,7 @@
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.gaming_pc_nvidia_geforce_rtx_4080_super_gpu_fan_2_fan',
'entity_id': 'sensor.gaming_pc_nvidia_geforce_rtx_4080_super_gpu_fan_2_speed',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
@@ -1011,12 +1011,12 @@
'labels': set({
}),
'name': None,
'object_id_base': 'GPU Fan 2 Fan',
'object_id_base': 'GPU Fan 2 Speed',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'GPU Fan 2 Fan',
'original_name': 'GPU Fan 2 Speed',
'platform': 'libre_hardware_monitor',
'previous_unique_id': None,
'suggested_object_id': None,
@@ -1026,17 +1026,17 @@
'unit_of_measurement': 'RPM',
})
# ---
# name: test_sensors_are_created[sensor.gaming_pc_nvidia_geforce_rtx_4080_super_gpu_fan_2_fan-state]
# name: test_sensors_are_created[sensor.gaming_pc_nvidia_geforce_rtx_4080_super_gpu_fan_2_speed-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': '[GAMING-PC] NVIDIA GeForce RTX 4080 SUPER GPU Fan 2 Fan',
'friendly_name': '[GAMING-PC] NVIDIA GeForce RTX 4080 SUPER GPU Fan 2 Speed',
'max_value': '0',
'min_value': '0',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'RPM',
}),
'context': <ANY>,
'entity_id': 'sensor.gaming_pc_nvidia_geforce_rtx_4080_super_gpu_fan_2_fan',
'entity_id': 'sensor.gaming_pc_nvidia_geforce_rtx_4080_super_gpu_fan_2_speed',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
@@ -1452,14 +1452,14 @@
}),
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': '[GAMING-PC] MSI MAG B650M MORTAR WIFI (MS-7D76) CPU Fan Fan',
'friendly_name': '[GAMING-PC] MSI MAG B650M MORTAR WIFI (MS-7D76) CPU Fan Speed',
'max_value': '0',
'min_value': '0',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'RPM',
}),
'context': <ANY>,
'entity_id': 'sensor.gaming_pc_msi_mag_b650m_mortar_wifi_ms_7d76_cpu_fan_fan',
'entity_id': 'sensor.gaming_pc_msi_mag_b650m_mortar_wifi_ms_7d76_cpu_fan_speed',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
@@ -1467,14 +1467,14 @@
}),
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': '[GAMING-PC] MSI MAG B650M MORTAR WIFI (MS-7D76) Pump Fan Fan',
'friendly_name': '[GAMING-PC] MSI MAG B650M MORTAR WIFI (MS-7D76) Pump Fan Speed',
'max_value': '0',
'min_value': '0',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'RPM',
}),
'context': <ANY>,
'entity_id': 'sensor.gaming_pc_msi_mag_b650m_mortar_wifi_ms_7d76_pump_fan_fan',
'entity_id': 'sensor.gaming_pc_msi_mag_b650m_mortar_wifi_ms_7d76_pump_fan_speed',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
@@ -1482,13 +1482,13 @@
}),
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': '[GAMING-PC] MSI MAG B650M MORTAR WIFI (MS-7D76) System Fan #1 Fan',
'friendly_name': '[GAMING-PC] MSI MAG B650M MORTAR WIFI (MS-7D76) System Fan #1 Speed',
'max_value': None,
'min_value': None,
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'context': <ANY>,
'entity_id': 'sensor.gaming_pc_msi_mag_b650m_mortar_wifi_ms_7d76_system_fan_1_fan',
'entity_id': 'sensor.gaming_pc_msi_mag_b650m_mortar_wifi_ms_7d76_system_fan_1_speed',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
@@ -1706,14 +1706,14 @@
}),
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': '[GAMING-PC] NVIDIA GeForce RTX 4080 SUPER GPU Fan 1 Fan',
'friendly_name': '[GAMING-PC] NVIDIA GeForce RTX 4080 SUPER GPU Fan 1 Speed',
'max_value': '0',
'min_value': '0',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'RPM',
}),
'context': <ANY>,
'entity_id': 'sensor.gaming_pc_nvidia_geforce_rtx_4080_super_gpu_fan_1_fan',
'entity_id': 'sensor.gaming_pc_nvidia_geforce_rtx_4080_super_gpu_fan_1_speed',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
@@ -1721,14 +1721,14 @@
}),
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': '[GAMING-PC] NVIDIA GeForce RTX 4080 SUPER GPU Fan 2 Fan',
'friendly_name': '[GAMING-PC] NVIDIA GeForce RTX 4080 SUPER GPU Fan 2 Speed',
'max_value': '0',
'min_value': '0',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'RPM',
}),
'context': <ANY>,
'entity_id': 'sensor.gaming_pc_nvidia_geforce_rtx_4080_super_gpu_fan_2_fan',
'entity_id': 'sensor.gaming_pc_nvidia_geforce_rtx_4080_super_gpu_fan_2_speed',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
@@ -1830,14 +1830,14 @@
}),
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': '[GAMING-PC] MSI MAG B650M MORTAR WIFI (MS-7D76) CPU Fan Fan',
'friendly_name': '[GAMING-PC] MSI MAG B650M MORTAR WIFI (MS-7D76) CPU Fan Speed',
'max_value': '0',
'min_value': '0',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'RPM',
}),
'context': <ANY>,
'entity_id': 'sensor.gaming_pc_msi_mag_b650m_mortar_wifi_ms_7d76_cpu_fan_fan',
'entity_id': 'sensor.gaming_pc_msi_mag_b650m_mortar_wifi_ms_7d76_cpu_fan_speed',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
@@ -1845,14 +1845,14 @@
}),
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': '[GAMING-PC] MSI MAG B650M MORTAR WIFI (MS-7D76) Pump Fan Fan',
'friendly_name': '[GAMING-PC] MSI MAG B650M MORTAR WIFI (MS-7D76) Pump Fan Speed',
'max_value': '0',
'min_value': '0',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'RPM',
}),
'context': <ANY>,
'entity_id': 'sensor.gaming_pc_msi_mag_b650m_mortar_wifi_ms_7d76_pump_fan_fan',
'entity_id': 'sensor.gaming_pc_msi_mag_b650m_mortar_wifi_ms_7d76_pump_fan_speed',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
@@ -1860,13 +1860,13 @@
}),
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': '[GAMING-PC] MSI MAG B650M MORTAR WIFI (MS-7D76) System Fan #1 Fan',
'friendly_name': '[GAMING-PC] MSI MAG B650M MORTAR WIFI (MS-7D76) System Fan #1 Speed',
'max_value': None,
'min_value': None,
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'context': <ANY>,
'entity_id': 'sensor.gaming_pc_msi_mag_b650m_mortar_wifi_ms_7d76_system_fan_1_fan',
'entity_id': 'sensor.gaming_pc_msi_mag_b650m_mortar_wifi_ms_7d76_system_fan_1_speed',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
@@ -2084,14 +2084,14 @@
}),
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': '[GAMING-PC] NVIDIA GeForce RTX 4080 SUPER GPU Fan 1 Fan',
'friendly_name': '[GAMING-PC] NVIDIA GeForce RTX 4080 SUPER GPU Fan 1 Speed',
'max_value': '0',
'min_value': '0',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'RPM',
}),
'context': <ANY>,
'entity_id': 'sensor.gaming_pc_nvidia_geforce_rtx_4080_super_gpu_fan_1_fan',
'entity_id': 'sensor.gaming_pc_nvidia_geforce_rtx_4080_super_gpu_fan_1_speed',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
@@ -2099,14 +2099,14 @@
}),
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': '[GAMING-PC] NVIDIA GeForce RTX 4080 SUPER GPU Fan 2 Fan',
'friendly_name': '[GAMING-PC] NVIDIA GeForce RTX 4080 SUPER GPU Fan 2 Speed',
'max_value': '0',
'min_value': '0',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'RPM',
}),
'context': <ANY>,
'entity_id': 'sensor.gaming_pc_nvidia_geforce_rtx_4080_super_gpu_fan_2_fan',
'entity_id': 'sensor.gaming_pc_nvidia_geforce_rtx_4080_super_gpu_fan_2_speed',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,

View File

@@ -6,6 +6,7 @@ from http import HTTPStatus
import json
import logging
from typing import Any
from unittest.mock import AsyncMock, patch
import aiohttp
import mcp
@@ -478,3 +479,42 @@ async def test_get_unknown_prompt(
async with mcp_client(hass, mcp_url, hass_supervisor_access_token) as session:
with pytest.raises(McpError):
await session.get_prompt(name="Unknown")
@pytest.mark.parametrize("llm_hass_api", [llm.LLM_API_ASSIST])
async def test_mcp_tool_call_unicode(
hass: HomeAssistant,
setup_integration: None,
mcp_url: str,
mcp_client: Any,
hass_supervisor_access_token: str,
) -> None:
"""Test the tool call endpoint preserves unicode characters."""
# Mock the API instance
mock_api = AsyncMock()
mock_api.api.name = "Assist"
mock_api.tools = []
mock_api.custom_serializer = None
mock_api.async_call_tool.return_value = {"message": "这是一个测试"}
# We need to ensure when the server calls llm.async_get_api, it gets our mock
# async_get_api is awaited, so we need an AsyncMock
with patch(
"homeassistant.helpers.llm.async_get_api", new_callable=AsyncMock
) as mock_get_api:
mock_get_api.return_value = mock_api
async with mcp_client(hass, mcp_url, hass_supervisor_access_token) as session:
result = await session.call_tool(
name="AnyTool",
arguments={},
)
assert not result.isError
assert len(result.content) == 1
assert result.content[0].type == "text"
# Check that the text contains the raw unicode characters, NOT the escaped version
response_text = result.content[0].text
assert "这是一个测试" in response_text
assert "\\u" not in response_text

View File

@@ -57,6 +57,7 @@ from homeassistant.util.unit_system import US_CUSTOMARY_SYSTEM
from . import (
MOCK_MAC,
init_integration,
mutate_rpc_device_status,
patch_platforms,
register_device,
register_entity,
@@ -1047,6 +1048,16 @@ async def test_rpc_linkedgo_st802_thermostat(
assert (state := hass.states.get(entity_id))
assert state.state == HVACMode.OFF
# Test current temperature update
assert (state := hass.states.get(entity_id))
assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 25.1
mutate_rpc_device_status(monkeypatch, mock_rpc_device, "number:201", "value", 22.4)
mock_rpc_device.mock_update()
assert (state := hass.states.get(entity_id))
assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 22.4
async def test_rpc_linkedgo_st1820_thermostat(
hass: HomeAssistant,

View File

@@ -2448,7 +2448,7 @@
'object_id_base': 'VPP backup reserve',
'options': dict({
}),
'original_device_class': <SensorDeviceClass.BATTERY: 'battery'>,
'original_device_class': None,
'original_icon': None,
'original_name': 'VPP backup reserve',
'platform': 'tesla_fleet',
@@ -2463,7 +2463,6 @@
# name: test_sensors[sensor.energy_site_vpp_backup_reserve-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'battery',
'friendly_name': 'Energy Site VPP backup reserve',
'unit_of_measurement': '%',
}),
@@ -2478,7 +2477,6 @@
# name: test_sensors[sensor.energy_site_vpp_backup_reserve-statealt]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'battery',
'friendly_name': 'Energy Site VPP backup reserve',
'unit_of_measurement': '%',
}),

View File

@@ -1,7 +1,9 @@
"""Unit tests for the Todoist todo platform."""
from datetime import date, datetime
from typing import Any
from unittest.mock import AsyncMock
from zoneinfo import ZoneInfo
import pytest
from todoist_api_python.models import Task
@@ -113,7 +115,7 @@ async def test_todo_item_state(
),
)
],
{"description": "", "due_date": "2023-11-18"},
{"description": "", "due_date": date(2023, 11, 18)},
{
"uid": "task-id-1",
"summary": "Soda",
@@ -138,7 +140,9 @@ async def test_todo_item_state(
],
{
"description": "",
"due_datetime": "2023-11-18T06:30:00-06:00",
"due_datetime": datetime(
2023, 11, 18, 6, 30, tzinfo=ZoneInfo("US/Central")
),
},
{
"uid": "task-id-1",
@@ -333,7 +337,7 @@ async def test_update_todo_item_status(
{
"task_id": "task-id-1",
"content": "Soda",
"due_date": "2023-11-18",
"due_date": date(2023, 11, 18),
"description": "",
},
{
@@ -361,7 +365,9 @@ async def test_update_todo_item_status(
{
"task_id": "task-id-1",
"content": "Soda",
"due_datetime": "2023-11-18T06:30:00-06:00",
"due_datetime": datetime(
2023, 11, 18, 6, 30, tzinfo=ZoneInfo("US/Central")
),
"description": "",
},
{
@@ -455,7 +461,7 @@ async def test_update_todo_item_status(
"task_id": "task-id-1",
"content": "Soda",
"description": "6-pack",
"due_date": "2024-02-01",
"due_date": date(2024, 2, 1),
"due_string": "every day",
},
{

View File

@@ -90,7 +90,6 @@
'preset_modes': list([
'auto',
'manual',
'off',
]),
'target_temp_step': 1.0,
}),
@@ -137,7 +136,6 @@
'preset_modes': list([
'auto',
'manual',
'off',
]),
'supported_features': <ClimateEntityFeature: 17>,
'target_temp_step': 1.0,
@@ -460,7 +458,6 @@
'preset_modes': list([
'auto',
'manual',
'off',
]),
'target_temp_step': 1.0,
}),
@@ -509,7 +506,6 @@
'preset_modes': list([
'auto',
'manual',
'off',
]),
'supported_features': <ClimateEntityFeature: 17>,
'target_temp_step': 1.0,
@@ -538,7 +534,6 @@
'preset_modes': list([
'auto',
'manual',
'off',
]),
'target_temp_step': 1.0,
}),
@@ -587,7 +582,6 @@
'preset_modes': list([
'auto',
'manual',
'off',
]),
'supported_features': <ClimateEntityFeature: 401>,
'target_temp_step': 1.0,

View File

@@ -1,4 +1,57 @@
# serializer version: 1
# name: test_sensor_entities[sensor.yardian_smart_sprinkler_active_zones-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.yardian_smart_sprinkler_active_zones',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Active zones',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Active zones',
'platform': 'yardian',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'active_zone_count',
'unique_id': 'yid123_active_zone_count',
'unit_of_measurement': 'zones',
})
# ---
# name: test_sensor_entities[sensor.yardian_smart_sprinkler_active_zones-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Yardian Smart Sprinkler Active zones',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'zones',
}),
'context': <ANY>,
'entity_id': 'sensor.yardian_smart_sprinkler_active_zones',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '1',
})
# ---
# name: test_sensor_entities[sensor.yardian_smart_sprinkler_rain_delay-entry]
EntityRegistryEntrySnapshot({
'aliases': set({

View File

@@ -1967,11 +1967,39 @@ async def test_invalid_entity_id(
assert entity.hass is None
assert entity.platform is None
assert "Invalid entity ID: invalid_entity_id" in caplog.text
# Ensure the valid entity was still added
assert entity2.hass is not None
assert entity2.platform is not None
async def test_invalid_entity_id_report_usage(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
) -> None:
"""Test that setting an invalid entity_id reports usage."""
platform = MockEntityPlatform(hass)
entity = MockEntity(entity_id="test_domain.INVALID-ENTITY-ID", unique_id="unique")
mock_integration = Mock(is_built_in=True, domain="test_platform")
with (
caplog.at_level(logging.WARNING),
patch(
"homeassistant.helpers.frame.async_get_issue_integration",
return_value=mock_integration,
),
):
await platform.async_add_entities([entity])
assert (
"Detected that integration 'test_platform' "
"sets an invalid entity ID: 'test_domain.INVALID-ENTITY-ID'"
) in caplog.text
# Ensure the entity was still added
assert entity.hass is not None
assert entity.platform is not None
class MockBlockingEntity(MockEntity):
"""Class to mock an entity that will block adding entities."""