Compare commits

...

32 Commits

Author SHA1 Message Date
Franck Nijhof
52fb0343e4 Bump version to 2026.2.0b4 2026-02-04 16:14:23 +00:00
Bram Kragten
1050b4580a Update frontend to 20260128.6 (#162214) 2026-02-04 16:10:08 +00:00
Åke Strandberg
344c42172e Add missing codes for Miele coffe systems (#162206) 2026-02-04 16:10:06 +00:00
Michael Hansen
93cc0fd7f1 Bump intents (#162205) 2026-02-04 16:10:05 +00:00
andreimoraru
05fe636b55 Bump yt-dlp to 2026.02.04 (#162204)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-02-04 16:10:03 +00:00
Marc Mueller
f22467d099 Pin auth0-python to <5.0 (#162203) 2026-02-04 16:10:01 +00:00
TheJulianJES
4bc3899b32 Bump ZHA to 0.0.89 (#162195) 2026-02-04 16:10:00 +00:00
Oliver
fc4d6bf5f1 Bump denonavr to 1.3.1 (#162183) 2026-02-04 16:09:58 +00:00
johanzander
8ed0672a8f Bump growattServer to 1.9.0 (#162179)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 16:09:57 +00:00
Norbert Rittel
282e347a1b Clarify action descriptions in media_player (#162172) 2026-02-04 16:09:55 +00:00
Erik Montnemery
1bfb02b440 Bump python-otbr-api to 2.8.0 (#162167) 2026-02-04 16:09:54 +00:00
Przemko92
71b03bd9ae Bump compit-inext-api to 0.8.0 (#162166) 2026-02-04 16:09:52 +00:00
Przemko92
cbd69822eb Update compit-inext-api to 0.7.0 (#162020) 2026-02-04 16:09:51 +00:00
Denis Shulyaka
db900f4dd2 Anthropic repair deprecated models (#162162)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-04 16:00:14 +00:00
Jonathan Bangert
a707e695bc Bump bleak-esphome to 3.6.0 (#162028) 2026-02-04 16:00:12 +00:00
Liquidmasl
4feceac205 Jellyfin native client controls (#161982) 2026-02-04 16:00:11 +00:00
Petro31
10c20faaca Fix template weather humidity (#161945) 2026-02-04 16:00:09 +00:00
Robert Svensson
abcd512401 Add missing OUI to Axis integration, discovery would abort with unsup… (#161943) 2026-02-04 16:00:07 +00:00
Bram Kragten
fdf8edf474 Bump version to 2026.2.0b3 2026-02-03 18:03:54 +01:00
Bram Kragten
47e1a98bee Update frontend to 20260128.5 (#162156) 2026-02-03 18:03:04 +01:00
Joost Lekkerkerker
2d8572b943 Add Heatit virtual brand (#162155) 2026-02-03 18:03:02 +01:00
Joost Lekkerkerker
660cfdbd50 Add Heiman virtual brand (#162152) 2026-02-03 18:03:00 +01:00
Steven Travers
4208595da6 Modify Analytics text on feature labs (#162151) 2026-02-03 18:02:59 +01:00
Paul Bottein
b6b2d2fc6f Update title and description of YAML dashboard repair (#162138) 2026-02-03 18:02:58 +01:00
victorigualada
6c4c632848 Handle chat log attachments in Cloud integration (#162121) 2026-02-03 18:02:57 +01:00
Shay Levy
78cf62176f Fix Shelly xpercent sensor state_class (#162107) 2026-02-03 18:02:56 +01:00
Denis Shulyaka
df971c7a42 Anthropic: Switch default model to Haiku 4.5 (#162093) 2026-02-03 18:02:55 +01:00
mezz64
1fcabb7f2d Bump pyhik to 0.4.2 (#162092)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-02-03 18:02:53 +01:00
Åke Strandberg
9fb60c9ea2 Update Senz temperature sensor (#162016) 2026-02-03 18:02:52 +01:00
J. Diego Rodríguez Royo
9c11a4646f Remove coffee machine's hot water sensor's state class at Home Connect (#161246) 2026-02-03 17:58:47 +01:00
jameson_uk
b036a78776 Remove invalid notification sensors for Alexa devices (#160422)
Co-authored-by: Simone Chemelli <simone.chemelli@gmail.com>
2026-02-03 17:58:45 +01:00
Kamil Breguła
60bb3cb704 Handle missing battery stats in systemmonitor (#158287)
Co-authored-by: mik-laj <12058428+mik-laj@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-02-03 17:58:43 +01:00
60 changed files with 1358 additions and 139 deletions

View File

@@ -0,0 +1,5 @@
{
"domain": "heatit",
"name": "Heatit",
"iot_standards": ["zwave"]
}

View File

@@ -0,0 +1,5 @@
{
"domain": "heiman",
"name": "Heiman",
"iot_standards": ["matter", "zigbee"]
}

View File

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

View File

@@ -28,6 +28,7 @@ from homeassistant.helpers.typing import StateType
from .const import CATEGORY_NOTIFICATIONS, CATEGORY_SENSORS
from .coordinator import AmazonConfigEntry
from .entity import AmazonEntity
from .utils import async_remove_unsupported_notification_sensors
# Coordinator is used to centralize the data updates
PARALLEL_UPDATES = 0
@@ -105,6 +106,9 @@ async def async_setup_entry(
coordinator = entry.runtime_data
# Remove notification sensors from unsupported devices
await async_remove_unsupported_notification_sensors(hass, coordinator)
known_devices: set[str] = set()
def _check_device() -> None:
@@ -122,6 +126,7 @@ async def async_setup_entry(
AmazonSensorEntity(coordinator, serial_num, notification_desc)
for notification_desc in NOTIFICATIONS
for serial_num in new_devices
if coordinator.data[serial_num].notifications_supported
]
async_add_entities(sensors_list + notifications_list)

View File

@@ -5,8 +5,14 @@ from functools import wraps
from typing import Any, Concatenate
from aioamazondevices.const.devices import SPEAKER_GROUP_FAMILY
from aioamazondevices.const.schedules import (
NOTIFICATION_ALARM,
NOTIFICATION_REMINDER,
NOTIFICATION_TIMER,
)
from aioamazondevices.exceptions import CannotConnect, CannotRetrieveData
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
@@ -81,3 +87,27 @@ async def async_remove_dnd_from_virtual_group(
if entity_id and is_group:
entity_registry.async_remove(entity_id)
_LOGGER.debug("Removed DND switch from virtual group %s", entity_id)
async def async_remove_unsupported_notification_sensors(
hass: HomeAssistant,
coordinator: AmazonDevicesCoordinator,
) -> None:
"""Remove notification sensors from unsupported devices."""
entity_registry = er.async_get(hass)
for serial_num in coordinator.data:
for notification_key in (
NOTIFICATION_ALARM,
NOTIFICATION_REMINDER,
NOTIFICATION_TIMER,
):
unique_id = f"{serial_num}-{notification_key}"
entity_id = entity_registry.async_get_entity_id(
domain=SENSOR_DOMAIN, platform=DOMAIN, unique_id=unique_id
)
is_unsupported = not coordinator.data[serial_num].notifications_supported
if entity_id and is_unsupported:
entity_registry.async_remove(entity_id)
_LOGGER.debug("Removed unsupported notification sensor %s", entity_id)

View File

@@ -1,7 +1,7 @@
{
"preview_features": {
"snapshots": {
"description": "This free, open source device database of the Open Home Foundation helps users find useful information about smart home devices used in real installations.\n\nYou can help build it by anonymously sharing data about your devices. Only device-specific details (like model or manufacturer) are shared — never personally identifying information (like the names you assign).\n\nLearn more about the device database and how we process your data in our [Data Use Statement](https://www.openhomefoundation.org/device-database-data-use-statement), which you accept by opting in.",
"description": "We're creating the [Open Home Foundation Device Database](https://www.home-assistant.io/blog/2026/02/02/about-device-database/): a free, open source community-powered resource to help users find practical information about how smart home devices perform in real installations.\n\nYou can help us build it by opting in to share anonymized data about your devices. This data will only ever include device-specific details (like model or manufacturer) never personally identifying information (like the names you assign).\n\nFind out how we process your data (should you choose to contribute) in our [Data Use Statement](https://www.openhomefoundation.org/device-database-data-use-statement).",
"disable_confirmation": "Your data will no longer be shared with the Open Home Foundation's device database.",
"enable_confirmation": "This feature is still in development and may change. The device database is being refined based on user feedback and is not yet complete.",
"name": "Device database"

View File

@@ -14,10 +14,18 @@ from homeassistant.helpers import (
config_validation as cv,
device_registry as dr,
entity_registry as er,
issue_registry as ir,
)
from homeassistant.helpers.typing import ConfigType
from .const import DEFAULT_CONVERSATION_NAME, DOMAIN, LOGGER
from .const import (
CONF_CHAT_MODEL,
DATA_REPAIR_DEFER_RELOAD,
DEFAULT_CONVERSATION_NAME,
DEPRECATED_MODELS,
DOMAIN,
LOGGER,
)
PLATFORMS = (Platform.AI_TASK, Platform.CONVERSATION)
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
@@ -27,6 +35,7 @@ type AnthropicConfigEntry = ConfigEntry[anthropic.AsyncClient]
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up Anthropic."""
hass.data.setdefault(DOMAIN, {}).setdefault(DATA_REPAIR_DEFER_RELOAD, set())
await async_migrate_integration(hass)
return True
@@ -50,6 +59,22 @@ async def async_setup_entry(hass: HomeAssistant, entry: AnthropicConfigEntry) ->
entry.async_on_unload(entry.add_update_listener(async_update_options))
for subentry in entry.subentries.values():
if (model := subentry.data.get(CONF_CHAT_MODEL)) and model.startswith(
tuple(DEPRECATED_MODELS)
):
ir.async_create_issue(
hass,
DOMAIN,
"model_deprecated",
is_fixable=True,
is_persistent=False,
learn_more_url="https://platform.claude.com/docs/en/about-claude/model-deprecations",
severity=ir.IssueSeverity.WARNING,
translation_key="model_deprecated",
)
break
return True
@@ -62,6 +87,11 @@ async def async_update_options(
hass: HomeAssistant, entry: AnthropicConfigEntry
) -> None:
"""Update options."""
defer_reload_entries: set[str] = hass.data.setdefault(DOMAIN, {}).setdefault(
DATA_REPAIR_DEFER_RELOAD, set()
)
if entry.entry_id in defer_reload_entries:
return
await hass.config_entries.async_reload(entry.entry_id)

View File

@@ -92,6 +92,40 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> None:
await client.models.list(timeout=10.0)
async def get_model_list(client: anthropic.AsyncAnthropic) -> list[SelectOptionDict]:
"""Get list of available models."""
try:
models = (await client.models.list()).data
except anthropic.AnthropicError:
models = []
_LOGGER.debug("Available models: %s", models)
model_options: list[SelectOptionDict] = []
short_form = re.compile(r"[^\d]-\d$")
for model_info in models:
# Resolve alias from versioned model name:
model_alias = (
model_info.id[:-9]
if model_info.id
not in (
"claude-3-haiku-20240307",
"claude-3-5-haiku-20241022",
"claude-3-opus-20240229",
)
else model_info.id
)
if short_form.search(model_alias):
model_alias += "-0"
if model_alias.endswith(("haiku", "opus", "sonnet")):
model_alias += "-latest"
model_options.append(
SelectOptionDict(
label=model_info.display_name,
value=model_alias,
)
)
return model_options
class AnthropicConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Anthropic."""
@@ -401,38 +435,13 @@ class ConversationSubentryFlowHandler(ConfigSubentryFlow):
async def _get_model_list(self) -> list[SelectOptionDict]:
"""Get list of available models."""
try:
client = await self.hass.async_add_executor_job(
partial(
anthropic.AsyncAnthropic,
api_key=self._get_entry().data[CONF_API_KEY],
)
client = await self.hass.async_add_executor_job(
partial(
anthropic.AsyncAnthropic,
api_key=self._get_entry().data[CONF_API_KEY],
)
models = (await client.models.list()).data
except anthropic.AnthropicError:
models = []
_LOGGER.debug("Available models: %s", models)
model_options: list[SelectOptionDict] = []
short_form = re.compile(r"[^\d]-\d$")
for model_info in models:
# Resolve alias from versioned model name:
model_alias = (
model_info.id[:-9]
if model_info.id
not in ("claude-3-haiku-20240307", "claude-3-opus-20240229")
else model_info.id
)
if short_form.search(model_alias):
model_alias += "-0"
if model_alias.endswith(("haiku", "opus", "sonnet")):
model_alias += "-latest"
model_options.append(
SelectOptionDict(
label=model_info.display_name,
value=model_alias,
)
)
return model_options
)
return await get_model_list(client)
async def _get_location_data(self) -> dict[str, str]:
"""Get approximate location data of the user."""

View File

@@ -22,8 +22,10 @@ CONF_WEB_SEARCH_REGION = "region"
CONF_WEB_SEARCH_COUNTRY = "country"
CONF_WEB_SEARCH_TIMEZONE = "timezone"
DATA_REPAIR_DEFER_RELOAD = "repair_defer_reload"
DEFAULT = {
CONF_CHAT_MODEL: "claude-3-5-haiku-latest",
CONF_CHAT_MODEL: "claude-haiku-4-5",
CONF_MAX_TOKENS: 3000,
CONF_TEMPERATURE: 1.0,
CONF_THINKING_BUDGET: 0,
@@ -46,3 +48,10 @@ WEB_SEARCH_UNSUPPORTED_MODELS = [
"claude-3-5-sonnet-20240620",
"claude-3-5-sonnet-20241022",
]
DEPRECATED_MODELS = [
"claude-3-5-haiku",
"claude-3-7-sonnet",
"claude-3-5-sonnet",
"claude-3-opus",
]

View File

@@ -0,0 +1,275 @@
"""Issue repair flow for Anthropic."""
from __future__ import annotations
from collections.abc import Iterator
from typing import cast
import voluptuous as vol
from homeassistant import data_entry_flow
from homeassistant.components.repairs import RepairsFlow
from homeassistant.config_entries import ConfigEntry, ConfigEntryState, ConfigSubentry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.selector import SelectSelector, SelectSelectorConfig
from .config_flow import get_model_list
from .const import (
CONF_CHAT_MODEL,
DATA_REPAIR_DEFER_RELOAD,
DEFAULT,
DEPRECATED_MODELS,
DOMAIN,
)
class ModelDeprecatedRepairFlow(RepairsFlow):
"""Handler for an issue fixing flow."""
_subentry_iter: Iterator[tuple[str, str]] | None
_current_entry_id: str | None
_current_subentry_id: str | None
_reload_pending: set[str]
_pending_updates: dict[str, dict[str, str]]
def __init__(self) -> None:
"""Initialize the flow."""
super().__init__()
self._subentry_iter = None
self._current_entry_id = None
self._current_subentry_id = None
self._reload_pending = set()
self._pending_updates = {}
async def async_step_init(
self, user_input: dict[str, str] | None = None
) -> data_entry_flow.FlowResult:
"""Handle the first step of a fix flow."""
previous_entry_id: str | None = None
if user_input is not None:
previous_entry_id = self._async_update_current_subentry(user_input)
self._clear_current_target()
target = await self._async_next_target()
next_entry_id = target[0].entry_id if target else None
if previous_entry_id and previous_entry_id != next_entry_id:
await self._async_apply_pending_updates(previous_entry_id)
if target is None:
await self._async_apply_all_pending_updates()
return self.async_create_entry(data={})
entry, subentry, model = target
client = entry.runtime_data
model_list = [
model_option
for model_option in await get_model_list(client)
if not model_option["value"].startswith(tuple(DEPRECATED_MODELS))
]
if "opus" in model:
suggested_model = "claude-opus-4-5"
elif "haiku" in model:
suggested_model = "claude-haiku-4-5"
elif "sonnet" in model:
suggested_model = "claude-sonnet-4-5"
else:
suggested_model = cast(str, DEFAULT[CONF_CHAT_MODEL])
schema = vol.Schema(
{
vol.Required(
CONF_CHAT_MODEL,
default=suggested_model,
): SelectSelector(
SelectSelectorConfig(options=model_list, custom_value=True)
),
}
)
return self.async_show_form(
step_id="init",
data_schema=schema,
description_placeholders={
"entry_name": entry.title,
"model": model,
"subentry_name": subentry.title,
"subentry_type": self._format_subentry_type(subentry.subentry_type),
},
)
def _iter_deprecated_subentries(self) -> Iterator[tuple[str, str]]:
"""Yield entry/subentry pairs that use deprecated models."""
for entry in self.hass.config_entries.async_entries(DOMAIN):
if entry.state is not ConfigEntryState.LOADED:
continue
for subentry in entry.subentries.values():
model = subentry.data.get(CONF_CHAT_MODEL)
if model and model.startswith(tuple(DEPRECATED_MODELS)):
yield entry.entry_id, subentry.subentry_id
async def _async_next_target(
self,
) -> tuple[ConfigEntry, ConfigSubentry, str] | None:
"""Return the next deprecated subentry target."""
if self._subentry_iter is None:
self._subentry_iter = self._iter_deprecated_subentries()
while True:
try:
entry_id, subentry_id = next(self._subentry_iter)
except StopIteration:
return None
entry = self.hass.config_entries.async_get_entry(entry_id)
if entry is None:
continue
subentry = entry.subentries.get(subentry_id)
if subentry is None:
continue
model = self._pending_model(entry_id, subentry_id)
if model is None:
model = subentry.data.get(CONF_CHAT_MODEL)
if not model or not model.startswith(tuple(DEPRECATED_MODELS)):
continue
self._current_entry_id = entry_id
self._current_subentry_id = subentry_id
return entry, subentry, model
def _async_update_current_subentry(self, user_input: dict[str, str]) -> str | None:
"""Update the currently selected subentry."""
if not self._current_entry_id or not self._current_subentry_id:
return None
entry = self.hass.config_entries.async_get_entry(self._current_entry_id)
if entry is None:
return None
subentry = entry.subentries.get(self._current_subentry_id)
if subentry is None:
return None
updated_data = {
**subentry.data,
CONF_CHAT_MODEL: user_input[CONF_CHAT_MODEL],
}
if updated_data == subentry.data:
return entry.entry_id
self._queue_pending_update(
entry.entry_id,
subentry.subentry_id,
updated_data[CONF_CHAT_MODEL],
)
return entry.entry_id
def _clear_current_target(self) -> None:
"""Clear current target tracking."""
self._current_entry_id = None
self._current_subentry_id = None
def _format_subentry_type(self, subentry_type: str) -> str:
"""Return a user-friendly subentry type label."""
if subentry_type == "conversation":
return "Conversation agent"
if subentry_type in ("ai_task", "ai_task_data"):
return "AI task"
return subentry_type
def _queue_pending_update(
self, entry_id: str, subentry_id: str, model: str
) -> None:
"""Store a pending model update for a subentry."""
self._pending_updates.setdefault(entry_id, {})[subentry_id] = model
def _pending_model(self, entry_id: str, subentry_id: str) -> str | None:
"""Return a pending model update if one exists."""
return self._pending_updates.get(entry_id, {}).get(subentry_id)
def _mark_entry_for_reload(self, entry_id: str) -> None:
"""Prevent reload until repairs are complete for the entry."""
self._reload_pending.add(entry_id)
defer_reload_entries: set[str] = self.hass.data.setdefault(
DOMAIN, {}
).setdefault(DATA_REPAIR_DEFER_RELOAD, set())
defer_reload_entries.add(entry_id)
async def _async_reload_entry(self, entry_id: str) -> None:
"""Reload an entry once all repairs are completed."""
if entry_id not in self._reload_pending:
return
entry = self.hass.config_entries.async_get_entry(entry_id)
if entry is not None and entry.state is not ConfigEntryState.LOADED:
self._clear_defer_reload(entry_id)
self._reload_pending.discard(entry_id)
return
if entry is not None:
await self.hass.config_entries.async_reload(entry_id)
self._clear_defer_reload(entry_id)
self._reload_pending.discard(entry_id)
def _clear_defer_reload(self, entry_id: str) -> None:
"""Remove entry from the deferred reload set."""
defer_reload_entries: set[str] = self.hass.data.setdefault(
DOMAIN, {}
).setdefault(DATA_REPAIR_DEFER_RELOAD, set())
defer_reload_entries.discard(entry_id)
async def _async_apply_pending_updates(self, entry_id: str) -> None:
"""Apply pending subentry updates for a single entry."""
updates = self._pending_updates.pop(entry_id, None)
if not updates:
return
entry = self.hass.config_entries.async_get_entry(entry_id)
if entry is None or entry.state is not ConfigEntryState.LOADED:
return
changed = False
for subentry_id, model in updates.items():
subentry = entry.subentries.get(subentry_id)
if subentry is None:
continue
updated_data = {
**subentry.data,
CONF_CHAT_MODEL: model,
}
if updated_data == subentry.data:
continue
if not changed:
self._mark_entry_for_reload(entry_id)
changed = True
self.hass.config_entries.async_update_subentry(
entry,
subentry,
data=updated_data,
)
if not changed:
return
await self._async_reload_entry(entry_id)
async def _async_apply_all_pending_updates(self) -> None:
"""Apply all pending updates across entries."""
for entry_id in list(self._pending_updates):
await self._async_apply_pending_updates(entry_id)
async def async_create_fix_flow(
hass: HomeAssistant,
issue_id: str,
data: dict[str, str | int | float | None] | None,
) -> RepairsFlow:
"""Create flow."""
if issue_id == "model_deprecated":
return ModelDeprecatedRepairFlow()
raise HomeAssistantError("Unknown issue ID")

View File

@@ -109,5 +109,21 @@
}
}
}
},
"issues": {
"model_deprecated": {
"fix_flow": {
"step": {
"init": {
"data": {
"chat_model": "[%key:common::generic::model%]"
},
"description": "You are updating {subentry_name} ({subentry_type}) in {entry_name}. The current model {model} is deprecated. Select a supported model to continue.",
"title": "Update model"
}
}
},
"title": "Model deprecated"
}
}
}

View File

@@ -52,7 +52,7 @@ from .const import (
from .errors import AuthenticationRequired, CannotConnect
from .hub import AxisHub, get_axis_api
AXIS_OUI = {"00:40:8c", "ac:cc:8e", "b8:a4:4f"}
AXIS_OUI = {"00:40:8c", "ac:cc:8e", "b8:a4:4f", "e8:27:25"}
DEFAULT_PORT = 443
DEFAULT_PROTOCOL = "https"
PROTOCOL_CHOICES = ["https", "http"]

View File

@@ -459,8 +459,17 @@ class BaseCloudLLMEntity(Entity):
last_content: Any = chat_log.content[-1]
if last_content.role == "user" and last_content.attachments:
files = await self._async_prepare_files_for_prompt(last_content.attachments)
current_content = last_content.content
last_content = [*(current_content or []), *files]
last_message = cast(dict[str, Any], messages[-1])
assert (
last_message["type"] == "message"
and last_message["role"] == "user"
and isinstance(last_message["content"], str)
)
last_message["content"] = [
{"type": "input_text", "text": last_message["content"]},
*files,
]
tools: list[ToolParam] = []
tool_choice: str | None = None

View File

@@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["compit"],
"quality_scale": "bronze",
"requirements": ["compit-inext-api==0.6.0"]
"requirements": ["compit-inext-api==0.8.0"]
}

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/conversation",
"integration_type": "entity",
"quality_scale": "internal",
"requirements": ["hassil==3.5.0", "home-assistant-intents==2026.1.28"]
"requirements": ["hassil==3.5.0", "home-assistant-intents==2026.2.3"]
}

View File

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

View File

@@ -19,7 +19,7 @@
"requirements": [
"aioesphomeapi==43.14.0",
"esphome-dashboard-api==1.3.0",
"bleak-esphome==3.5.0"
"bleak-esphome==3.6.0"
],
"zeroconf": ["_esphomelib._tcp.local."]
}

View File

@@ -21,5 +21,5 @@
"integration_type": "system",
"preview_features": { "winter_mode": {} },
"quality_scale": "internal",
"requirements": ["home-assistant-frontend==20260128.4"]
"requirements": ["home-assistant-frontend==20260128.6"]
}

View File

@@ -7,5 +7,5 @@
"integration_type": "hub",
"iot_class": "cloud_polling",
"loggers": ["growattServer"],
"requirements": ["growattServer==1.7.1"]
"requirements": ["growattServer==1.9.0"]
}

View File

@@ -8,5 +8,5 @@
"iot_class": "local_push",
"loggers": ["pyhik"],
"quality_scale": "legacy",
"requirements": ["pyHik==0.4.1"]
"requirements": ["pyHik==0.4.2"]
}

View File

@@ -115,7 +115,6 @@ SENSORS = (
entity_category=EntityCategory.DIAGNOSTIC,
native_unit_of_measurement=UnitOfVolume.MILLILITERS,
device_class=SensorDeviceClass.VOLUME,
state_class=SensorStateClass.TOTAL_INCREASING,
translation_key="hot_water_counter",
),
HomeConnectSensorEntityDescription(

View File

@@ -150,7 +150,9 @@ class JellyfinMediaPlayer(JellyfinClientEntity, MediaPlayerEntity):
self._attr_state = state
self._attr_is_volume_muted = volume_muted
self._attr_volume_level = volume_level
# Only update volume_level if the API provides it, otherwise preserve current value
if volume_level is not None:
self._attr_volume_level = volume_level
self._attr_media_content_type = media_content_type
self._attr_media_content_id = media_content_id
self._attr_media_title = media_title
@@ -190,7 +192,9 @@ class JellyfinMediaPlayer(JellyfinClientEntity, MediaPlayerEntity):
)
features = MediaPlayerEntityFeature(0)
if "PlayMediaSource" in commands:
if "PlayMediaSource" in commands or self.capabilities.get(
"SupportsMediaControl", False
):
features |= (
MediaPlayerEntityFeature.BROWSE_MEDIA
| MediaPlayerEntityFeature.PLAY_MEDIA
@@ -201,10 +205,10 @@ class JellyfinMediaPlayer(JellyfinClientEntity, MediaPlayerEntity):
| MediaPlayerEntityFeature.SEARCH_MEDIA
)
if "Mute" in commands:
if "Mute" in commands and "Unmute" in commands:
features |= MediaPlayerEntityFeature.VOLUME_MUTE
if "VolumeSet" in commands:
if "VolumeSet" in commands or "SetVolume" in commands:
features |= MediaPlayerEntityFeature.VOLUME_SET
return features
@@ -219,11 +223,13 @@ class JellyfinMediaPlayer(JellyfinClientEntity, MediaPlayerEntity):
"""Send pause command."""
self.coordinator.api_client.jellyfin.remote_pause(self.session_id)
self._attr_state = MediaPlayerState.PAUSED
self.schedule_update_ha_state()
def media_play(self) -> None:
"""Send play command."""
self.coordinator.api_client.jellyfin.remote_unpause(self.session_id)
self._attr_state = MediaPlayerState.PLAYING
self.schedule_update_ha_state()
def media_play_pause(self) -> None:
"""Send the PlayPause command to the session."""
@@ -233,6 +239,7 @@ class JellyfinMediaPlayer(JellyfinClientEntity, MediaPlayerEntity):
"""Send stop command."""
self.coordinator.api_client.jellyfin.remote_stop(self.session_id)
self._attr_state = MediaPlayerState.IDLE
self.schedule_update_ha_state()
def play_media(
self, media_type: MediaType | str, media_id: str, **kwargs: Any
@@ -247,6 +254,8 @@ class JellyfinMediaPlayer(JellyfinClientEntity, MediaPlayerEntity):
self.coordinator.api_client.jellyfin.remote_set_volume(
self.session_id, int(volume * 100)
)
self._attr_volume_level = volume
self.schedule_update_ha_state()
def mute_volume(self, mute: bool) -> None:
"""Mute the volume."""
@@ -254,6 +263,8 @@ class JellyfinMediaPlayer(JellyfinClientEntity, MediaPlayerEntity):
self.coordinator.api_client.jellyfin.remote_mute(self.session_id)
else:
self.coordinator.api_client.jellyfin.remote_unmute(self.session_id)
self._attr_is_volume_muted = mute
self.schedule_update_ha_state()
async def async_browse_media(
self,

View File

@@ -6,8 +6,8 @@
},
"issues": {
"yaml_mode_deprecated": {
"description": "The `mode` option in `lovelace:` configuration is deprecated and will be removed in Home Assistant 2026.8.\n\nTo migrate:\n\n1. Remove `mode: yaml` from `lovelace:` in your `configuration.yaml`\n2. If you have `resources:` declared in your lovelace configuration, add `resource_mode: yaml` to keep loading resources from YAML\n3. Add a dashboard entry in your `configuration.yaml`:\n\n ```yaml\n lovelace:\n resource_mode: yaml # Add this if you have resources declared\n dashboards:\n lovelace:\n mode: yaml\n filename: {config_file}\n title: Overview\n icon: mdi:view-dashboard\n show_in_sidebar: true\n ```\n\n4. Restart Home Assistant",
"title": "Lovelace YAML mode deprecated"
"description": "Your YAML dashboard configuration uses the legacy `mode: yaml` option, which will be removed in Home Assistant 2026.8. Your YAML dashboards will continue to work, you just need to update how they are defined.\n\nTo update your configuration:\n\n1. Remove `mode: yaml` from `lovelace:` in your `configuration.yaml`\n2. Add a dashboard entry instead:\n\n ```yaml\n lovelace:\n resource_mode: yaml\n dashboards:\n lovelace:\n mode: yaml\n filename: {config_file}\n title: Overview\n icon: mdi:view-dashboard\n show_in_sidebar: true\n ```\n\n3. Restart Home Assistant\n\nNote: `resource_mode: yaml` keeps loading resources from YAML. If you want to manage resources through the UI instead, you can remove this line and move your resources to Settings > Dashboards > Resources.",
"title": "Lovelace YAML configuration needs update"
}
},
"services": {

View File

@@ -8,6 +8,6 @@
"iot_class": "calculated",
"loggers": ["yt_dlp"],
"quality_scale": "internal",
"requirements": ["yt-dlp[default]==2025.12.08"],
"requirements": ["yt-dlp[default]==2026.02.04"],
"single_config_entry": true
}

View File

@@ -266,7 +266,7 @@
"name": "Browse media"
},
"clear_playlist": {
"description": "Removes all items from the playlist.",
"description": "Removes all items from a media player's playlist.",
"name": "Clear playlist"
},
"join": {
@@ -284,15 +284,15 @@
"name": "Next"
},
"media_pause": {
"description": "Pauses.",
"description": "Pauses playback on a media player.",
"name": "[%key:common::action::pause%]"
},
"media_play": {
"description": "Starts playing.",
"description": "Starts playback on a media player.",
"name": "Play"
},
"media_play_pause": {
"description": "Toggles play/pause.",
"description": "Toggles play/pause on a media player.",
"name": "Play/Pause"
},
"media_previous_track": {
@@ -310,7 +310,7 @@
"name": "Seek"
},
"media_stop": {
"description": "Stops playing.",
"description": "Stops playback on a media player.",
"name": "[%key:common::action::stop%]"
},
"play_media": {
@@ -374,7 +374,7 @@
"name": "Select sound mode"
},
"select_source": {
"description": "Sends the media player the command to change input source.",
"description": "Sends a media player the command to change the input source.",
"fields": {
"source": {
"description": "Name of the source to switch to. Platform dependent.",
@@ -398,23 +398,23 @@
"name": "[%key:common::action::toggle%]"
},
"turn_off": {
"description": "Turns off the power of the media player.",
"description": "Turns off the power of a media player.",
"name": "[%key:common::action::turn_off%]"
},
"turn_on": {
"description": "Turns on the power of the media player.",
"description": "Turns on the power of a media player.",
"name": "[%key:common::action::turn_on%]"
},
"unjoin": {
"description": "Removes the player from a group. Only works on platforms which support player groups.",
"description": "Removes a media player from a group. Only works on platforms which support player groups.",
"name": "Unjoin"
},
"volume_down": {
"description": "Turns down the volume.",
"description": "Turns down the volume of a media player.",
"name": "Turn down volume"
},
"volume_mute": {
"description": "Mutes or unmutes the media player.",
"description": "Mutes or unmutes a media player.",
"fields": {
"is_volume_muted": {
"description": "Defines whether or not it is muted.",
@@ -424,7 +424,7 @@
"name": "Mute/unmute volume"
},
"volume_set": {
"description": "Sets the volume level.",
"description": "Sets the volume level of a media player.",
"fields": {
"volume_level": {
"description": "The volume. 0 is inaudible, 1 is the maximum volume.",
@@ -434,7 +434,7 @@
"name": "Set volume"
},
"volume_up": {
"description": "Turns up the volume.",
"description": "Turns up the volume of a media player.",
"name": "Turn up volume"
}
},

View File

@@ -806,6 +806,8 @@ class CoffeeSystemProgramId(MieleEnum, missing_to_none=True):
24813, # add profile
)
appliance_rinse = 24750, 24759, 24773, 24787, 24788
intermediate_rinsing = 24758
automatic_maintenance = 24778
descaling = 24751
brewing_unit_degrease = 24753
milk_pipework_rinse = 24754

View File

@@ -288,6 +288,7 @@
"auto": "[%key:common::state::auto%]",
"auto_roast": "Auto roast",
"automatic": "Automatic",
"automatic_maintenance": "Automatic maintenance",
"automatic_plus": "Automatic plus",
"baguettes": "Baguettes",
"barista_assistant": "BaristaAssistant",
@@ -551,6 +552,7 @@
"hygiene": "Hygiene",
"intensive": "Intensive",
"intensive_bake": "Intensive bake",
"intermediate_rinsing": "Intermediate rinsing",
"iridescent_shark_fillet": "Iridescent shark (fillet)",
"japanese_tea": "Japanese tea",
"jasmine_rice_rapid_steam_cooking": "Jasmine rice (rapid steam cooking)",

View File

@@ -8,5 +8,5 @@
"documentation": "https://www.home-assistant.io/integrations/otbr",
"integration_type": "service",
"iot_class": "local_polling",
"requirements": ["python-otbr-api==2.7.1"]
"requirements": ["python-otbr-api==2.8.0"]
}

View File

@@ -14,7 +14,7 @@ from homeassistant.components.sensor import (
SensorStateClass,
)
from homeassistant.const import UnitOfTemperature
from homeassistant.core import HomeAssistant
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
@@ -81,6 +81,12 @@ class SENZSensor(CoordinatorEntity[SENZDataUpdateCoordinator], SensorEntity):
serial_number=thermostat.serial_number,
)
@callback
def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""
self._thermostat = self.coordinator.data[self._thermostat.serial_number]
self.async_write_ha_state()
@property
def available(self) -> bool:
"""Return True if the thermostat is available."""

View File

@@ -1283,6 +1283,7 @@ RPC_SENSORS: Final = {
key="voltmeter",
sub_key="xvoltage",
translation_key="voltmeter_value",
state_class=SensorStateClass.MEASUREMENT,
removal_condition=lambda _, status, key: (status[key].get("xvoltage") is None),
unit=lambda config: config["xvoltage"]["unit"] or None,
),
@@ -1300,6 +1301,7 @@ RPC_SENSORS: Final = {
key="input",
sub_key="xpercent",
translation_key="analog_value",
state_class=SensorStateClass.MEASUREMENT,
removal_condition=lambda config, status, key: (
config[key]["type"] != "analog"
or config[key]["enable"] is False
@@ -1344,6 +1346,7 @@ RPC_SENSORS: Final = {
key="input",
sub_key="xfreq",
translation_key="pulse_counter_frequency_value",
state_class=SensorStateClass.MEASUREMENT,
removal_condition=lambda config, status, key: (
config[key]["type"] != "count"
or config[key]["enable"] is False

View File

@@ -284,6 +284,8 @@ class SystemMonitorCoordinator(TimestampDataUpdateCoordinator[SensorData]):
try:
battery = self._psutil.sensors_battery()
_LOGGER.debug("battery: %s", battery)
except (FileNotFoundError, PermissionError) as err:
_LOGGER.debug("OS error when accessing battery sensors: %s", err)
except (AttributeError, FileNotFoundError):
_LOGGER.debug("OS does not provide battery sensors")

View File

@@ -670,23 +670,41 @@ class WeatherExtraStoredData(ExtraStoredData):
@classmethod
def from_dict(cls, restored: dict[str, Any]) -> Self | None:
"""Initialize a stored event state from a dict."""
try:
return cls(
last_apparent_temperature=restored["last_apparent_temperature"],
last_cloud_coverage=restored["last_cloud_coverage"],
last_dew_point=restored["last_dew_point"],
last_humidity=restored["last_humidity"],
last_ozone=restored["last_ozone"],
last_pressure=restored["last_pressure"],
last_temperature=restored["last_temperature"],
last_uv_index=restored["last_uv_index"],
last_visibility=restored["last_visibility"],
last_wind_bearing=restored["last_wind_bearing"],
last_wind_gust_speed=restored["last_wind_gust_speed"],
last_wind_speed=restored["last_wind_speed"],
)
except KeyError:
return None
for key, vtypes in (
("last_apparent_temperature", (float, int)),
("last_cloud_coverage", (float, int)),
("last_dew_point", (float, int)),
("last_humidity", (float, int)),
("last_ozone", (float, int)),
("last_pressure", (float, int)),
("last_temperature", (float, int)),
("last_uv_index", (float, int)),
("last_visibility", (float, int)),
("last_wind_bearing", (float, int, str)),
("last_wind_gust_speed", (float, int)),
("last_wind_speed", (float, int)),
):
# This is needed to safeguard against previous restore data that has strings
# instead of floats or ints.
if key not in restored or (
(value := restored[key]) is not None and not isinstance(value, vtypes)
):
return None
return cls(
last_apparent_temperature=restored["last_apparent_temperature"],
last_cloud_coverage=restored["last_cloud_coverage"],
last_dew_point=restored["last_dew_point"],
last_humidity=restored["last_humidity"],
last_ozone=restored["last_ozone"],
last_pressure=restored["last_pressure"],
last_temperature=restored["last_temperature"],
last_uv_index=restored["last_uv_index"],
last_visibility=restored["last_visibility"],
last_wind_bearing=restored["last_wind_bearing"],
last_wind_gust_speed=restored["last_wind_gust_speed"],
last_wind_speed=restored["last_wind_speed"],
)
class TriggerWeatherEntity(TriggerEntity, AbstractTemplateWeather, RestoreEntity):
@@ -824,18 +842,18 @@ class TriggerWeatherEntity(TriggerEntity, AbstractTemplateWeather, RestoreEntity
def extra_restore_state_data(self) -> WeatherExtraStoredData:
"""Return weather specific state data to be restored."""
return WeatherExtraStoredData(
last_apparent_temperature=self._rendered.get(CONF_APPARENT_TEMPERATURE),
last_cloud_coverage=self._rendered.get(CONF_CLOUD_COVERAGE),
last_dew_point=self._rendered.get(CONF_DEW_POINT),
last_humidity=self._rendered.get(CONF_HUMIDITY),
last_ozone=self._rendered.get(CONF_OZONE),
last_pressure=self._rendered.get(CONF_PRESSURE),
last_temperature=self._rendered.get(CONF_TEMPERATURE),
last_uv_index=self._rendered.get(CONF_UV_INDEX),
last_visibility=self._rendered.get(CONF_VISIBILITY),
last_wind_bearing=self._rendered.get(CONF_WIND_BEARING),
last_wind_gust_speed=self._rendered.get(CONF_WIND_GUST_SPEED),
last_wind_speed=self._rendered.get(CONF_WIND_SPEED),
last_apparent_temperature=self.native_apparent_temperature,
last_cloud_coverage=self._attr_cloud_coverage,
last_dew_point=self.native_dew_point,
last_humidity=self.humidity,
last_ozone=self.ozone,
last_pressure=self.native_pressure,
last_temperature=self.native_temperature,
last_uv_index=self.uv_index,
last_visibility=self.native_visibility,
last_wind_bearing=self.wind_bearing,
last_wind_gust_speed=self.native_wind_gust_speed,
last_wind_speed=self.native_wind_speed,
)
async def async_get_last_weather_data(self) -> WeatherExtraStoredData | None:

View File

@@ -54,7 +54,7 @@ class DatasetEntry:
return cast(tlv_parser.Channel, channel).channel
@cached_property
def dataset(self) -> dict[MeshcopTLVType, tlv_parser.MeshcopTLVItem]:
def dataset(self) -> dict[MeshcopTLVType | int, tlv_parser.MeshcopTLVItem]:
"""Return the dataset in dict format."""
return tlv_parser.parse_tlv(self.tlv)

View File

@@ -7,7 +7,7 @@
"documentation": "https://www.home-assistant.io/integrations/thread",
"integration_type": "service",
"iot_class": "local_polling",
"requirements": ["python-otbr-api==2.7.1", "pyroute2==0.7.5"],
"requirements": ["python-otbr-api==2.8.0", "pyroute2==0.7.5"],
"single_config_entry": true,
"zeroconf": ["_meshcop._udp.local."]
}

View File

@@ -23,7 +23,7 @@
"universal_silabs_flasher",
"serialx"
],
"requirements": ["zha==0.0.88", "serialx==0.6.2"],
"requirements": ["zha==0.0.89", "serialx==0.6.2"],
"usb": [
{
"description": "*2652*",

View File

@@ -17,7 +17,7 @@ if TYPE_CHECKING:
APPLICATION_NAME: Final = "HomeAssistant"
MAJOR_VERSION: Final = 2026
MINOR_VERSION: Final = 2
PATCH_VERSION: Final = "0b2"
PATCH_VERSION: Final = "0b4"
__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

@@ -2701,6 +2701,12 @@
"config_flow": false,
"iot_class": "local_push"
},
"heatit": {
"name": "Heatit",
"iot_standards": [
"zwave"
]
},
"heatmiser": {
"name": "Heatmiser",
"integration_type": "hub",
@@ -2712,6 +2718,13 @@
"integration_type": "virtual",
"supported_by": "motion_blinds"
},
"heiman": {
"name": "Heiman",
"iot_standards": [
"matter",
"zigbee"
]
},
"heiwa": {
"name": "Heiwa",
"integration_type": "virtual",

View File

@@ -39,8 +39,8 @@ habluetooth==5.8.0
hass-nabucasa==1.12.0
hassil==3.5.0
home-assistant-bluetooth==1.13.1
home-assistant-frontend==20260128.4
home-assistant-intents==2026.1.28
home-assistant-frontend==20260128.6
home-assistant-intents==2026.2.3
httpx==0.28.1
ifaddr==0.2.0
Jinja2==3.1.6
@@ -230,3 +230,8 @@ pytest-rerunfailures==16.0.1
# Fixes detected blocking call to load_default_certs https://github.com/home-assistant/core/issues/157475
aiomqtt>=2.5.0
# auth0-python v5.0 is a major rewrite with breaking changes
# used by sharkiq==1.5.0
# https://github.com/auth0/auth0-python/releases/tag/5.0.0
auth0-python<5.0

View File

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

2
requirements.txt generated
View File

@@ -27,7 +27,7 @@ ha-ffmpeg==3.2.2
hass-nabucasa==1.12.0
hassil==3.5.0
home-assistant-bluetooth==1.13.1
home-assistant-intents==2026.1.28
home-assistant-intents==2026.2.3
httpx==0.28.1
ifaddr==0.2.0
Jinja2==3.1.6

22
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.0.2
aioamazondevices==11.1.1
# homeassistant.components.ambient_network
# homeassistant.components.ambient_station
@@ -637,7 +637,7 @@ bimmer-connected[china]==0.17.3
bizkaibus==0.1.1
# homeassistant.components.esphome
bleak-esphome==3.5.0
bleak-esphome==3.6.0
# homeassistant.components.bluetooth
bleak-retry-connector==4.4.3
@@ -740,7 +740,7 @@ colorlog==6.10.1
colorthief==0.2.1
# homeassistant.components.compit
compit-inext-api==0.6.0
compit-inext-api==0.8.0
# homeassistant.components.concord232
concord232==0.15.1
@@ -797,7 +797,7 @@ deluge-client==1.10.2
demetriek==1.3.0
# homeassistant.components.denonavr
denonavr==1.2.0
denonavr==1.3.1
# homeassistant.components.devialet
devialet==1.5.7
@@ -1142,7 +1142,7 @@ greenwavereality==0.5.1
gridnet==5.0.1
# homeassistant.components.growatt_server
growattServer==1.7.1
growattServer==1.9.0
# homeassistant.components.google_sheets
gspread==5.5.0
@@ -1219,10 +1219,10 @@ hole==0.9.0
holidays==0.84
# homeassistant.components.frontend
home-assistant-frontend==20260128.4
home-assistant-frontend==20260128.6
# homeassistant.components.conversation
home-assistant-intents==2026.1.28
home-assistant-intents==2026.2.3
# homeassistant.components.gentex_homelink
homelink-integration-api==0.0.1
@@ -1865,7 +1865,7 @@ pyElectra==1.2.4
pyEmby==1.10
# homeassistant.components.hikvision
pyHik==0.4.1
pyHik==0.4.2
# homeassistant.components.homee
pyHomee==1.3.8
@@ -2576,7 +2576,7 @@ python-opensky==1.0.1
# homeassistant.components.otbr
# homeassistant.components.thread
python-otbr-api==2.7.1
python-otbr-api==2.8.0
# homeassistant.components.overseerr
python-overseerr==0.8.0
@@ -3278,7 +3278,7 @@ youless-api==2.2.0
youtubeaio==2.1.1
# homeassistant.components.media_extractor
yt-dlp[default]==2025.12.08
yt-dlp[default]==2026.02.04
# homeassistant.components.zabbix
zabbix-utils==2.0.3
@@ -3296,7 +3296,7 @@ zeroconf==0.148.0
zeversolar==0.3.2
# homeassistant.components.zha
zha==0.0.88
zha==0.0.89
# homeassistant.components.zhong_hong
zhong-hong-hvac==1.0.13

View File

@@ -181,7 +181,7 @@ aioairzone-cloud==0.7.2
aioairzone==1.0.5
# homeassistant.components.alexa_devices
aioamazondevices==11.0.2
aioamazondevices==11.1.1
# homeassistant.components.ambient_network
# homeassistant.components.ambient_station
@@ -574,7 +574,7 @@ beautifulsoup4==4.13.3
bimmer-connected[china]==0.17.3
# homeassistant.components.esphome
bleak-esphome==3.5.0
bleak-esphome==3.6.0
# homeassistant.components.bluetooth
bleak-retry-connector==4.4.3
@@ -655,7 +655,7 @@ colorlog==6.10.1
colorthief==0.2.1
# homeassistant.components.compit
compit-inext-api==0.6.0
compit-inext-api==0.8.0
# homeassistant.components.concord232
concord232==0.15.1
@@ -706,7 +706,7 @@ deluge-client==1.10.2
demetriek==1.3.0
# homeassistant.components.denonavr
denonavr==1.2.0
denonavr==1.3.1
# homeassistant.components.devialet
devialet==1.5.7
@@ -1012,7 +1012,7 @@ greenplanet-energy-api==0.1.4
gridnet==5.0.1
# homeassistant.components.growatt_server
growattServer==1.7.1
growattServer==1.9.0
# homeassistant.components.google_sheets
gspread==5.5.0
@@ -1077,10 +1077,10 @@ hole==0.9.0
holidays==0.84
# homeassistant.components.frontend
home-assistant-frontend==20260128.4
home-assistant-frontend==20260128.6
# homeassistant.components.conversation
home-assistant-intents==2026.1.28
home-assistant-intents==2026.2.3
# homeassistant.components.gentex_homelink
homelink-integration-api==0.0.1
@@ -1602,7 +1602,7 @@ pyDuotecno==2024.10.1
pyElectra==1.2.4
# homeassistant.components.hikvision
pyHik==0.4.1
pyHik==0.4.2
# homeassistant.components.homee
pyHomee==1.3.8
@@ -2169,7 +2169,7 @@ python-opensky==1.0.1
# homeassistant.components.otbr
# homeassistant.components.thread
python-otbr-api==2.7.1
python-otbr-api==2.8.0
# homeassistant.components.overseerr
python-overseerr==0.8.0
@@ -2748,7 +2748,7 @@ youless-api==2.2.0
youtubeaio==2.1.1
# homeassistant.components.media_extractor
yt-dlp[default]==2025.12.08
yt-dlp[default]==2026.02.04
# homeassistant.components.zamg
zamg==0.3.6
@@ -2763,7 +2763,7 @@ zeroconf==0.148.0
zeversolar==0.3.2
# homeassistant.components.zha
zha==0.0.88
zha==0.0.89
# homeassistant.components.zwave_js
zwave-js-server-python==0.68.0

View File

@@ -220,6 +220,11 @@ pytest-rerunfailures==16.0.1
# Fixes detected blocking call to load_default_certs https://github.com/home-assistant/core/issues/157475
aiomqtt>=2.5.0
# auth0-python v5.0 is a major rewrite with breaking changes
# used by sharkiq==1.5.0
# https://github.com/auth0/auth0-python/releases/tag/5.0.0
auth0-python<5.0
"""
GENERATED_MESSAGE = (

View File

@@ -46,6 +46,7 @@ TEST_DEVICE_1 = AmazonDevice(
scale="CELSIUS",
),
},
notifications_supported=True,
notifications={
NOTIFICATION_ALARM: AmazonSchedule(
type=NOTIFICATION_ALARM,
@@ -93,5 +94,6 @@ TEST_DEVICE_2 = AmazonDevice(
scale="CELSIUS",
)
},
notifications_supported=False,
notifications={},
)

View File

@@ -37,6 +37,7 @@
'type': 'Timer',
}),
}),
'notifications_supported': True,
'online': True,
'sensors': dict({
'dnd': dict({
@@ -103,6 +104,7 @@
'type': 'Timer',
}),
}),
'notifications_supported': True,
'online': True,
'sensors': dict({
'dnd': dict({

View File

@@ -7,6 +7,7 @@ from aioamazondevices.exceptions import CannotConnect, CannotRetrieveData
import pytest
from homeassistant.components.alexa_devices.const import DOMAIN
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN, SERVICE_TURN_ON
from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF
from homeassistant.core import HomeAssistant
@@ -134,3 +135,42 @@ async def test_alexa_dnd_group_removal(
await hass.async_block_till_done()
assert not hass.states.get(entity.entity_id)
async def test_alexa_unsupported_notification_sensor_removal(
hass: HomeAssistant,
mock_amazon_devices_client: AsyncMock,
mock_config_entry: MockConfigEntry,
device_registry: dr.DeviceRegistry,
entity_registry: er.EntityRegistry,
) -> None:
"""Test notification sensors are removed from devices that do not support them."""
mock_config_entry.add_to_hass(hass)
device = device_registry.async_get_or_create(
config_entry_id=mock_config_entry.entry_id,
identifiers={(DOMAIN, mock_config_entry.entry_id)},
name=mock_config_entry.title,
manufacturer="Amazon",
model=SPEAKER_GROUP_MODEL,
entry_type=dr.DeviceEntryType.SERVICE,
)
entity = entity_registry.async_get_or_create(
DOMAIN,
SENSOR_DOMAIN,
unique_id=f"{TEST_DEVICE_1_SN}-Timer",
device_id=device.id,
config_entry=mock_config_entry,
has_entity_name=True,
)
mock_amazon_devices_client.get_devices_data.return_value[
TEST_DEVICE_1_SN
].notifications_supported = False
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
assert not hass.states.get(entity.entity_id)

View File

@@ -387,7 +387,7 @@ async def test_model_list(
},
{
"label": "Claude Haiku 3.5",
"value": "claude-3-5-haiku-latest",
"value": "claude-3-5-haiku-20241022",
},
{
"label": "Claude Haiku 3",
@@ -500,7 +500,7 @@ async def test_model_list_error(
CONF_LLM_HASS_API: [],
},
{
CONF_CHAT_MODEL: "claude-3-5-haiku-latest",
CONF_CHAT_MODEL: "claude-3-5-haiku-20241022",
CONF_TEMPERATURE: 1.0,
},
{
@@ -513,7 +513,7 @@ async def test_model_list_error(
CONF_RECOMMENDED: False,
CONF_PROMPT: "Speak like a pirate",
CONF_TEMPERATURE: 1.0,
CONF_CHAT_MODEL: "claude-3-5-haiku-latest",
CONF_CHAT_MODEL: "claude-3-5-haiku-20241022",
CONF_MAX_TOKENS: DEFAULT[CONF_MAX_TOKENS],
CONF_WEB_SEARCH: False,
CONF_WEB_SEARCH_MAX_USES: 10,
@@ -581,6 +581,7 @@ async def test_model_list_error(
CONF_TEMPERATURE: 0.3,
CONF_CHAT_MODEL: DEFAULT[CONF_CHAT_MODEL],
CONF_MAX_TOKENS: DEFAULT[CONF_MAX_TOKENS],
CONF_THINKING_BUDGET: 0,
CONF_WEB_SEARCH: False,
CONF_WEB_SEARCH_MAX_USES: 5,
CONF_WEB_SEARCH_USER_LOCATION: False,

View File

@@ -0,0 +1,301 @@
"""Tests for the Anthropic repairs flow."""
from __future__ import annotations
from typing import Any
from unittest.mock import AsyncMock, MagicMock, call, patch
from homeassistant.components.anthropic.const import CONF_CHAT_MODEL, DOMAIN
from homeassistant.config_entries import ConfigEntryState, ConfigSubentry
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
from homeassistant.helpers import issue_registry as ir
from homeassistant.setup import async_setup_component
from tests.common import MockConfigEntry
from tests.components.repairs import (
async_process_repairs_platforms,
process_repair_fix_flow,
start_repair_fix_flow,
)
from tests.typing import ClientSessionGenerator
def _make_entry(
hass: HomeAssistant,
*,
title: str,
api_key: str,
subentries_data: list[dict[str, Any]],
) -> MockConfigEntry:
"""Create a config entry with subentries and runtime data."""
entry = MockConfigEntry(
domain=DOMAIN,
title=title,
data={"api_key": api_key},
version=2,
subentries_data=subentries_data,
)
entry.add_to_hass(hass)
object.__setattr__(entry, "state", ConfigEntryState.LOADED)
entry.runtime_data = MagicMock()
return entry
def _get_subentry(
entry: MockConfigEntry,
subentry_type: str,
) -> ConfigSubentry:
"""Return the first subentry of a type."""
return next(
subentry
for subentry in entry.subentries.values()
if subentry.subentry_type == subentry_type
)
async def _setup_repairs(hass: HomeAssistant) -> None:
hass.config.components.add(DOMAIN)
assert await async_setup_component(hass, "repairs", {})
await async_process_repairs_platforms(hass)
async def test_repair_flow_iterates_subentries(
hass: HomeAssistant,
hass_client: ClientSessionGenerator,
issue_registry: ir.IssueRegistry,
) -> None:
"""Test the repair flow iterates across deprecated subentries."""
entry_one: MockConfigEntry = _make_entry(
hass,
title="Entry One",
api_key="key-one",
subentries_data=[
{
"data": {CONF_CHAT_MODEL: "claude-3-5-haiku-20241022"},
"subentry_type": "conversation",
"title": "Conversation One",
"unique_id": None,
},
{
"data": {CONF_CHAT_MODEL: "claude-3-5-sonnet-20241022"},
"subentry_type": "ai_task_data",
"title": "AI task One",
"unique_id": None,
},
],
)
entry_two: MockConfigEntry = _make_entry(
hass,
title="Entry Two",
api_key="key-two",
subentries_data=[
{
"data": {CONF_CHAT_MODEL: "claude-3-opus-20240229"},
"subentry_type": "conversation",
"title": "Conversation Two",
"unique_id": None,
},
],
)
ir.async_create_issue(
hass,
DOMAIN,
"model_deprecated",
is_fixable=True,
is_persistent=False,
severity=ir.IssueSeverity.WARNING,
translation_key="model_deprecated",
)
await _setup_repairs(hass)
client = await hass_client()
model_options: list[dict[str, str]] = [
{"label": "Claude Haiku 4.5", "value": "claude-haiku-4-5"},
{"label": "Claude Sonnet 4.5", "value": "claude-sonnet-4-5"},
{"label": "Claude Opus 4.5", "value": "claude-opus-4-5"},
]
with patch(
"homeassistant.components.anthropic.repairs.get_model_list",
new_callable=AsyncMock,
return_value=model_options,
):
result = await start_repair_fix_flow(client, DOMAIN, "model_deprecated")
assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "init"
placeholders = result["description_placeholders"]
assert placeholders["entry_name"] == entry_one.title
assert placeholders["subentry_name"] == "Conversation One"
assert placeholders["subentry_type"] == "Conversation agent"
flow_id = result["flow_id"]
result = await process_repair_fix_flow(
client,
flow_id,
json={CONF_CHAT_MODEL: "claude-haiku-4-5"},
)
assert result["type"] == FlowResultType.FORM
assert (
_get_subentry(entry_one, "conversation").data[CONF_CHAT_MODEL]
== "claude-3-5-haiku-20241022"
)
placeholders = result["description_placeholders"]
assert placeholders["entry_name"] == entry_one.title
assert placeholders["subentry_name"] == "AI task One"
assert placeholders["subentry_type"] == "AI task"
result = await process_repair_fix_flow(
client,
flow_id,
json={CONF_CHAT_MODEL: "claude-sonnet-4-5"},
)
assert result["type"] == FlowResultType.FORM
assert (
_get_subentry(entry_one, "ai_task_data").data[CONF_CHAT_MODEL]
== "claude-sonnet-4-5"
)
assert (
_get_subentry(entry_one, "conversation").data[CONF_CHAT_MODEL]
== "claude-haiku-4-5"
)
placeholders = result["description_placeholders"]
assert placeholders["entry_name"] == entry_two.title
assert placeholders["subentry_name"] == "Conversation Two"
assert placeholders["subentry_type"] == "Conversation agent"
result = await process_repair_fix_flow(
client,
flow_id,
json={CONF_CHAT_MODEL: "claude-opus-4-5"},
)
assert result["type"] == FlowResultType.CREATE_ENTRY
assert (
_get_subentry(entry_two, "conversation").data[CONF_CHAT_MODEL]
== "claude-opus-4-5"
)
assert issue_registry.async_get_issue(DOMAIN, "model_deprecated") is None
async def test_repair_flow_no_deprecated_models(
hass: HomeAssistant,
hass_client: ClientSessionGenerator,
issue_registry: ir.IssueRegistry,
) -> None:
"""Test the repair flow completes when everything was fixed."""
_make_entry(
hass,
title="Entry One",
api_key="key-one",
subentries_data=[
{
"data": {CONF_CHAT_MODEL: "claude-sonnet-4-5"},
"subentry_type": "conversation",
"title": "Conversation One",
"unique_id": None,
}
],
)
ir.async_create_issue(
hass,
DOMAIN,
"model_deprecated",
is_fixable=True,
is_persistent=False,
severity=ir.IssueSeverity.WARNING,
translation_key="model_deprecated",
)
await _setup_repairs(hass)
client = await hass_client()
result = await start_repair_fix_flow(client, DOMAIN, "model_deprecated")
assert result["type"] == FlowResultType.CREATE_ENTRY
assert issue_registry.async_get_issue(DOMAIN, "model_deprecated") is None
async def test_repair_flow_defers_reload_until_entry_done(
hass: HomeAssistant,
hass_client: ClientSessionGenerator,
issue_registry: ir.IssueRegistry,
) -> None:
"""Test reload is deferred until all subentries for an entry are fixed."""
entry = _make_entry(
hass,
title="Entry One",
api_key="key-one",
subentries_data=[
{
"data": {CONF_CHAT_MODEL: "claude-3-5-haiku-20241022"},
"subentry_type": "conversation",
"title": "Conversation One",
"unique_id": None,
},
{
"data": {CONF_CHAT_MODEL: "claude-3-5-sonnet-20241022"},
"subentry_type": "ai_task_data",
"title": "AI task One",
"unique_id": None,
},
],
)
ir.async_create_issue(
hass,
DOMAIN,
"model_deprecated",
is_fixable=True,
is_persistent=False,
severity=ir.IssueSeverity.WARNING,
translation_key="model_deprecated",
)
await _setup_repairs(hass)
client = await hass_client()
model_options: list[dict[str, str]] = [
{"label": "Claude Haiku 4.5", "value": "claude-haiku-4-5"},
{"label": "Claude Sonnet 4.5", "value": "claude-sonnet-4-5"},
]
with (
patch(
"homeassistant.components.anthropic.repairs.get_model_list",
new_callable=AsyncMock,
return_value=model_options,
),
patch.object(
hass.config_entries,
"async_reload",
new_callable=AsyncMock,
return_value=True,
) as reload_mock,
):
result = await start_repair_fix_flow(client, DOMAIN, "model_deprecated")
flow_id = result["flow_id"]
assert result["step_id"] == "init"
result = await process_repair_fix_flow(
client,
flow_id,
json={CONF_CHAT_MODEL: "claude-haiku-4-5"},
)
assert result["type"] == FlowResultType.FORM
assert reload_mock.await_count == 0
result = await process_repair_fix_flow(
client,
flow_id,
json={CONF_CHAT_MODEL: "claude-sonnet-4-5"},
)
assert result["type"] == FlowResultType.CREATE_ENTRY
assert reload_mock.await_count == 1
assert reload_mock.call_args_list == [call(entry.entry_id)]

View File

@@ -206,6 +206,17 @@ async def test_prepare_chat_for_generation_appends_attachments(
assert response["messages"] is messages
mock_prepare_files_for_prompt.assert_awaited_once_with([attachment])
# Verify that files are actually added to the last user message
last_message = messages[-1]
assert last_message["type"] == "message"
assert last_message["role"] == "user"
assert isinstance(last_message["content"], list)
assert last_message["content"][0] == {
"type": "input_text",
"text": "Describe the door",
}
assert last_message["content"][1] == files[0]
async def test_prepare_chat_for_generation_passes_messages_through(
hass: HomeAssistant, cloud_entity: BaseCloudLLMEntity

View File

@@ -21,7 +21,7 @@
],
"Capabilities": {
"PlayableMediaTypes": ["Video"],
"SupportedCommands": ["VolumeSet", "Mute"],
"SupportedCommands": ["VolumeSet", "Mute", "Unmute"],
"SupportsMediaControl": true,
"SupportsContentUploading": true,
"MessageCallbackUrl": "string",
@@ -1781,7 +1781,7 @@
],
"Capabilities": {
"PlayableMediaTypes": ["Video"],
"SupportedCommands": ["VolumeSet", "Mute"],
"SupportedCommands": ["VolumeSet", "Mute", "Unmute"],
"SupportsMediaControl": true,
"SupportsContentUploading": true,
"MessageCallbackUrl": "string",

View File

@@ -21,7 +21,7 @@
],
"Capabilities": {
"PlayableMediaTypes": ["Video"],
"SupportedCommands": ["VolumeSet", "Mute", "PlayMediaSource"],
"SupportedCommands": ["VolumeSet", "Mute", "Unmute", "PlayMediaSource"],
"SupportsMediaControl": true,
"SupportsContentUploading": true,
"MessageCallbackUrl": "string",
@@ -1781,7 +1781,7 @@
],
"Capabilities": {
"PlayableMediaTypes": ["Video"],
"SupportedCommands": ["VolumeSet", "Mute"],
"SupportedCommands": ["VolumeSet", "Mute", "Unmute"],
"SupportsMediaControl": true,
"SupportsContentUploading": true,
"MessageCallbackUrl": "string",
@@ -4548,5 +4548,133 @@
"PlayMediaSource",
"PlayTrailers"
]
},
{
"PlayState": {
"PositionTicks": 100000000,
"CanSeek": true,
"IsPaused": false,
"IsMuted": false,
"VolumeLevel": 50,
"AudioStreamIndex": 0,
"SubtitleStreamIndex": 0,
"MediaSourceId": "string",
"PlayMethod": "Transcode",
"RepeatMode": "RepeatNone",
"LiveStreamId": "string"
},
"AdditionalUsers": [],
"Capabilities": {
"PlayableMediaTypes": ["Video"],
"SupportedCommands": ["Mute", "VolumeSet", "PlayMediaSource"],
"SupportsMediaControl": true,
"SupportsContentUploading": false,
"MessageCallbackUrl": "string",
"SupportsPersistentIdentifier": false,
"SupportsSync": false,
"DeviceProfile": null
},
"RemoteEndPoint": "192.168.1.1",
"PlayableMediaTypes": ["Video"],
"Id": "SESSION-UUID-FIVE",
"UserId": "USER-UUID",
"UserName": "USER",
"Client": "Test Client Five",
"LastActivityDate": "2021-05-21T06:09:06.919Z",
"LastPlaybackCheckIn": "2021-05-21T06:09:06.919Z",
"DeviceName": "JELLYFIN-DEVICE-FIVE",
"DeviceId": "DEVICE-UUID-FIVE",
"ApplicationVersion": "1.0.0",
"IsActive": true,
"SupportsMediaControl": true,
"SupportsRemoteControl": true,
"HasCustomDeviceName": false,
"ServerId": "SERVER-UUID",
"SupportedCommands": ["MoveUp"],
"NowPlayingItem": {
"Name": "TEST VIDEO",
"ServerId": "SERVER-UUID",
"Id": "VIDEO-UUID-FIVE",
"RunTimeTicks": 600000000,
"Type": "Episode",
"UserData": {
"PlaybackPositionTicks": 0,
"PlayCount": 0,
"IsFavorite": false,
"Played": false,
"Key": "string"
},
"PrimaryImageAspectRatio": 1,
"SeriesName": "TEST SERIES",
"ParentIndexNumber": 1,
"IndexNumber": 1,
"ImageTags": {
"Primary": "tag"
}
}
},
{
"PlayState": {
"PositionTicks": 100000000,
"CanSeek": true,
"IsPaused": false,
"IsMuted": false,
"VolumeLevel": 50,
"AudioStreamIndex": 0,
"SubtitleStreamIndex": 0,
"MediaSourceId": "string",
"PlayMethod": "Transcode",
"RepeatMode": "RepeatNone",
"LiveStreamId": "string"
},
"AdditionalUsers": [],
"Capabilities": {
"PlayableMediaTypes": ["Video"],
"SupportedCommands": ["Unmute", "VolumeSet", "PlayMediaSource"],
"SupportsMediaControl": true,
"SupportsContentUploading": false,
"MessageCallbackUrl": "string",
"SupportsPersistentIdentifier": false,
"SupportsSync": false,
"DeviceProfile": null
},
"RemoteEndPoint": "192.168.1.1",
"PlayableMediaTypes": ["Video"],
"Id": "SESSION-UUID-SIX",
"UserId": "USER-UUID",
"UserName": "USER",
"Client": "Test Client Six",
"LastActivityDate": "2021-05-21T06:09:06.919Z",
"LastPlaybackCheckIn": "2021-05-21T06:09:06.919Z",
"DeviceName": "JELLYFIN-DEVICE-SIX",
"DeviceId": "DEVICE-UUID-SIX",
"ApplicationVersion": "1.0.0",
"IsActive": true,
"SupportsMediaControl": true,
"SupportsRemoteControl": true,
"HasCustomDeviceName": false,
"ServerId": "SERVER-UUID",
"SupportedCommands": ["MoveUp"],
"NowPlayingItem": {
"Name": "TEST VIDEO",
"ServerId": "SERVER-UUID",
"Id": "VIDEO-UUID-SIX",
"RunTimeTicks": 600000000,
"Type": "Episode",
"UserData": {
"PlaybackPositionTicks": 0,
"PlayCount": 0,
"IsFavorite": false,
"Played": false,
"Key": "string"
},
"PrimaryImageAspectRatio": 1,
"SeriesName": "TEST SERIES",
"ParentIndexNumber": 1,
"IndexNumber": 1,
"ImageTags": {
"Primary": "tag"
}
}
}
]

View File

@@ -182,6 +182,7 @@
'SupportedCommands': list([
'VolumeSet',
'Mute',
'Unmute',
'PlayMediaSource',
]),
'SupportsContentUploading': True,
@@ -902,6 +903,7 @@
'SupportedCommands': list([
'VolumeSet',
'Mute',
'Unmute',
]),
'SupportsContentUploading': True,
'SupportsMediaControl': True,
@@ -1785,6 +1787,122 @@
}),
'user_id': 'USER-UUID-TWO',
}),
dict({
'capabilities': dict({
'DeviceProfile': None,
'MessageCallbackUrl': 'string',
'PlayableMediaTypes': list([
'Video',
]),
'SupportedCommands': list([
'Mute',
'VolumeSet',
'PlayMediaSource',
]),
'SupportsContentUploading': False,
'SupportsMediaControl': True,
'SupportsPersistentIdentifier': False,
'SupportsSync': False,
}),
'client_name': 'Test Client Five',
'client_version': '1.0.0',
'device_id': 'DEVICE-UUID-FIVE',
'device_name': 'JELLYFIN-DEVICE-FIVE',
'id': 'SESSION-UUID-FIVE',
'now_playing': dict({
'Id': 'VIDEO-UUID-FIVE',
'ImageTags': dict({
'Primary': 'tag',
}),
'IndexNumber': 1,
'Name': 'TEST VIDEO',
'ParentIndexNumber': 1,
'PrimaryImageAspectRatio': 1,
'RunTimeTicks': 600000000,
'SeriesName': 'TEST SERIES',
'ServerId': 'SERVER-UUID',
'Type': 'Episode',
'UserData': dict({
'IsFavorite': False,
'Key': 'string',
'PlayCount': 0,
'PlaybackPositionTicks': 0,
'Played': False,
}),
}),
'play_state': dict({
'AudioStreamIndex': 0,
'CanSeek': True,
'IsMuted': False,
'IsPaused': False,
'LiveStreamId': 'string',
'MediaSourceId': 'string',
'PlayMethod': 'Transcode',
'PositionTicks': 100000000,
'RepeatMode': 'RepeatNone',
'SubtitleStreamIndex': 0,
'VolumeLevel': 50,
}),
'user_id': 'USER-UUID',
}),
dict({
'capabilities': dict({
'DeviceProfile': None,
'MessageCallbackUrl': 'string',
'PlayableMediaTypes': list([
'Video',
]),
'SupportedCommands': list([
'Unmute',
'VolumeSet',
'PlayMediaSource',
]),
'SupportsContentUploading': False,
'SupportsMediaControl': True,
'SupportsPersistentIdentifier': False,
'SupportsSync': False,
}),
'client_name': 'Test Client Six',
'client_version': '1.0.0',
'device_id': 'DEVICE-UUID-SIX',
'device_name': 'JELLYFIN-DEVICE-SIX',
'id': 'SESSION-UUID-SIX',
'now_playing': dict({
'Id': 'VIDEO-UUID-SIX',
'ImageTags': dict({
'Primary': 'tag',
}),
'IndexNumber': 1,
'Name': 'TEST VIDEO',
'ParentIndexNumber': 1,
'PrimaryImageAspectRatio': 1,
'RunTimeTicks': 600000000,
'SeriesName': 'TEST SERIES',
'ServerId': 'SERVER-UUID',
'Type': 'Episode',
'UserData': dict({
'IsFavorite': False,
'Key': 'string',
'PlayCount': 0,
'PlaybackPositionTicks': 0,
'Played': False,
}),
}),
'play_state': dict({
'AudioStreamIndex': 0,
'CanSeek': True,
'IsMuted': False,
'IsPaused': False,
'LiveStreamId': 'string',
'MediaSourceId': 'string',
'PlayMethod': 'Transcode',
'PositionTicks': 100000000,
'RepeatMode': 'RepeatNone',
'SubtitleStreamIndex': 0,
'VolumeLevel': 50,
}),
'user_id': 'USER-UUID',
}),
]),
})
# ---

View File

@@ -21,6 +21,7 @@ from homeassistant.components.media_player import (
ATTR_MEDIA_VOLUME_MUTED,
DOMAIN as MP_DOMAIN,
MediaClass,
MediaPlayerEntityFeature,
MediaPlayerState,
MediaType,
)
@@ -423,3 +424,98 @@ async def test_new_client_connected(
state = hass.states.get("media_player.jellyfin_device_five")
assert state
async def test_supports_media_control_fallback(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
init_integration: MockConfigEntry,
mock_jellyfin: MagicMock,
mock_api: MagicMock,
) -> None:
"""Test that SupportsMediaControl enables controls without PlayMediaSource."""
# SESSION-UUID-TWO has SupportsMediaControl: true but no PlayMediaSource command
state = hass.states.get("media_player.jellyfin_device_two")
assert state
assert state.state == MediaPlayerState.PLAYING
entry = entity_registry.async_get(state.entity_id)
assert entry
# Get the entity to check supported features
entity = hass.data["entity_components"]["media_player"].get_entity(state.entity_id)
features = entity.supported_features
# Should have basic playback controls
assert features & MediaPlayerEntityFeature.PLAY
assert features & MediaPlayerEntityFeature.PAUSE
assert features & MediaPlayerEntityFeature.STOP
assert features & MediaPlayerEntityFeature.SEEK
assert features & MediaPlayerEntityFeature.BROWSE_MEDIA
assert features & MediaPlayerEntityFeature.PLAY_MEDIA
# Should also have volume controls since it has VolumeSet, Mute, and Unmute
assert features & MediaPlayerEntityFeature.VOLUME_SET
assert features & MediaPlayerEntityFeature.VOLUME_MUTE
async def test_set_volume_command_alternative(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
init_integration: MockConfigEntry,
mock_jellyfin: MagicMock,
mock_api: MagicMock,
) -> None:
"""Test that SetVolume command (alternative to VolumeSet) enables volume control."""
# SESSION-UUID-FOUR has SetVolume instead of VolumeSet
state = hass.states.get("media_player.jellyfin_device_four")
assert state
# Get the entity to check supported features
entity = hass.data["entity_components"]["media_player"].get_entity(state.entity_id)
features = entity.supported_features
# Should have volume control via SetVolume command
assert features & MediaPlayerEntityFeature.VOLUME_SET
async def test_mute_requires_both_commands(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
init_integration: MockConfigEntry,
mock_jellyfin: MagicMock,
mock_api: MagicMock,
) -> None:
"""Test that VOLUME_MUTE requires both Mute AND Unmute commands."""
# SESSION-UUID-FIVE has only Mute (no Unmute) - should NOT have VOLUME_MUTE
state_five = hass.states.get("media_player.jellyfin_device_five")
assert state_five
entity_five = hass.data["entity_components"]["media_player"].get_entity(
state_five.entity_id
)
features_five = entity_five.supported_features
# Should NOT have mute feature
assert not (features_five & MediaPlayerEntityFeature.VOLUME_MUTE)
# But should still have other features
assert features_five & MediaPlayerEntityFeature.PLAY
assert features_five & MediaPlayerEntityFeature.VOLUME_SET
# SESSION-UUID-SIX has only Unmute (no Mute) - should NOT have VOLUME_MUTE
state_six = hass.states.get("media_player.jellyfin_device_six")
assert state_six
entity_six = hass.data["entity_components"]["media_player"].get_entity(
state_six.entity_id
)
features_six = entity_six.supported_features
# Should NOT have mute feature
assert not (features_six & MediaPlayerEntityFeature.VOLUME_MUTE)
# But should still have other features
assert features_six & MediaPlayerEntityFeature.PLAY
assert features_six & MediaPlayerEntityFeature.VOLUME_SET

View File

@@ -25,7 +25,7 @@ async def test_watching(
assert state.attributes.get(ATTR_FRIENDLY_NAME) == "JELLYFIN-SERVER Active clients"
assert state.attributes.get(ATTR_ICON) is None
assert state.attributes.get(ATTR_STATE_CLASS) is None
assert state.state == "3"
assert state.state == "5"
entry = entity_registry.async_get(state.entity_id)
assert entry

View File

@@ -102,6 +102,7 @@
'options': list([
'appliance_rinse',
'appliance_settings',
'automatic_maintenance',
'barista_assistant',
'black_tea',
'brewing_unit_degrease',
@@ -121,6 +122,7 @@
'herbal_tea',
'hot_milk',
'hot_water',
'intermediate_rinsing',
'japanese_tea',
'latte_macchiato',
'long_coffee',
@@ -171,6 +173,7 @@
'options': list([
'appliance_rinse',
'appliance_settings',
'automatic_maintenance',
'barista_assistant',
'black_tea',
'brewing_unit_degrease',
@@ -190,6 +193,7 @@
'herbal_tea',
'hot_milk',
'hot_water',
'intermediate_rinsing',
'japanese_tea',
'latte_macchiato',
'long_coffee',

View File

@@ -389,6 +389,44 @@ async def test_exception_handling_disk_sensor(
assert disk_sensor.attributes["unit_of_measurement"] == "%"
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
@pytest.mark.freeze_time("2024-02-24 15:00:00", tz_offset=0)
@pytest.mark.parametrize("exception_class", [FileNotFoundError, PermissionError])
async def test_exception_handling_battery_sensor(
hass: HomeAssistant,
mock_psutil: Mock,
mock_os: Mock,
freezer: FrozenDateTimeFactory,
mock_config_entry: MockConfigEntry,
caplog: pytest.LogCaptureFixture,
exception_class: type[Exception],
) -> None:
"""Test the battery failures."""
mock_psutil.sensors_battery.side_effect = exception_class(
"[Errno 2] No such file or directory: '/sys/class/power_supply'"
)
mock_config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
assert (temp_entity := hass.states.get("sensor.system_monitor_battery"))
assert temp_entity.state == STATE_UNAVAILABLE
assert (temp_entity := hass.states.get("sensor.system_monitor_battery_empty"))
assert temp_entity.state == STATE_UNAVAILABLE
assert "OS error when accessing battery sensors" in caplog.text
mock_psutil.sensors_battery.side_effect = None
freezer.tick(timedelta(minutes=1))
async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True)
assert (temp_entity := hass.states.get("sensor.system_monitor_battery"))
assert temp_entity.state == "93"
assert (temp_entity := hass.states.get("sensor.system_monitor_battery_empty"))
assert temp_entity.state == "2024-02-24T19:38:00+00:00"
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
async def test_cpu_percentage_is_zero_returns_unknown(
hass: HomeAssistant,

View File

@@ -269,10 +269,10 @@
'last_apparent_temperature': None,
'last_cloud_coverage': None,
'last_dew_point': None,
'last_humidity': '25.0',
'last_humidity': 25.0,
'last_ozone': None,
'last_pressure': None,
'last_temperature': '15.0',
'last_temperature': 15.0,
'last_uv_index': None,
'last_visibility': None,
'last_wind_bearing': None,

View File

@@ -839,11 +839,27 @@ SAVED_EXTRA_DATA_MISSING_KEY = {
"last_wind_speed": None,
}
SAVED_EXTRA_DATA_STRING_HUMIDITY = {
"last_apparent_temperature": None,
"last_cloud_coverage": None,
"last_dew_point": None,
"last_humidity": "20.0",
"last_ozone": None,
"last_pressure": None,
"last_temperature": 20.0,
"last_uv_index": None,
"last_visibility": None,
"last_wind_bearing": None,
"last_wind_gust_speed": None,
"last_wind_speed": None,
}
@pytest.mark.parametrize(
("saved_attributes", "saved_extra_data"),
[
(SAVED_ATTRIBUTES_1, SAVED_EXTRA_DATA_MISSING_KEY),
(SAVED_ATTRIBUTES_1, SAVED_EXTRA_DATA_STRING_HUMIDITY),
(SAVED_ATTRIBUTES_1, None),
],
)

View File

@@ -64,7 +64,7 @@ DATASET_1_LARGER_TIMESTAMP = (
async def test_add_invalid_dataset(hass: HomeAssistant) -> None:
"""Test adding an invalid dataset."""
with pytest.raises(TLVError, match="unknown type 222"):
with pytest.raises(TLVError, match="expected 173 bytes for tag 222, got 2"):
await dataset_store.async_add_dataset(hass, "source", "DEADBEEF")
store = await dataset_store.async_get_store(hass)

View File

@@ -57,7 +57,10 @@ async def test_add_invalid_dataset(
)
msg = await client.receive_json()
assert not msg["success"]
assert msg["error"] == {"code": "invalid_format", "message": "unknown type 222"}
assert msg["error"] == {
"code": "invalid_format",
"message": "expected 173 bytes for tag 222, got 2",
}
async def test_delete_dataset(