diff --git a/.strict-typing b/.strict-typing index fee39a8060eaf4..ed9b74594fcb13 100644 --- a/.strict-typing +++ b/.strict-typing @@ -545,6 +545,7 @@ homeassistant.components.tcp.* homeassistant.components.technove.* homeassistant.components.tedee.* homeassistant.components.telegram_bot.* +homeassistant.components.teslemetry.* homeassistant.components.text.* homeassistant.components.thethingsnetwork.* homeassistant.components.threshold.* diff --git a/homeassistant/components/airobot/number.py b/homeassistant/components/airobot/number.py index 8cdd0b56a4c89d..e8d041e9489f96 100644 --- a/homeassistant/components/airobot/number.py +++ b/homeassistant/components/airobot/number.py @@ -93,7 +93,6 @@ async def async_set_native_value(self, value: float) -> None: raise ServiceValidationError( translation_domain=DOMAIN, translation_key="set_value_failed", - translation_placeholders={"error": str(err)}, ) from err else: await self.coordinator.async_request_refresh() diff --git a/homeassistant/components/airobot/strings.json b/homeassistant/components/airobot/strings.json index ecccf553736bd4..e12b5c333bb408 100644 --- a/homeassistant/components/airobot/strings.json +++ b/homeassistant/components/airobot/strings.json @@ -112,7 +112,7 @@ "message": "Failed to set temperature to {temperature}." }, "set_value_failed": { - "message": "Failed to set value: {error}" + "message": "Failed to set value." }, "switch_turn_off_failed": { "message": "Failed to turn off {switch}." diff --git a/homeassistant/components/anthropic/entity.py b/homeassistant/components/anthropic/entity.py index 658267219e3506..38a99cc39d9486 100644 --- a/homeassistant/components/anthropic/entity.py +++ b/homeassistant/components/anthropic/entity.py @@ -400,8 +400,8 @@ def _convert_content( # If there is only one text block, simplify the content to a string messages[-1]["content"] = messages[-1]["content"][0]["text"] else: - # Note: We don't pass SystemContent here as its passed to the API as the prompt - raise TypeError(f"Unexpected content type: {type(content)}") + # Note: We don't pass SystemContent here as it's passed to the API as the prompt + raise HomeAssistantError("Unexpected content type in chat log") return messages, container_id @@ -442,8 +442,8 @@ async def _transform_stream( # noqa: C901 - This is complex, but better to have Each message could contain multiple blocks of the same type. """ - if stream is None: - raise TypeError("Expected a stream of messages") + if stream is None or not hasattr(stream, "__aiter__"): + raise HomeAssistantError("Expected a stream of messages") current_tool_block: ToolUseBlockParam | ServerToolUseBlockParam | None = None current_tool_args: str @@ -456,8 +456,6 @@ async def _transform_stream( # noqa: C901 - This is complex, but better to have LOGGER.debug("Received response: %s", response) if isinstance(response, RawMessageStartEvent): - if response.message.role != "assistant": - raise ValueError("Unexpected message role") input_usage = response.message.usage first_block = True elif isinstance(response, RawContentBlockStartEvent): @@ -666,7 +664,7 @@ async def _async_handle_chat_log( system = chat_log.content[0] if not isinstance(system, conversation.SystemContent): - raise TypeError("First message must be a system message") + raise HomeAssistantError("First message must be a system message") # System prompt with caching enabled system_prompt: list[TextBlockParam] = [ diff --git a/homeassistant/components/anthropic/quality_scale.yaml b/homeassistant/components/anthropic/quality_scale.yaml index eec8ce302039f4..37f605b1532a88 100644 --- a/homeassistant/components/anthropic/quality_scale.yaml +++ b/homeassistant/components/anthropic/quality_scale.yaml @@ -31,10 +31,7 @@ rules: test-before-setup: done unique-config-entry: done # Silver - action-exceptions: - status: todo - comment: | - Reevaluate exceptions for entity services. + action-exceptions: done config-entry-unloading: done docs-configuration-parameters: done docs-installation-parameters: done diff --git a/homeassistant/components/aquostv/media_player.py b/homeassistant/components/aquostv/media_player.py index 734bd10cfbe0d5..3fc6bed54a1c37 100644 --- a/homeassistant/components/aquostv/media_player.py +++ b/homeassistant/components/aquostv/media_player.py @@ -117,6 +117,7 @@ class SharpAquosTVDevice(MediaPlayerEntity): | MediaPlayerEntityFeature.VOLUME_SET | MediaPlayerEntityFeature.PLAY ) + _attr_volume_step = 2 / 60 def __init__( self, name: str, remote: sharp_aquos_rc.TV, power_on_enabled: bool = False @@ -161,22 +162,6 @@ def turn_off(self) -> None: """Turn off tvplayer.""" self._remote.power(0) - @_retry - def volume_up(self) -> None: - """Volume up the media player.""" - if self.volume_level is None: - _LOGGER.debug("Unknown volume in volume_up") - return - self._remote.volume(int(self.volume_level * 60) + 2) - - @_retry - def volume_down(self) -> None: - """Volume down media player.""" - if self.volume_level is None: - _LOGGER.debug("Unknown volume in volume_down") - return - self._remote.volume(int(self.volume_level * 60) - 2) - @_retry def set_volume_level(self, volume: float) -> None: """Set Volume media player.""" diff --git a/homeassistant/components/bluesound/media_player.py b/homeassistant/components/bluesound/media_player.py index f8de9203f4ad5e..fd09be71601852 100644 --- a/homeassistant/components/bluesound/media_player.py +++ b/homeassistant/components/bluesound/media_player.py @@ -85,6 +85,7 @@ class BluesoundPlayer(CoordinatorEntity[BluesoundCoordinator], MediaPlayerEntity _attr_media_content_type = MediaType.MUSIC _attr_has_entity_name = True _attr_name = None + _attr_volume_step = 0.01 def __init__( self, @@ -688,24 +689,6 @@ async def async_play_media( await self._player.play_url(url) - async def async_volume_up(self) -> None: - """Volume up the media player.""" - if self.volume_level is None: - return - - new_volume = self.volume_level + 0.01 - new_volume = min(1, new_volume) - await self.async_set_volume_level(new_volume) - - async def async_volume_down(self) -> None: - """Volume down the media player.""" - if self.volume_level is None: - return - - new_volume = self.volume_level - 0.01 - new_volume = max(0, new_volume) - await self.async_set_volume_level(new_volume) - async def async_set_volume_level(self, volume: float) -> None: """Send volume_up command to media player.""" volume = int(round(volume * 100)) diff --git a/homeassistant/components/demo/media_player.py b/homeassistant/components/demo/media_player.py index 0c001921c7a517..c65cdd12becd88 100644 --- a/homeassistant/components/demo/media_player.py +++ b/homeassistant/components/demo/media_player.py @@ -139,18 +139,6 @@ def mute_volume(self, mute: bool) -> None: self._attr_is_volume_muted = mute self.schedule_update_ha_state() - def volume_up(self) -> None: - """Increase volume.""" - assert self.volume_level is not None - self._attr_volume_level = min(1.0, self.volume_level + 0.1) - self.schedule_update_ha_state() - - def volume_down(self) -> None: - """Decrease volume.""" - assert self.volume_level is not None - self._attr_volume_level = max(0.0, self.volume_level - 0.1) - self.schedule_update_ha_state() - def set_volume_level(self, volume: float) -> None: """Set the volume level, range 0..1.""" self._attr_volume_level = volume diff --git a/homeassistant/components/frontier_silicon/media_player.py b/homeassistant/components/frontier_silicon/media_player.py index 1a85245933a61d..6601a2070cf232 100644 --- a/homeassistant/components/frontier_silicon/media_player.py +++ b/homeassistant/components/frontier_silicon/media_player.py @@ -151,6 +151,8 @@ async def async_update(self) -> None: # If call to get_volume fails set to 0 and try again next time. if not self._max_volume: self._max_volume = int(await afsapi.get_volume_steps() or 1) - 1 + if self._max_volume: + self._attr_volume_step = 1 / self._max_volume if self._attr_state != MediaPlayerState.OFF: info_name = await afsapi.get_play_name() @@ -239,18 +241,6 @@ async def async_mute_volume(self, mute: bool) -> None: await self.fs_device.set_mute(mute) # volume - async def async_volume_up(self) -> None: - """Send volume up command.""" - volume = await self.fs_device.get_volume() - volume = int(volume or 0) + 1 - await self.fs_device.set_volume(min(volume, self._max_volume or 1)) - - async def async_volume_down(self) -> None: - """Send volume down command.""" - volume = await self.fs_device.get_volume() - volume = int(volume or 0) - 1 - await self.fs_device.set_volume(max(volume, 0)) - async def async_set_volume_level(self, volume: float) -> None: """Set volume command.""" if self._max_volume: # Can't do anything sensible if not set diff --git a/homeassistant/components/govee_ble/manifest.json b/homeassistant/components/govee_ble/manifest.json index 696194266f478c..1bfd73e875f9b2 100644 --- a/homeassistant/components/govee_ble/manifest.json +++ b/homeassistant/components/govee_ble/manifest.json @@ -140,5 +140,5 @@ "documentation": "https://www.home-assistant.io/integrations/govee_ble", "integration_type": "device", "iot_class": "local_push", - "requirements": ["govee-ble==0.44.0"] + "requirements": ["govee-ble==1.2.0"] } diff --git a/homeassistant/components/monoprice/media_player.py b/homeassistant/components/monoprice/media_player.py index 734dbecd88b781..1ca07eb8dbae97 100644 --- a/homeassistant/components/monoprice/media_player.py +++ b/homeassistant/components/monoprice/media_player.py @@ -128,6 +128,7 @@ class MonopriceZone(MediaPlayerEntity): ) _attr_has_entity_name = True _attr_name = None + _attr_volume_step = 1 / MAX_VOLUME def __init__(self, monoprice, sources, namespace, zone_id): """Initialize new zone.""" @@ -211,17 +212,3 @@ def mute_volume(self, mute: bool) -> None: def set_volume_level(self, volume: float) -> None: """Set volume level, range 0..1.""" self._monoprice.set_volume(self._zone_id, round(volume * MAX_VOLUME)) - - def volume_up(self) -> None: - """Volume up the media player.""" - if self.volume_level is None: - return - volume = round(self.volume_level * MAX_VOLUME) - self._monoprice.set_volume(self._zone_id, min(volume + 1, MAX_VOLUME)) - - def volume_down(self) -> None: - """Volume down media player.""" - if self.volume_level is None: - return - volume = round(self.volume_level * MAX_VOLUME) - self._monoprice.set_volume(self._zone_id, max(volume - 1, 0)) diff --git a/homeassistant/components/mpd/media_player.py b/homeassistant/components/mpd/media_player.py index 14b69e941b7d0e..8a33e6ff6c2f40 100644 --- a/homeassistant/components/mpd/media_player.py +++ b/homeassistant/components/mpd/media_player.py @@ -93,6 +93,7 @@ class MpdDevice(MediaPlayerEntity): _attr_media_content_type = MediaType.MUSIC _attr_has_entity_name = True _attr_name = None + _attr_volume_step = 0.05 def __init__( self, server: str, port: int, password: str | None, unique_id: str @@ -393,24 +394,6 @@ async def async_set_volume_level(self, volume: float) -> None: if "volume" in self._status: await self._client.setvol(int(volume * 100)) - async def async_volume_up(self) -> None: - """Service to send the MPD the command for volume up.""" - async with self.connection(): - if "volume" in self._status: - current_volume = int(self._status["volume"]) - - if current_volume <= 100: - self._client.setvol(current_volume + 5) - - async def async_volume_down(self) -> None: - """Service to send the MPD the command for volume down.""" - async with self.connection(): - if "volume" in self._status: - current_volume = int(self._status["volume"]) - - if current_volume >= 0: - await self._client.setvol(current_volume - 5) - async def async_media_play(self) -> None: """Service to send the MPD the command for play/pause.""" async with self.connection(): diff --git a/homeassistant/components/nad/media_player.py b/homeassistant/components/nad/media_player.py index c1efa18f72b39d..2af8c607610086 100644 --- a/homeassistant/components/nad/media_player.py +++ b/homeassistant/components/nad/media_player.py @@ -198,8 +198,10 @@ def __init__(self, config): self._nad_receiver = NADReceiverTCP(config.get(CONF_HOST)) self._min_vol = (config[CONF_MIN_VOLUME] + 90) * 2 # from dB to nad vol (0-200) self._max_vol = (config[CONF_MAX_VOLUME] + 90) * 2 # from dB to nad vol (0-200) - self._volume_step = config[CONF_VOLUME_STEP] self._nad_volume = None + vol_range = self._max_vol - self._min_vol + if vol_range: + self._attr_volume_step = 2 * config[CONF_VOLUME_STEP] / vol_range self._source_list = self._nad_receiver.available_sources() def turn_off(self) -> None: @@ -210,14 +212,6 @@ def turn_on(self) -> None: """Turn the media player on.""" self._nad_receiver.power_on() - def volume_up(self) -> None: - """Step volume up in the configured increments.""" - self._nad_receiver.set_volume(self._nad_volume + 2 * self._volume_step) - - def volume_down(self) -> None: - """Step volume down in the configured increments.""" - self._nad_receiver.set_volume(self._nad_volume - 2 * self._volume_step) - def set_volume_level(self, volume: float) -> None: """Set volume level, range 0..1.""" nad_volume_to_set = int( diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index f5cff59a2e95c5..2120f5e50e6328 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -66,6 +66,7 @@ from .services import async_setup_services from .utils import ( async_create_issue_unsupported_firmware, + async_migrate_rpc_sensor_description_unique_ids, async_migrate_rpc_virtual_components_unique_ids, get_coap_context, get_device_entry_gen, @@ -296,6 +297,12 @@ async def _async_setup_rpc_entry(hass: HomeAssistant, entry: ShellyConfigEntry) runtime_data = entry.runtime_data runtime_data.platforms = RPC_SLEEPING_PLATFORMS + await er.async_migrate_entries( + hass, + entry.entry_id, + async_migrate_rpc_sensor_description_unique_ids, + ) + if sleep_period == 0: # Not a sleeping device, finish setup LOGGER.debug("Setting up online RPC device %s", entry.title) diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index 41d710cf2da082..5eeb818c59a56d 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -1220,7 +1220,7 @@ def __init__( entity_category=EntityCategory.DIAGNOSTIC, use_polling_coordinator=True, ), - "temperature_0": RpcSensorDescription( + "temperature_tc": RpcSensorDescription( key="temperature", sub_key="tC", native_unit_of_measurement=UnitOfTemperature.CELSIUS, @@ -1249,7 +1249,7 @@ def __init__( entity_category=EntityCategory.DIAGNOSTIC, use_polling_coordinator=True, ), - "humidity_0": RpcSensorDescription( + "humidity_rh": RpcSensorDescription( key="humidity", sub_key="rh", native_unit_of_measurement=PERCENTAGE, diff --git a/homeassistant/components/shelly/utils.py b/homeassistant/components/shelly/utils.py index b7da839e6fccd4..27afa335e5e478 100644 --- a/homeassistant/components/shelly/utils.py +++ b/homeassistant/components/shelly/utils.py @@ -969,6 +969,30 @@ def format_ble_addr(ble_addr: str) -> str: return ble_addr.replace(":", "").upper() +@callback +def async_migrate_rpc_sensor_description_unique_ids( + entity_entry: er.RegistryEntry, +) -> dict[str, Any] | None: + """Migrate RPC sensor unique_ids after sensor description key rename.""" + unique_id_map = { + "-temperature_0": "-temperature_tc", + "-humidity_0": "-humidity_rh", + } + + for old_suffix, new_suffix in unique_id_map.items(): + if entity_entry.unique_id.endswith(old_suffix): + new_unique_id = entity_entry.unique_id.removesuffix(old_suffix) + new_suffix + LOGGER.debug( + "Migrating unique_id for %s entity from [%s] to [%s]", + entity_entry.entity_id, + entity_entry.unique_id, + new_unique_id, + ) + return {"new_unique_id": new_unique_id} + + return None + + @callback def async_migrate_rpc_virtual_components_unique_ids( config: dict[str, Any], entity_entry: er.RegistryEntry diff --git a/homeassistant/components/teslemetry/__init__.py b/homeassistant/components/teslemetry/__init__.py index 8eac44d32d84f2..fddec0df345c63 100644 --- a/homeassistant/components/teslemetry/__init__.py +++ b/homeassistant/components/teslemetry/__init__.py @@ -3,7 +3,7 @@ import asyncio from collections.abc import Callable from functools import partial -from typing import Final +from typing import Any, Final from aiohttp import ClientError, ClientResponseError from tesla_fleet_api.const import Scope @@ -106,7 +106,7 @@ async def _get_access_token(oauth_session: OAuth2Session) -> str: translation_domain=DOMAIN, translation_key="not_ready_connection_error", ) from err - return oauth_session.token[CONF_ACCESS_TOKEN] + return str(oauth_session.token[CONF_ACCESS_TOKEN]) async def async_setup_entry(hass: HomeAssistant, entry: TeslemetryConfigEntry) -> bool: @@ -227,7 +227,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: TeslemetryConfigEntry) - stream=stream, stream_vehicle=stream_vehicle, vin=vin, - firmware=firmware, + firmware=firmware or "", device=device, ) ) @@ -398,10 +398,12 @@ async def async_migrate_entry( return True -def create_handle_vehicle_stream(vin: str, coordinator) -> Callable[[dict], None]: +def create_handle_vehicle_stream( + vin: str, coordinator: TeslemetryVehicleDataCoordinator +) -> Callable[[dict[str, Any]], None]: """Create a handle vehicle stream function.""" - def handle_vehicle_stream(data: dict) -> None: + def handle_vehicle_stream(data: dict[str, Any]) -> None: """Handle vehicle data from the stream.""" if "vehicle_data" in data: LOGGER.debug("Streaming received vehicle data from %s", vin) @@ -450,7 +452,7 @@ def async_setup_energy_device( async def async_setup_stream( hass: HomeAssistant, entry: TeslemetryConfigEntry, vehicle: TeslemetryVehicleData -): +) -> None: """Set up the stream for a vehicle.""" await vehicle.stream_vehicle.get_config() diff --git a/homeassistant/components/teslemetry/climate.py b/homeassistant/components/teslemetry/climate.py index 15965044771866..a82a712ec72a61 100644 --- a/homeassistant/components/teslemetry/climate.py +++ b/homeassistant/components/teslemetry/climate.py @@ -329,11 +329,11 @@ async def async_added_to_hass(self) -> None: ) ) - def _async_handle_inside_temp(self, data: float | None): + def _async_handle_inside_temp(self, data: float | None) -> None: self._attr_current_temperature = data self.async_write_ha_state() - def _async_handle_hvac_power(self, data: str | None): + def _async_handle_hvac_power(self, data: str | None) -> None: self._attr_hvac_mode = ( None if data is None @@ -343,15 +343,15 @@ def _async_handle_hvac_power(self, data: str | None): ) self.async_write_ha_state() - def _async_handle_climate_keeper_mode(self, data: str | None): + def _async_handle_climate_keeper_mode(self, data: str | None) -> None: self._attr_preset_mode = PRESET_MODES.get(data) if data else None self.async_write_ha_state() - def _async_handle_hvac_temperature_request(self, data: float | None): + def _async_handle_hvac_temperature_request(self, data: float | None) -> None: self._attr_target_temperature = data self.async_write_ha_state() - def _async_handle_rhd(self, data: bool | None): + def _async_handle_rhd(self, data: bool | None) -> None: if data is not None: self.rhd = data @@ -538,15 +538,15 @@ async def async_added_to_hass(self) -> None: ) ) - def _async_handle_inside_temp(self, value: float | None): + def _async_handle_inside_temp(self, value: float | None) -> None: self._attr_current_temperature = value self.async_write_ha_state() - def _async_handle_protection_mode(self, value: str | None): + def _async_handle_protection_mode(self, value: str | None) -> None: self._attr_hvac_mode = COP_MODES.get(value) if value is not None else None self.async_write_ha_state() - def _async_handle_temperature_limit(self, value: str | None): + def _async_handle_temperature_limit(self, value: str | None) -> None: self._attr_target_temperature = ( COP_LEVELS.get(value) if value is not None else None ) diff --git a/homeassistant/components/teslemetry/coordinator.py b/homeassistant/components/teslemetry/coordinator.py index c19886ec0d0902..bc472b1a85ed9a 100644 --- a/homeassistant/components/teslemetry/coordinator.py +++ b/homeassistant/components/teslemetry/coordinator.py @@ -70,7 +70,7 @@ def __init__( hass: HomeAssistant, config_entry: TeslemetryConfigEntry, api: Vehicle, - product: dict, + product: dict[str, Any], ) -> None: """Initialize Teslemetry Vehicle Update Coordinator.""" super().__init__( @@ -119,7 +119,7 @@ def __init__( hass: HomeAssistant, config_entry: TeslemetryConfigEntry, api: EnergySite, - data: dict, + data: dict[str, Any], ) -> None: """Initialize Teslemetry Energy Site Live coordinator.""" super().__init__( @@ -140,7 +140,7 @@ def __init__( async def _async_update_data(self) -> dict[str, Any]: """Update energy site data using Teslemetry API.""" try: - data = (await self.api.live_status())["response"] + data: dict[str, Any] = (await self.api.live_status())["response"] except (InvalidToken, SubscriptionRequired) as e: raise ConfigEntryAuthFailed from e except RETRY_EXCEPTIONS as e: @@ -171,7 +171,7 @@ def __init__( hass: HomeAssistant, config_entry: TeslemetryConfigEntry, api: EnergySite, - product: dict, + product: dict[str, Any], ) -> None: """Initialize Teslemetry Energy Info coordinator.""" super().__init__( diff --git a/homeassistant/components/teslemetry/cover.py b/homeassistant/components/teslemetry/cover.py index 5c86d6e19fe26b..ac683b7497d94f 100644 --- a/homeassistant/components/teslemetry/cover.py +++ b/homeassistant/components/teslemetry/cover.py @@ -199,7 +199,7 @@ async def async_added_to_hass(self) -> None: f"Adding field {signal} to {self.vehicle.vin}", ) - def _handle_stream_update(self, data) -> None: + def _handle_stream_update(self, data: dict[str, Any]) -> None: """Update the entity attributes.""" change = False diff --git a/homeassistant/components/teslemetry/entity.py b/homeassistant/components/teslemetry/entity.py index ce874565160b29..cf778e1f2680af 100644 --- a/homeassistant/components/teslemetry/entity.py +++ b/homeassistant/components/teslemetry/entity.py @@ -28,7 +28,7 @@ class TeslemetryRootEntity(Entity): _attr_has_entity_name = True scoped: bool - def raise_for_scope(self, scope: Scope): + def raise_for_scope(self, scope: Scope) -> None: """Raise an error if a scope is not available.""" if not self.scoped: raise ServiceValidationError( @@ -231,11 +231,12 @@ def __init__( @property def _value(self) -> StateType: """Return a specific wall connector value from coordinator data.""" - return ( + value: StateType = ( self.coordinator.data.get("wall_connectors", {}) .get(self.din, {}) .get(self.key) ) + return value @property def exists(self) -> bool: diff --git a/homeassistant/components/teslemetry/helpers.py b/homeassistant/components/teslemetry/helpers.py index cfca3a07805aa9..834b8831d49f4b 100644 --- a/homeassistant/components/teslemetry/helpers.py +++ b/homeassistant/components/teslemetry/helpers.py @@ -1,5 +1,6 @@ """Teslemetry helper functions.""" +from collections.abc import Awaitable from typing import Any from tesla_fleet_api.exceptions import TeslaFleetError @@ -30,7 +31,7 @@ def flatten( return result -async def handle_command(command) -> dict[str, Any]: +async def handle_command(command: Awaitable[dict[str, Any]]) -> dict[str, Any]: """Handle a command.""" try: result = await command @@ -44,7 +45,7 @@ async def handle_command(command) -> dict[str, Any]: return result -async def handle_vehicle_command(command) -> Any: +async def handle_vehicle_command(command: Awaitable[dict[str, Any]]) -> Any: """Handle a vehicle command.""" result = await handle_command(command) if (response := result.get("response")) is None: diff --git a/homeassistant/components/teslemetry/models.py b/homeassistant/components/teslemetry/models.py index 3492e9da986af2..9189560cf9fba4 100644 --- a/homeassistant/components/teslemetry/models.py +++ b/homeassistant/components/teslemetry/models.py @@ -3,7 +3,7 @@ from __future__ import annotations import asyncio -from dataclasses import dataclass +from dataclasses import dataclass, field from tesla_fleet_api.const import Scope from tesla_fleet_api.teslemetry import EnergySite, Vehicle @@ -43,7 +43,7 @@ class TeslemetryVehicleData: vin: str firmware: str device: DeviceInfo - wakelock = asyncio.Lock() + wakelock: asyncio.Lock = field(default_factory=asyncio.Lock) @dataclass diff --git a/homeassistant/components/teslemetry/quality_scale.yaml b/homeassistant/components/teslemetry/quality_scale.yaml index a3e26512cdb614..3752ed7a3ba499 100644 --- a/homeassistant/components/teslemetry/quality_scale.yaml +++ b/homeassistant/components/teslemetry/quality_scale.yaml @@ -66,4 +66,4 @@ rules: # Platinum async-dependency: done inject-websession: done - strict-typing: todo + strict-typing: done diff --git a/homeassistant/components/teslemetry/update.py b/homeassistant/components/teslemetry/update.py index 253488d579dae3..d0e1d271636556 100644 --- a/homeassistant/components/teslemetry/update.py +++ b/homeassistant/components/teslemetry/update.py @@ -188,7 +188,7 @@ async def async_added_to_hass(self) -> None: def _async_handle_software_update_download_percent_complete( self, value: float | None - ): + ) -> None: """Handle software update download percent complete.""" self._download_percentage = round(value) if value is not None else 0 @@ -203,20 +203,22 @@ def _async_handle_software_update_download_percent_complete( def _async_handle_software_update_installation_percent_complete( self, value: float | None - ): + ) -> None: """Handle software update installation percent complete.""" self._install_percentage = round(value) if value is not None else 0 self._async_update_progress() self.async_write_ha_state() - def _async_handle_software_update_scheduled_start_time(self, value: str | None): + def _async_handle_software_update_scheduled_start_time( + self, value: str | None + ) -> None: """Handle software update scheduled start time.""" self._attr_in_progress = value is not None self.async_write_ha_state() - def _async_handle_software_update_version(self, value: str | None): + def _async_handle_software_update_version(self, value: str | None) -> None: """Handle software update version.""" self._attr_latest_version = ( @@ -224,7 +226,7 @@ def _async_handle_software_update_version(self, value: str | None): ) self.async_write_ha_state() - def _async_handle_version(self, value: str | None): + def _async_handle_version(self, value: str | None) -> None: """Handle version.""" if value is not None: diff --git a/mypy.ini b/mypy.ini index 79f7c850ff4dee..d09d40e7904b7a 100644 --- a/mypy.ini +++ b/mypy.ini @@ -5208,6 +5208,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.teslemetry.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.text.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/requirements_all.txt b/requirements_all.txt index 544763f603ff08..2f921c52735ab7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1116,7 +1116,7 @@ goslide-api==0.7.0 gotailwind==0.3.0 # homeassistant.components.govee_ble -govee-ble==0.44.0 +govee-ble==1.2.0 # homeassistant.components.govee_light_local govee-local-api==2.3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 409aac1b66097c..46052cfb7b7cdb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -992,7 +992,7 @@ goslide-api==0.7.0 gotailwind==0.3.0 # homeassistant.components.govee_ble -govee-ble==0.44.0 +govee-ble==1.2.0 # homeassistant.components.govee_light_local govee-local-api==2.3.0 diff --git a/tests/components/anthropic/conftest.py b/tests/components/anthropic/conftest.py index 1a5512f0aae6b3..c6cfb733554cac 100644 --- a/tests/components/anthropic/conftest.py +++ b/tests/components/anthropic/conftest.py @@ -2,7 +2,7 @@ from collections.abc import AsyncGenerator, Generator, Iterable import datetime -from unittest.mock import AsyncMock, patch +from unittest.mock import DEFAULT, AsyncMock, patch from anthropic.pagination import AsyncPage from anthropic.types import ( @@ -239,8 +239,10 @@ async def mock_generator(events: Iterable[RawMessageStreamEvent], **kwargs): "anthropic.resources.messages.AsyncMessages.create", new_callable=AsyncMock, ) as mock_create: - mock_create.side_effect = lambda **kwargs: mock_generator( - mock_create.return_value.pop(0), **kwargs + mock_create.side_effect = lambda **kwargs: ( + mock_generator(mock_create.return_value.pop(0), **kwargs) + if isinstance(mock_create.return_value, list) + else DEFAULT ) yield mock_create diff --git a/tests/components/anthropic/test_ai_task.py b/tests/components/anthropic/test_ai_task.py index 6a7a1229b70ebd..f1abf956222831 100644 --- a/tests/components/anthropic/test_ai_task.py +++ b/tests/components/anthropic/test_ai_task.py @@ -3,6 +3,7 @@ from pathlib import Path from unittest.mock import AsyncMock, patch +from anthropic.types import Message, TextBlock, Usage from freezegun import freeze_time import pytest from syrupy.assertion import SnapshotAssertion @@ -71,7 +72,6 @@ async def test_empty_data( mock_config_entry: MockConfigEntry, mock_init_component, mock_create_stream: AsyncMock, - entity_registry: er.EntityRegistry, ) -> None: """Test AI Task data generation but the data returned is empty.""" mock_create_stream.return_value = [create_content_block(0, [""])] @@ -87,6 +87,31 @@ async def test_empty_data( ) +async def test_stream_wrong_type( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_init_component, + mock_create_stream: AsyncMock, +) -> None: + """Test error if the response is not a stream.""" + mock_create_stream.return_value = Message( + type="message", + id="message_id", + model="claude-opus-4-6", + role="assistant", + content=[TextBlock(type="text", text="This is not a stream")], + usage=Usage(input_tokens=42, output_tokens=42), + ) + + with pytest.raises(HomeAssistantError, match="Expected a stream of messages"): + await ai_task.async_generate_data( + hass, + task_name="Test Task", + entity_id="ai_task.claude_ai_task", + instructions="Generate test data", + ) + + @freeze_time("2026-01-01 12:00:00") async def test_generate_structured_data_legacy( hass: HomeAssistant, diff --git a/tests/components/anthropic/test_conversation.py b/tests/components/anthropic/test_conversation.py index 8c3327b6cc9642..3ff1ea5fb2d5af 100644 --- a/tests/components/anthropic/test_conversation.py +++ b/tests/components/anthropic/test_conversation.py @@ -8,10 +8,13 @@ from anthropic.types import ( CitationsWebSearchResultLocation, CitationWebSearchResultLocationParam, + Message, + TextBlock, TextEditorCodeExecutionCreateResultBlock, TextEditorCodeExecutionStrReplaceResultBlock, TextEditorCodeExecutionToolResultError, TextEditorCodeExecutionViewResultBlock, + Usage, WebSearchResultBlock, ) from anthropic.types.text_editor_code_execution_tool_result_block import ( @@ -584,6 +587,68 @@ async def test_refusal( ) +async def test_stream_wrong_type( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_init_component, + mock_create_stream: AsyncMock, +) -> None: + """Test error if the response is not a stream.""" + mock_create_stream.return_value = Message( + type="message", + id="message_id", + model="claude-opus-4-6", + role="assistant", + content=[TextBlock(type="text", text="This is not a stream")], + usage=Usage(input_tokens=42, output_tokens=42), + ) + + result = await conversation.async_converse( + hass, + "Hi", + None, + Context(), + agent_id="conversation.claude_conversation", + ) + + assert result.response.response_type == intent.IntentResponseType.ERROR + assert result.response.error_code == "unknown" + assert result.response.speech["plain"]["speech"] == "Expected a stream of messages" + + +async def test_double_system_messages( + hass: HomeAssistant, + mock_config_entry_with_assist: MockConfigEntry, + mock_init_component, + mock_create_stream: AsyncMock, +) -> None: + """Test error for two or more system prompts.""" + conversation_id = "conversation_id" + with ( + chat_session.async_get_chat_session(hass, conversation_id) as session, + conversation.async_get_chat_log(hass, session) as chat_log, + ): + chat_log.content = [ + conversation.chat_log.SystemContent("You are a helpful assistant."), + conversation.chat_log.SystemContent("And I am the user."), + ] + + result = await conversation.async_converse( + hass, + "What time is it?", + conversation_id, + Context(), + agent_id="conversation.claude_conversation", + ) + + assert result.response.response_type == intent.IntentResponseType.ERROR + assert result.response.error_code == "unknown" + assert ( + result.response.speech["plain"]["speech"] + == "Unexpected content type in chat log" + ) + + async def test_extended_thinking( hass: HomeAssistant, mock_config_entry: MockConfigEntry, diff --git a/tests/components/shelly/snapshots/test_devices.ambr b/tests/components/shelly/snapshots/test_devices.ambr index a9cd0823cc9c34..378d979244db96 100644 --- a/tests/components/shelly/snapshots/test_devices.ambr +++ b/tests/components/shelly/snapshots/test_devices.ambr @@ -5902,7 +5902,7 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, - 'unique_id': '123456789ABC-humidity:0-humidity_0', + 'unique_id': '123456789ABC-humidity:0-humidity_rh', 'unit_of_measurement': '%', }) # --- @@ -6178,7 +6178,7 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, - 'unique_id': '123456789ABC-temperature:0-temperature_0', + 'unique_id': '123456789ABC-temperature:0-temperature_tc', 'unit_of_measurement': , }) # --- @@ -11364,7 +11364,7 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, - 'unique_id': '123456789ABC-temperature:0-temperature_0', + 'unique_id': '123456789ABC-temperature:0-temperature_tc', 'unit_of_measurement': , }) # --- diff --git a/tests/components/shelly/test_coordinator.py b/tests/components/shelly/test_coordinator.py index 1fe881a3464b73..d0d41dda76b7e7 100644 --- a/tests/components/shelly/test_coordinator.py +++ b/tests/components/shelly/test_coordinator.py @@ -616,7 +616,7 @@ async def test_rpc_update_entry_sleep_period( hass, SENSOR_DOMAIN, "test_name_temperature", - "temperature:0-temperature_0", + "temperature:0-temperature_tc", entry, ) @@ -650,7 +650,7 @@ async def test_rpc_sleeping_device_no_periodic_updates( hass, SENSOR_DOMAIN, "test_name_temperature", - "temperature:0-temperature_0", + "temperature:0-temperature_tc", entry, ) diff --git a/tests/components/shelly/test_sensor.py b/tests/components/shelly/test_sensor.py index 56b24dec89a453..b44213cea2c4b8 100644 --- a/tests/components/shelly/test_sensor.py +++ b/tests/components/shelly/test_sensor.py @@ -637,9 +637,6 @@ async def test_rpc_sleeping_sensor( monkeypatch.setitem(mock_rpc_device.status["sys"], "wakeup_period", 1000) await init_integration(hass, 2, sleep_period=1000) - # Sensor should be created when device is online - assert hass.states.get(entity_id) is None - # Make device online mock_rpc_device.mock_online() await hass.async_block_till_done(wait_background_tasks=True) @@ -669,9 +666,6 @@ async def test_rpc_sleeping_sensor_with_channel_name( monkeypatch.setitem(mock_rpc_device.status["sys"], "wakeup_period", 1000) await init_integration(hass, 2, sleep_period=1000) - # Sensor should be created when device is online - assert hass.states.get(entity_id) is None - # Make device online mock_rpc_device.mock_online() await hass.async_block_till_done(wait_background_tasks=True) @@ -700,7 +694,7 @@ async def test_rpc_restored_sleeping_sensor( hass, SENSOR_DOMAIN, "test_name_temperature", - "temperature:0-temperature_0", + "temperature:0-temperature_tc", entry, device_id=device.id, ) @@ -747,7 +741,7 @@ async def test_rpc_restored_sleeping_sensor_no_last_state( hass, SENSOR_DOMAIN, "test_name_temperature", - "temperature:0-temperature_0", + "temperature:0-temperature_tc", entry, device_id=device.id, ) @@ -824,9 +818,6 @@ async def test_rpc_sleeping_update_entity_service( monkeypatch.setitem(mock_rpc_device.status["sys"], "wakeup_period", 1000) await init_integration(hass, 2, sleep_period=1000) - # Entity should be created when device is online - assert hass.states.get(entity_id) is None - # Make device online mock_rpc_device.mock_online() await hass.async_block_till_done(wait_background_tasks=True) @@ -846,7 +837,7 @@ async def test_rpc_sleeping_update_entity_service( assert state.state == "22.9" assert (entry := entity_registry.async_get(entity_id)) - assert entry.unique_id == "123456789ABC-temperature:0-temperature_0" + assert entry.unique_id == "123456789ABC-temperature:0-temperature_tc" assert ( "Entity sensor.test_name_temperature comes from a sleeping device" @@ -1219,6 +1210,54 @@ async def test_migrate_unique_id_virtual_components_roles( assert "Migrating unique_id for sensor.test_name_test_sensor" in caplog.text +@pytest.mark.parametrize( + ("old_unique_id", "new_unique_id", "entity_id"), + [ + ( + "123456789ABC-temperature:0-temperature_0", + "123456789ABC-temperature:0-temperature_tc", + "sensor.test_name_temperature", + ), + ( + "123456789ABC-humidity:0-humidity_0", + "123456789ABC-humidity:0-humidity_rh", + "sensor.test_name_humidity", + ), + ], +) +@pytest.mark.usefixtures("disable_async_remove_shelly_rpc_entities") +async def test_migrate_unique_id_rpc_sensor_description_key_rename( + hass: HomeAssistant, + mock_rpc_device: Mock, + entity_registry: EntityRegistry, + caplog: pytest.LogCaptureFixture, + old_unique_id: str, + new_unique_id: str, + entity_id: str, +) -> None: + """Test migration of RPC sensor unique_id after description key rename.""" + entry = await init_integration(hass, 2, skip_setup=True) + + entity = entity_registry.async_get_or_create( + suggested_object_id=entity_id.split(".")[1], + disabled_by=None, + domain=SENSOR_DOMAIN, + platform=DOMAIN, + unique_id=old_unique_id, + config_entry=entry, + ) + assert entity.unique_id == old_unique_id + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + entity_entry = entity_registry.async_get(entity_id) + assert entity_entry + assert entity_entry.unique_id == new_unique_id + + assert f"Migrating unique_id for {entity_id} entity" in caplog.text + + @pytest.mark.usefixtures("disable_async_remove_shelly_rpc_entities") async def test_rpc_remove_text_virtual_sensor_when_mode_field( hass: HomeAssistant,