Compare commits

...

16 Commits

Author SHA1 Message Date
Franck Nijhof
4a301eceac Bump version to 2026.3.0b1 2026-02-26 19:32:15 +00:00
Bram Kragten
d138a99e62 Update frontend to 20260226.0 (#164262) 2026-02-26 19:31:52 +00:00
Johnny Willemsen
a431f84dc9 Update state labels to use common keys in compit (#164261) 2026-02-26 19:31:50 +00:00
epenet
aa9534600e Simplify portainer entity initialisation (#164256) 2026-02-26 19:31:49 +00:00
Denis Shulyaka
54fa49e754 Disable code interpreter with minimal reasoning for OpenAI (#164254) 2026-02-26 19:31:47 +00:00
Joost Lekkerkerker
459b6152f4 Remove invalid color mode from philips_js (#164204) 2026-02-26 19:31:46 +00:00
Denis Shulyaka
60c8d997ca Update reasoning options for gpt-5.3-codex (#164179) 2026-02-26 19:31:45 +00:00
AlCalzone
a598368895 Rename "Z-Wave Supervisor app" to "Z-Wave JS app" (#164147) 2026-02-26 19:31:43 +00:00
Erwin Douna
2ff1499c48 Fix stack devices merging with container devices in Portainer (#164135)
Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
2026-02-26 19:31:42 +00:00
Norbert Rittel
348ddbe124 Replace "add-ons" with "apps" in backup issues (#164129) 2026-02-26 19:31:40 +00:00
Paulus Schoutsen
71ed43faf2 Simplify Anthropic integration name (#164124)
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-26 19:31:39 +00:00
mettolen
dc69a90296 Remove error translation placeholders from Saunum (#164121) 2026-02-26 19:31:37 +00:00
Liquidmasl
f5db8e6ba4 Sonarr post merge changes (#164112) 2026-02-26 19:31:36 +00:00
Artur Pragacz
b82a26ef68 Fix Matter vacuum clean area status check (#164108) 2026-02-26 19:31:35 +00:00
Maciej Bieniek
0eaaeedf11 Bump accuweather to 5.1.0 (#164034) 2026-02-26 19:31:33 +00:00
Franck Nijhof
62e26e53ac Bump version to 2026.3.0b0 2026-02-25 19:36:43 +00:00
33 changed files with 323 additions and 242 deletions

View File

@@ -7,5 +7,5 @@
"integration_type": "service",
"iot_class": "cloud_polling",
"loggers": ["accuweather"],
"requirements": ["accuweather==5.0.0"]
"requirements": ["accuweather==5.1.0"]
}

View File

@@ -30,6 +30,8 @@ async def system_health_info(hass: HomeAssistant) -> dict[str, Any]:
)
return {
"can_reach_server": system_health.async_check_can_reach_url(hass, ENDPOINT),
"can_reach_server": system_health.async_check_can_reach_url(
hass, str(ENDPOINT)
),
"remaining_requests": remaining_requests,
}

View File

@@ -1,6 +1,6 @@
{
"domain": "anthropic",
"name": "Anthropic Conversation",
"name": "Anthropic",
"after_dependencies": ["assist_pipeline", "intent"],
"codeowners": ["@Shulyaka"],
"config_flow": true,

View File

@@ -43,11 +43,11 @@
"title": "The backup location {agent_id} is unavailable"
},
"automatic_backup_failed_addons": {
"description": "Add-ons {failed_addons} could not be included in automatic backup. Please check the Supervisor logs for more information. Another attempt will be made at the next scheduled time if a backup schedule is configured.",
"title": "Not all add-ons could be included in automatic backup"
"description": "Apps {failed_addons} could not be included in automatic backup. Please check the Supervisor logs for more information. Another attempt will be made at the next scheduled time if a backup schedule is configured.",
"title": "Not all apps could be included in automatic backup"
},
"automatic_backup_failed_agents_addons_folders": {
"description": "The automatic backup was created with errors:\n* Locations which the backup could not be uploaded to: {failed_agents}\n* Add-ons which could not be backed up: {failed_addons}\n* Folders which could not be backed up: {failed_folders}\n\nPlease check the Core and Supervisor logs for more information. Another attempt will be made at the next scheduled time if a backup schedule is configured.",
"description": "The automatic backup was created with errors:\n* Locations which the backup could not be uploaded to: {failed_agents}\n* Apps which could not be backed up: {failed_addons}\n* Folders which could not be backed up: {failed_folders}\n\nPlease check the Core and Supervisor logs for more information. Another attempt will be made at the next scheduled time if a backup schedule is configured.",
"title": "Automatic backup was created with errors"
},
"automatic_backup_failed_create": {

View File

@@ -324,8 +324,8 @@
"nano_nr_3": "Nano 3",
"nano_nr_4": "Nano 4",
"nano_nr_5": "Nano 5",
"off": "Off",
"on": "On",
"off": "[%key:common::state::off%]",
"on": "[%key:common::state::on%]",
"summer": "Summer",
"winter": "Winter"
}
@@ -363,8 +363,8 @@
"pump_status": {
"name": "Pump status",
"state": {
"off": "Off",
"on": "On"
"off": "[%key:common::state::off%]",
"on": "[%key:common::state::on%]"
}
},
"return_circuit_temperature": {

View File

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

View File

@@ -206,10 +206,11 @@ class MatterVacuum(MatterEntity, StateVacuumEntity):
if (
response
and response.status != clusters.ServiceArea.Enums.SelectAreasStatus.kSuccess
and response["status"]
!= clusters.ServiceArea.Enums.SelectAreasStatus.kSuccess
):
raise HomeAssistantError(
f"Failed to select areas: {response.statusText or response.status.name}"
f"Failed to select areas: {response['statusText'] or response['status']}"
)
await self.send_device_command(

View File

@@ -512,6 +512,11 @@ class OpenAISubentryFlowHandler(ConfigSubentryFlow):
options.pop(CONF_WEB_SEARCH_REGION, None)
options.pop(CONF_WEB_SEARCH_COUNTRY, None)
options.pop(CONF_WEB_SEARCH_TIMEZONE, None)
if (
user_input.get(CONF_CODE_INTERPRETER)
and user_input.get(CONF_REASONING_EFFORT) == "minimal"
):
errors[CONF_CODE_INTERPRETER] = "code_interpreter_minimal_reasoning"
options.update(user_input)
if not errors:
@@ -539,15 +544,15 @@ class OpenAISubentryFlowHandler(ConfigSubentryFlow):
if not model.startswith(("o", "gpt-5")) or model.startswith("gpt-5-pro"):
return []
MODELS_REASONING_MAP = {
models_reasoning_map: dict[str | tuple[str, ...], list[str]] = {
"gpt-5.2-pro": ["medium", "high", "xhigh"],
"gpt-5.2": ["none", "low", "medium", "high", "xhigh"],
("gpt-5.2", "gpt-5.3"): ["none", "low", "medium", "high", "xhigh"],
"gpt-5.1": ["none", "low", "medium", "high"],
"gpt-5": ["minimal", "low", "medium", "high"],
"": ["low", "medium", "high"], # The default case
}
for prefix, options in MODELS_REASONING_MAP.items():
for prefix, options in models_reasoning_map.items():
if model.startswith(prefix):
return options
return [] # pragma: no cover

View File

@@ -38,6 +38,7 @@
},
"entry_type": "AI task",
"error": {
"code_interpreter_minimal_reasoning": "[%key:component::openai_conversation::config_subentries::conversation::error::code_interpreter_minimal_reasoning%]",
"model_not_supported": "[%key:component::openai_conversation::config_subentries::conversation::error::model_not_supported%]",
"web_search_minimal_reasoning": "[%key:component::openai_conversation::config_subentries::conversation::error::web_search_minimal_reasoning%]"
},
@@ -93,6 +94,7 @@
},
"entry_type": "Conversation agent",
"error": {
"code_interpreter_minimal_reasoning": "Code interpreter is not supported with minimal reasoning effort",
"model_not_supported": "This model is not supported, please select a different model",
"web_search_minimal_reasoning": "Web search is currently not supported with minimal reasoning effort"
},

View File

@@ -137,11 +137,10 @@ class PhilipsTVLightEntity(PhilipsJsEntity, LightEntity):
_attr_effect: str
_attr_translation_key = "ambilight"
_attr_supported_color_modes = {ColorMode.HS}
_attr_supported_features = LightEntityFeature.EFFECT
def __init__(
self,
coordinator: PhilipsTVDataUpdateCoordinator,
) -> None:
def __init__(self, coordinator: PhilipsTVDataUpdateCoordinator) -> None:
"""Initialize light."""
self._tv = coordinator.api
self._hs = None
@@ -150,8 +149,6 @@ class PhilipsTVLightEntity(PhilipsJsEntity, LightEntity):
self._last_selected_effect: AmbilightEffect | None = None
super().__init__(coordinator)
self._attr_supported_color_modes = {ColorMode.HS, ColorMode.ONOFF}
self._attr_supported_features = LightEntityFeature.EFFECT
self._attr_unique_id = coordinator.unique_id
self._update_from_coordinator()

View File

@@ -16,7 +16,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import PortainerConfigEntry
from .const import CONTAINER_STATE_RUNNING, STACK_STATUS_ACTIVE
from .coordinator import PortainerContainerData, PortainerCoordinator
from .coordinator import PortainerContainerData
from .entity import (
PortainerContainerEntity,
PortainerCoordinatorData,
@@ -165,18 +165,6 @@ class PortainerEndpointSensor(PortainerEndpointEntity, BinarySensorEntity):
entity_description: PortainerEndpointBinarySensorEntityDescription
def __init__(
self,
coordinator: PortainerCoordinator,
entity_description: PortainerEndpointBinarySensorEntityDescription,
device_info: PortainerCoordinatorData,
) -> None:
"""Initialize Portainer endpoint binary sensor entity."""
self.entity_description = entity_description
super().__init__(device_info, coordinator)
self._attr_unique_id = f"{coordinator.config_entry.entry_id}_{device_info.id}_{entity_description.key}"
@property
def is_on(self) -> bool | None:
"""Return true if the binary sensor is on."""
@@ -188,19 +176,6 @@ class PortainerContainerSensor(PortainerContainerEntity, BinarySensorEntity):
entity_description: PortainerContainerBinarySensorEntityDescription
def __init__(
self,
coordinator: PortainerCoordinator,
entity_description: PortainerContainerBinarySensorEntityDescription,
device_info: PortainerContainerData,
via_device: PortainerCoordinatorData,
) -> None:
"""Initialize the Portainer container sensor."""
self.entity_description = entity_description
super().__init__(device_info, coordinator, via_device)
self._attr_unique_id = f"{coordinator.config_entry.entry_id}_{self.device_name}_{entity_description.key}"
@property
def is_on(self) -> bool | None:
"""Return true if the binary sensor is on."""
@@ -212,19 +187,6 @@ class PortainerStackSensor(PortainerStackEntity, BinarySensorEntity):
entity_description: PortainerStackBinarySensorEntityDescription
def __init__(
self,
coordinator: PortainerCoordinator,
entity_description: PortainerStackBinarySensorEntityDescription,
device_info: PortainerStackData,
via_device: PortainerCoordinatorData,
) -> None:
"""Initialize the Portainer stack sensor."""
self.entity_description = entity_description
super().__init__(device_info, coordinator, via_device)
self._attr_unique_id = f"{coordinator.config_entry.entry_id}_{device_info.stack.id}_{entity_description.key}"
@property
def is_on(self) -> bool | None:
"""Return true if the binary sensor is on."""

View File

@@ -167,18 +167,6 @@ class PortainerEndpointButton(PortainerEndpointEntity, PortainerBaseButton):
entity_description: PortainerButtonDescription
def __init__(
self,
coordinator: PortainerCoordinator,
entity_description: PortainerButtonDescription,
device_info: PortainerCoordinatorData,
) -> None:
"""Initialize the Portainer endpoint button entity."""
self.entity_description = entity_description
super().__init__(device_info, coordinator)
self._attr_unique_id = f"{coordinator.config_entry.entry_id}_{device_info.id}_{entity_description.key}"
async def _async_press_call(self) -> None:
"""Call the endpoint button press action."""
await self.entity_description.press_action(
@@ -191,19 +179,6 @@ class PortainerContainerButton(PortainerContainerEntity, PortainerBaseButton):
entity_description: PortainerButtonDescription
def __init__(
self,
coordinator: PortainerCoordinator,
entity_description: PortainerButtonDescription,
device_info: PortainerContainerData,
via_device: PortainerCoordinatorData,
) -> None:
"""Initialize the Portainer button entity."""
self.entity_description = entity_description
super().__init__(device_info, coordinator, via_device)
self._attr_unique_id = f"{coordinator.config_entry.entry_id}_{self.device_name}_{entity_description.key}"
async def _async_press_call(self) -> None:
"""Call the container button press action."""
await self.entity_description.press_action(

View File

@@ -4,6 +4,7 @@ from yarl import URL
from homeassistant.const import CONF_URL
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.entity import EntityDescription
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DEFAULT_NAME, DOMAIN
@@ -26,11 +27,13 @@ class PortainerEndpointEntity(PortainerCoordinatorEntity):
def __init__(
self,
device_info: PortainerCoordinatorData,
coordinator: PortainerCoordinator,
entity_description: EntityDescription,
device_info: PortainerCoordinatorData,
) -> None:
"""Initialize a Portainer endpoint."""
super().__init__(coordinator)
self.entity_description = entity_description
self._device_info = device_info
self.device_id = device_info.endpoint.id
self._attr_device_info = DeviceInfo(
@@ -45,6 +48,7 @@ class PortainerEndpointEntity(PortainerCoordinatorEntity):
name=device_info.endpoint.name,
entry_type=DeviceEntryType.SERVICE,
)
self._attr_unique_id = f"{coordinator.config_entry.entry_id}_{device_info.id}_{entity_description.key}"
@property
def available(self) -> bool:
@@ -57,12 +61,14 @@ class PortainerContainerEntity(PortainerCoordinatorEntity):
def __init__(
self,
device_info: PortainerContainerData,
coordinator: PortainerCoordinator,
entity_description: EntityDescription,
device_info: PortainerContainerData,
via_device: PortainerCoordinatorData,
) -> None:
"""Initialize a Portainer container."""
super().__init__(coordinator)
self.entity_description = entity_description
self._device_info = device_info
self.device_id = self._device_info.container.id
self.endpoint_id = via_device.endpoint.id
@@ -91,13 +97,14 @@ class PortainerContainerEntity(PortainerCoordinatorEntity):
# else it's the endpoint
via_device=(
DOMAIN,
f"{coordinator.config_entry.entry_id}_{self.endpoint_id}_{device_info.stack.name}"
f"{coordinator.config_entry.entry_id}_{self.endpoint_id}_stack_{device_info.stack.id}"
if device_info.stack
else f"{coordinator.config_entry.entry_id}_{self.endpoint_id}",
),
translation_key=None if self.device_name else "unknown_container",
entry_type=DeviceEntryType.SERVICE,
)
self._attr_unique_id = f"{coordinator.config_entry.entry_id}_{self.device_name}_{entity_description.key}"
@property
def available(self) -> bool:
@@ -119,12 +126,14 @@ class PortainerStackEntity(PortainerCoordinatorEntity):
def __init__(
self,
device_info: PortainerStackData,
coordinator: PortainerCoordinator,
entity_description: EntityDescription,
device_info: PortainerStackData,
via_device: PortainerCoordinatorData,
) -> None:
"""Initialize a Portainer stack."""
super().__init__(coordinator)
self.entity_description = entity_description
self._device_info = device_info
self.stack_id = device_info.stack.id
self.device_name = device_info.stack.name
@@ -135,7 +144,7 @@ class PortainerStackEntity(PortainerCoordinatorEntity):
identifiers={
(
DOMAIN,
f"{coordinator.config_entry.entry_id}_{self.endpoint_id}_{self.device_name}",
f"{coordinator.config_entry.entry_id}_{self.endpoint_id}_stack_{self.stack_id}",
)
},
manufacturer=DEFAULT_NAME,
@@ -149,6 +158,7 @@ class PortainerStackEntity(PortainerCoordinatorEntity):
f"{coordinator.config_entry.entry_id}_{self.endpoint_id}",
),
)
self._attr_unique_id = f"{coordinator.config_entry.entry_id}_{self.stack_id}_{entity_description.key}"
@property
def available(self) -> bool:

View File

@@ -21,7 +21,6 @@ from .const import STACK_TYPE_COMPOSE, STACK_TYPE_KUBERNETES, STACK_TYPE_SWARM
from .coordinator import (
PortainerConfigEntry,
PortainerContainerData,
PortainerCoordinator,
PortainerStackData,
)
from .entity import (
@@ -398,19 +397,6 @@ class PortainerContainerSensor(PortainerContainerEntity, SensorEntity):
entity_description: PortainerContainerSensorEntityDescription
def __init__(
self,
coordinator: PortainerCoordinator,
entity_description: PortainerContainerSensorEntityDescription,
device_info: PortainerContainerData,
via_device: PortainerCoordinatorData,
) -> None:
"""Initialize the Portainer container sensor."""
self.entity_description = entity_description
super().__init__(device_info, coordinator, via_device)
self._attr_unique_id = f"{coordinator.config_entry.entry_id}_{self.device_name}_{entity_description.key}"
@property
def native_value(self) -> StateType:
"""Return the state of the sensor."""
@@ -422,18 +408,6 @@ class PortainerEndpointSensor(PortainerEndpointEntity, SensorEntity):
entity_description: PortainerEndpointSensorEntityDescription
def __init__(
self,
coordinator: PortainerCoordinator,
entity_description: PortainerEndpointSensorEntityDescription,
device_info: PortainerCoordinatorData,
) -> None:
"""Initialize the Portainer endpoint sensor."""
self.entity_description = entity_description
super().__init__(device_info, coordinator)
self._attr_unique_id = f"{coordinator.config_entry.entry_id}_{device_info.id}_{entity_description.key}"
@property
def native_value(self) -> StateType:
"""Return the state of the sensor."""
@@ -446,19 +420,6 @@ class PortainerStackSensor(PortainerStackEntity, SensorEntity):
entity_description: PortainerStackSensorEntityDescription
def __init__(
self,
coordinator: PortainerCoordinator,
entity_description: PortainerStackSensorEntityDescription,
device_info: PortainerStackData,
via_device: PortainerCoordinatorData,
) -> None:
"""Initialize the Portainer stack sensor."""
self.entity_description = entity_description
super().__init__(device_info, coordinator, via_device)
self._attr_unique_id = f"{coordinator.config_entry.entry_id}_{device_info.stack.id}_{entity_description.key}"
@property
def native_value(self) -> StateType:
"""Return the state of the sensor."""

View File

@@ -167,19 +167,6 @@ class PortainerContainerSwitch(PortainerContainerEntity, SwitchEntity):
entity_description: PortainerSwitchEntityDescription
def __init__(
self,
coordinator: PortainerCoordinator,
entity_description: PortainerSwitchEntityDescription,
device_info: PortainerContainerData,
via_device: PortainerCoordinatorData,
) -> None:
"""Initialize the Portainer container switch."""
self.entity_description = entity_description
super().__init__(device_info, coordinator, via_device)
self._attr_unique_id = f"{coordinator.config_entry.entry_id}_{self.device_name}_{entity_description.key}"
@property
def is_on(self) -> bool | None:
"""Return the state of the device."""
@@ -209,19 +196,6 @@ class PortainerStackSwitch(PortainerStackEntity, SwitchEntity):
entity_description: PortainerStackSwitchEntityDescription
def __init__(
self,
coordinator: PortainerCoordinator,
entity_description: PortainerStackSwitchEntityDescription,
device_info: PortainerStackData,
via_device: PortainerCoordinatorData,
) -> None:
"""Initialize the Portainer stack switch."""
self.entity_description = entity_description
super().__init__(device_info, coordinator, via_device)
self._attr_unique_id = f"{coordinator.config_entry.entry_id}_{device_info.stack.id}_{entity_description.key}"
@property
def is_on(self) -> bool | None:
"""Return the state of the device."""

View File

@@ -269,7 +269,6 @@ class LeilSaunaClimate(LeilSaunaEntity, ClimateEntity):
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="start_session_failed",
translation_placeholders={"error": str(err)},
) from err
await self.coordinator.async_request_refresh()

View File

@@ -47,5 +47,4 @@ class LeilSaunaCoordinator(DataUpdateCoordinator[SaunumData]):
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="communication_error",
translation_placeholders={"error": str(err)},
) from err

View File

@@ -88,7 +88,7 @@
},
"exceptions": {
"communication_error": {
"message": "Communication error: {error}"
"message": "Communication error with sauna control unit"
},
"door_open": {
"message": "Cannot start sauna session when sauna door is open"
@@ -130,7 +130,7 @@
"message": "Failed to set temperature to {temperature}"
},
"start_session_failed": {
"message": "Failed to start sauna session: {error}"
"message": "Failed to start sauna session"
}
},
"options": {

View File

@@ -2,6 +2,8 @@
from __future__ import annotations
from dataclasses import fields
from aiopyarr.models.host_configuration import PyArrHostConfiguration
from aiopyarr.sonarr_client import SonarrClient
@@ -37,7 +39,6 @@ from .coordinator import (
SeriesDataUpdateCoordinator,
SonarrConfigEntry,
SonarrData,
SonarrDataUpdateCoordinator,
StatusDataUpdateCoordinator,
WantedDataUpdateCoordinator,
)
@@ -89,16 +90,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: SonarrConfigEntry) -> bo
)
# Temporary, until we add diagnostic entities
_version = None
coordinators: list[SonarrDataUpdateCoordinator] = [
data.upcoming,
data.commands,
data.diskspace,
data.queue,
data.series,
data.status,
data.wanted,
]
for coordinator in coordinators:
for field in fields(data):
coordinator = getattr(data, field.name)
await coordinator.async_config_entry_first_refresh()
if isinstance(coordinator, StatusDataUpdateCoordinator):
_version = coordinator.data.version

View File

@@ -128,35 +128,6 @@ def format_queue(
return shows
def format_episode_item(
series: SonarrSeries, episode_data: dict[str, Any], base_url: str | None = None
) -> dict[str, Any]:
"""Format a single episode item."""
result: dict[str, Any] = {
"id": episode_data.get("id"),
"episode_number": episode_data.get("episodeNumber"),
"season_number": episode_data.get("seasonNumber"),
"title": episode_data.get("title"),
"air_date": str(episode_data.get("airDate", "")),
"overview": episode_data.get("overview"),
"has_file": episode_data.get("hasFile", False),
"monitored": episode_data.get("monitored", False),
}
# Add episode images if available
if images := episode_data.get("images"):
result["images"] = {}
for image in images:
cover_type = image.coverType
# Prefer remoteUrl (public TVDB URL) over local path
if remote_url := getattr(image, "remoteUrl", None):
result["images"][cover_type] = remote_url
elif base_url and (url := getattr(image, "url", None)):
result["images"][cover_type] = f"{base_url.rstrip('/')}{url}"
return result
def format_series(
series_list: list[SonarrSeries], base_url: str | None = None
) -> dict[str, dict[str, Any]]:

View File

@@ -46,7 +46,7 @@ CONF_SEASON_NUMBER = "season_number"
CONF_SPACE_UNIT = "space_unit"
# Valid space units
SPACE_UNITS = ["bytes", "kb", "kib", "mb", "mib", "gb", "gib", "tb", "tib", "pb", "pib"]
SPACE_UNITS = ["bytes", "KB", "KiB", "MB", "MiB", "GB", "GiB", "TB", "TiB", "PB", "PiB"]
DEFAULT_SPACE_UNIT = "bytes"
# Default values - 0 means no limit

View File

@@ -78,7 +78,7 @@
"name": "Sonarr entry"
},
"space_unit": {
"description": "Unit for space values. Use binary units (kib, mib, gib, tib, pib) for 1024-based values or decimal units (kb, mb, gb, tb, pb) for 1000-based values.",
"description": "Unit for space values. Use binary units (KiB, MiB, GiB, TiB, PiB) for 1024-based values or decimal units (KB, MB, GB, TB, PB) for 1000-based values. The default is bytes.",
"name": "Space unit"
}
},

View File

@@ -1,13 +1,13 @@
{
"config": {
"abort": {
"addon_get_discovery_info_failed": "Failed to get Z-Wave app discovery info.",
"addon_info_failed": "Failed to get Z-Wave app info.",
"addon_install_failed": "Failed to install the Z-Wave app.",
"addon_required": "The Z-Wave migration flow requires the integration to be configured using the Z-Wave Supervisor app. If you are using Z-Wave JS UI, please follow our [migration instructions]({zwave_js_ui_migration}).",
"addon_set_config_failed": "Failed to set Z-Wave configuration.",
"addon_start_failed": "Failed to start the Z-Wave app.",
"addon_stop_failed": "Failed to stop the Z-Wave app.",
"addon_get_discovery_info_failed": "Failed to get Z-Wave JS app discovery info.",
"addon_info_failed": "Failed to get Z-Wave JS app info.",
"addon_install_failed": "Failed to install the Z-Wave JS app.",
"addon_required": "The Z-Wave migration flow requires the integration to be configured using the Z-Wave JS app. If you are using Z-Wave JS UI, please follow our [migration instructions]({zwave_js_ui_migration}).",
"addon_set_config_failed": "Failed to set Z-Wave JS app configuration.",
"addon_start_failed": "Failed to start the Z-Wave JS app.",
"addon_stop_failed": "Failed to stop the Z-Wave JS app.",
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
"backup_failed": "Failed to back up network.",
@@ -17,15 +17,15 @@
"discovery_requires_supervisor": "Discovery requires the Home Assistant Supervisor.",
"migration_low_sdk_version": "The SDK version of the old adapter is lower than {ok_sdk_version}. This means it's not possible to migrate the non-volatile memory (NVM) of the old adapter to another adapter.\n\nCheck the documentation on the manufacturer support pages of the old adapter, if it's possible to upgrade the firmware of the old adapter to a version that is built with SDK version {ok_sdk_version} or higher.",
"migration_successful": "Migration successful.",
"not_hassio": "ESPHome discovery requires Home Assistant to configure the Z-Wave app.",
"not_hassio": "ESPHome discovery requires Home Assistant to configure the Z-Wave JS app.",
"not_zwave_device": "Discovered device is not a Z-Wave device.",
"not_zwave_js_addon": "Discovered app is not the official Z-Wave app.",
"not_zwave_js_addon": "Discovered app is not the official Z-Wave JS app.",
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]",
"reset_failed": "Failed to reset adapter.",
"usb_ports_failed": "Failed to get USB devices."
},
"error": {
"addon_start_failed": "Failed to start the Z-Wave app. Check the configuration.",
"addon_start_failed": "Failed to start the Z-Wave JS app. Check the configuration.",
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_ws_url": "Invalid websocket URL",
"unknown": "[%key:common::config_flow::error::unknown%]"
@@ -65,7 +65,7 @@
"usb_path": "[%key:common::config_flow::data::usb_path%]"
},
"description": "Select your Z-Wave adapter",
"title": "Enter the Z-Wave app configuration"
"title": "Enter the Z-Wave JS app configuration"
},
"configure_security_keys": {
"data": {
@@ -84,7 +84,7 @@
"title": "Migrate to a new adapter"
},
"hassio_confirm": {
"description": "Do you want to set up the Z-Wave integration with the Z-Wave app?"
"description": "Do you want to set up the Z-Wave integration with the Z-Wave JS app?"
},
"install_addon": {
"title": "Installing app"
@@ -127,9 +127,9 @@
},
"on_supervisor": {
"data": {
"use_addon": "Use the Z-Wave Supervisor app"
"use_addon": "Use the Z-Wave JS app"
},
"description": "Do you want to use the Z-Wave Supervisor app?",
"description": "Do you want to use the Z-Wave JS app?",
"title": "Select connection method"
},
"on_supervisor_reconfigure": {

View File

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

View File

@@ -381,7 +381,7 @@
"iot_class": "local_push"
},
"anthropic": {
"name": "Anthropic Conversation",
"name": "Anthropic",
"integration_type": "service",
"config_flow": true,
"iot_class": "cloud_polling"

View File

@@ -40,7 +40,7 @@ habluetooth==5.8.0
hass-nabucasa==1.15.0
hassil==3.5.0
home-assistant-bluetooth==1.13.1
home-assistant-frontend==20260225.0
home-assistant-frontend==20260226.0
home-assistant-intents==2026.2.13
httpx==0.28.1
ifaddr==0.2.0

View File

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

4
requirements_all.txt generated
View File

@@ -130,7 +130,7 @@ TwitterAPI==2.7.12
WSDiscovery==2.1.2
# homeassistant.components.accuweather
accuweather==5.0.0
accuweather==5.1.0
# homeassistant.components.actron_air
actron-neo-api==0.4.1
@@ -1226,7 +1226,7 @@ hole==0.9.0
holidays==0.84
# homeassistant.components.frontend
home-assistant-frontend==20260225.0
home-assistant-frontend==20260226.0
# homeassistant.components.conversation
home-assistant-intents==2026.2.13

View File

@@ -121,7 +121,7 @@ Tami4EdgeAPI==3.0
WSDiscovery==2.1.2
# homeassistant.components.accuweather
accuweather==5.0.0
accuweather==5.1.0
# homeassistant.components.actron_air
actron-neo-api==0.4.1
@@ -1087,7 +1087,7 @@ hole==0.9.0
holidays==0.84
# homeassistant.components.frontend
home-assistant-frontend==20260225.0
home-assistant-frontend==20260226.0
# homeassistant.components.conversation
home-assistant-intents==2026.2.13

View File

@@ -365,12 +365,11 @@ async def test_vacuum_clean_area(
},
)
# Mock a successful SelectAreasResponse
matter_client.send_device_command.return_value = (
clusters.ServiceArea.Commands.SelectAreasResponse(
status=clusters.ServiceArea.Enums.SelectAreasStatus.kSuccess,
)
)
# Mock a successful SelectAreasResponse (returns as dict over websocket)
matter_client.send_device_command.return_value = {
"status": clusters.ServiceArea.Enums.SelectAreasStatus.kSuccess,
"statusText": "",
}
await hass.services.async_call(
VACUUM_DOMAIN,
@@ -420,13 +419,11 @@ async def test_vacuum_clean_area_select_areas_failure(
},
)
# Mock a failed SelectAreasResponse
matter_client.send_device_command.return_value = (
clusters.ServiceArea.Commands.SelectAreasResponse(
status=clusters.ServiceArea.Enums.SelectAreasStatus.kUnsupportedArea,
statusText="Area 7 not supported",
)
)
# Mock a failed SelectAreasResponse (returns as dict over websocket)
matter_client.send_device_command.return_value = {
"status": clusters.ServiceArea.Enums.SelectAreasStatus.kUnsupportedArea,
"statusText": "Area 7 not supported",
}
with pytest.raises(HomeAssistantError, match="Failed to select areas"):
await hass.services.async_call(

View File

@@ -267,6 +267,7 @@ async def test_subentry_unsupported_model(
("gpt-5.1", ["none", "low", "medium", "high"]),
("gpt-5.2", ["none", "low", "medium", "high", "xhigh"]),
("gpt-5.2-pro", ["medium", "high", "xhigh"]),
("gpt-5.3-codex", ["none", "low", "medium", "high", "xhigh"]),
],
)
async def test_subentry_reasoning_effort_list(
@@ -311,8 +312,15 @@ async def test_subentry_reasoning_effort_list(
)
async def test_subentry_websearch_unsupported_reasoning_effort(
hass: HomeAssistant, mock_config_entry, mock_init_component
@pytest.mark.parametrize(
("parameter", "error"),
[
(CONF_WEB_SEARCH, "web_search_minimal_reasoning"),
(CONF_CODE_INTERPRETER, "code_interpreter_minimal_reasoning"),
],
)
async def test_subentry_unsupported_reasoning_effort(
hass: HomeAssistant, mock_config_entry, mock_init_component, parameter, error
) -> None:
"""Test the subentry form giving error about unsupported minimal reasoning effort."""
subentry = next(iter(mock_config_entry.subentries.values()))
@@ -349,18 +357,18 @@ async def test_subentry_websearch_unsupported_reasoning_effort(
subentry_flow["flow_id"],
{
CONF_REASONING_EFFORT: "minimal",
CONF_WEB_SEARCH: True,
parameter: True,
},
)
assert subentry_flow["type"] is FlowResultType.FORM
assert subentry_flow["errors"] == {"web_search": "web_search_minimal_reasoning"}
assert subentry_flow["errors"] == {parameter: error}
# Reconfigure model step
subentry_flow = await hass.config_entries.subentries.async_configure(
subentry_flow["flow_id"],
{
CONF_REASONING_EFFORT: "low",
CONF_WEB_SEARCH: True,
parameter: True,
},
)
assert subentry_flow["type"] is FlowResultType.ABORT

View File

@@ -0,0 +1,208 @@
# serializer version: 1
# name: test_device_registry
list([
DeviceRegistryEntrySnapshot({
'area_id': None,
'config_entries': <ANY>,
'config_entries_subentries': <ANY>,
'configuration_url': 'https://127.0.0.1:9000/#!/1/docker/dashboard',
'connections': set({
}),
'disabled_by': None,
'entry_type': <DeviceEntryType.SERVICE: 'service'>,
'hw_version': None,
'id': <ANY>,
'identifiers': set({
tuple(
'portainer',
'portainer_test_entry_123_1',
),
}),
'labels': set({
}),
'manufacturer': 'Portainer',
'model': 'Endpoint',
'model_id': None,
'name': 'my-environment',
'name_by_user': None,
'primary_config_entry': <ANY>,
'serial_number': None,
'sw_version': None,
'via_device_id': None,
}),
DeviceRegistryEntrySnapshot({
'area_id': None,
'config_entries': <ANY>,
'config_entries_subentries': <ANY>,
'configuration_url': 'https://127.0.0.1:9000/#!/1/docker/containers/aa86eacfb3b3ed4cd362c1e88fc89a53908ad05fb3a4103bca3f9b28292d14bf',
'connections': set({
}),
'disabled_by': None,
'entry_type': <DeviceEntryType.SERVICE: 'service'>,
'hw_version': None,
'id': <ANY>,
'identifiers': set({
tuple(
'portainer',
'portainer_test_entry_123_1_funny_chatelet',
),
}),
'labels': set({
}),
'manufacturer': 'Portainer',
'model': 'Container',
'model_id': None,
'name': 'funny_chatelet',
'name_by_user': None,
'primary_config_entry': <ANY>,
'serial_number': None,
'sw_version': None,
'via_device_id': <ANY>,
}),
DeviceRegistryEntrySnapshot({
'area_id': None,
'config_entries': <ANY>,
'config_entries_subentries': <ANY>,
'configuration_url': 'https://127.0.0.1:9000/#!/1/docker/containers/dd19facfb3b3ed4cd362c1e88fc89a53908ad05fb3a4103bca3f9b28292d14bf',
'connections': set({
}),
'disabled_by': None,
'entry_type': <DeviceEntryType.SERVICE: 'service'>,
'hw_version': None,
'id': <ANY>,
'identifiers': set({
tuple(
'portainer',
'portainer_test_entry_123_1_focused_einstein',
),
}),
'labels': set({
}),
'manufacturer': 'Portainer',
'model': 'Container',
'model_id': None,
'name': 'focused_einstein',
'name_by_user': None,
'primary_config_entry': <ANY>,
'serial_number': None,
'sw_version': None,
'via_device_id': <ANY>,
}),
DeviceRegistryEntrySnapshot({
'area_id': None,
'config_entries': <ANY>,
'config_entries_subentries': <ANY>,
'configuration_url': 'https://127.0.0.1:9000/#!/1/docker/containers/ee20facfb3b3ed4cd362c1e88fc89a53908ad05fb3a4103bca3f9b28292d14bf',
'connections': set({
}),
'disabled_by': None,
'entry_type': <DeviceEntryType.SERVICE: 'service'>,
'hw_version': None,
'id': <ANY>,
'identifiers': set({
tuple(
'portainer',
'portainer_test_entry_123_1_practical_morse',
),
}),
'labels': set({
}),
'manufacturer': 'Portainer',
'model': 'Container',
'model_id': None,
'name': 'practical_morse',
'name_by_user': None,
'primary_config_entry': <ANY>,
'serial_number': None,
'sw_version': None,
'via_device_id': <ANY>,
}),
DeviceRegistryEntrySnapshot({
'area_id': None,
'config_entries': <ANY>,
'config_entries_subentries': <ANY>,
'configuration_url': 'https://127.0.0.1:9000/#!/1/docker/stacks/webstack',
'connections': set({
}),
'disabled_by': None,
'entry_type': None,
'hw_version': None,
'id': <ANY>,
'identifiers': set({
tuple(
'portainer',
'portainer_test_entry_123_1_stack_1',
),
}),
'labels': set({
}),
'manufacturer': 'Portainer',
'model': 'Stack',
'model_id': None,
'name': 'webstack',
'name_by_user': None,
'primary_config_entry': <ANY>,
'serial_number': None,
'sw_version': None,
'via_device_id': <ANY>,
}),
DeviceRegistryEntrySnapshot({
'area_id': None,
'config_entries': <ANY>,
'config_entries_subentries': <ANY>,
'configuration_url': 'https://127.0.0.1:9000/#!/1/docker/containers/bb97facfb3b3ed4cd362c1e88fc89a53908ad05fb3a4103bca3f9b28292d14bf',
'connections': set({
}),
'disabled_by': None,
'entry_type': <DeviceEntryType.SERVICE: 'service'>,
'hw_version': None,
'id': <ANY>,
'identifiers': set({
tuple(
'portainer',
'portainer_test_entry_123_1_serene_banach',
),
}),
'labels': set({
}),
'manufacturer': 'Portainer',
'model': 'Container',
'model_id': None,
'name': 'serene_banach',
'name_by_user': None,
'primary_config_entry': <ANY>,
'serial_number': None,
'sw_version': None,
'via_device_id': <ANY>,
}),
DeviceRegistryEntrySnapshot({
'area_id': None,
'config_entries': <ANY>,
'config_entries_subentries': <ANY>,
'configuration_url': 'https://127.0.0.1:9000/#!/1/docker/containers/cc08facfb3b3ed4cd362c1e88fc89a53908ad05fb3a4103bca3f9b28292d14bf',
'connections': set({
}),
'disabled_by': None,
'entry_type': <DeviceEntryType.SERVICE: 'service'>,
'hw_version': None,
'id': <ANY>,
'identifiers': set({
tuple(
'portainer',
'portainer_test_entry_123_1_stoic_turing',
),
}),
'labels': set({
}),
'manufacturer': 'Portainer',
'model': 'Container',
'model_id': None,
'name': 'stoic_turing',
'name_by_user': None,
'primary_config_entry': <ANY>,
'serial_number': None,
'sw_version': None,
'via_device_id': <ANY>,
}),
])
# ---

View File

@@ -8,6 +8,7 @@ from pyportainer.exceptions import (
PortainerTimeoutError,
)
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.portainer.const import DOMAIN
from homeassistant.config_entries import ConfigEntryState
@@ -166,3 +167,19 @@ async def test_migration_v3_to_v4(
(DOMAIN, f"{entry.entry_id}_1_adguard"),
}
assert entity_after.unique_id == f"{entry.entry_id}_1_adguard_container"
async def test_device_registry(
hass: HomeAssistant,
mock_portainer_client: AsyncMock,
mock_config_entry: MockConfigEntry,
device_registry: dr.DeviceRegistry,
snapshot: SnapshotAssertion,
) -> None:
"""Test devices are correctly registered."""
await setup_integration(hass, mock_config_entry)
device_entries = dr.async_entries_for_config_entry(
device_registry, mock_config_entry.entry_id
)
assert device_entries == snapshot