Compare commits

...

93 Commits

Author SHA1 Message Date
Franck Nijhof
2c47e83342 2026.3.2 (#165675) 2026-03-16 13:23:27 +01:00
Franck Nijhof
e3c6a2184d Bump version to 2026.3.2 2026-03-16 10:27:01 +00:00
Simone Chemelli
0ba0829350 Bump aiocomelit to 2.0.1 (#165663) 2026-03-16 10:25:08 +00:00
Allen Porter
678048e681 Upgrade ical dependency to 13.2.2. (#165642) 2026-03-16 10:25:07 +00:00
Jan Bouwhuis
743eeeae53 Fix MQTT device tracker overrides via JSON state attributes without reset (#165529) 2026-03-16 10:25:05 +00:00
Raj Laud
46555c6d9a Fix victron_ble warning sensor using duplicate alarm translation key (#165502)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-16 10:25:04 +00:00
Simone Chemelli
dbaca0a723 Bump aioamazondevices to 13.0.1 (#165476) 2026-03-16 10:25:02 +00:00
Joost Lekkerkerker
9bb2959029 Bump pySmartThings to 3.7.0 (#165468) 2026-03-16 10:25:01 +00:00
Robert Resch
0304781fa9 Bump orjson to 3.11.7 (#165443) 2026-03-16 10:25:00 +00:00
J. Nick Koston
e081d28aa4 Handle OAuth token request exceptions in Yale setup (#165430) 2026-03-16 10:24:58 +00:00
TheJulianJES
34aa28c72f Bump ZHA to 1.0.2 (#165423) 2026-03-16 10:24:56 +00:00
Bram Kragten
cfa2946db8 Update frontend to 20260312.0 (#165420) 2026-03-16 10:24:55 +00:00
Galorhallen
1b0779347c Update govee local api to 2.4.0 (#165418) 2026-03-16 10:24:54 +00:00
Joost Lekkerkerker
93a281e7af Remove stateclass from timestamp entity in Intellifire (#165403) 2026-03-16 10:24:53 +00:00
Josef Zweck
6b32e27fd3 Bump onedrive-personal-sdk to 0.1.7 (#165401) 2026-03-16 10:24:51 +00:00
Zach Feldman
79928a8c7c August oauth2 exception migration (#165397)
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-03-16 10:24:50 +00:00
Steve Easley
9146518e13 Bump pyjvcprojector to 2.0.3 (#165327) 2026-03-16 10:24:48 +00:00
Dan Raper
e9c5172f43 Bump ohme to 1.7.0 (#165318) 2026-03-16 10:24:47 +00:00
TheJulianJES
cce21ad4b9 Bump python-otbr-api to 2.9.0 (#165298) 2026-03-16 10:24:46 +00:00
Simone Chemelli
10ec02ca3c Fix switch set for Vodafone Station (#165273) 2026-03-16 10:18:26 +00:00
Josef Zweck
bdf54491e5 Bump onedrive-personal-sdk to 0.1.6 (#165219) 2026-03-16 10:18:25 +00:00
Bram Kragten
0b05d34238 Add reorder support to area selector (#165211) 2026-03-16 10:18:24 +00:00
Åke Strandberg
4c69a1c5f7 Add missing code for Miele dryer (#165122) 2026-03-16 10:17:00 +00:00
Steve Easley
6f1f56dcaa Bump jvc_projector dependency to 2.0.2 (#165099) 2026-03-16 10:16:59 +00:00
Jordan Harvey
d0b9991232 Bump pyanglianwater to 3.1.1 (#165097) 2026-03-16 10:16:58 +00:00
Artur Pragacz
aacf39be8a Make restore state resilient to extra_restore_state_data errors (#165086) 2026-03-16 10:16:56 +00:00
Erwin Douna
bf055da82c Bump pyportainer to 1.0.33 (#165080) 2026-03-16 10:12:26 +00:00
Erwin Douna
0fb118bcd9 Bump pyportainer 1.0.32 (#164803) 2026-03-16 10:12:25 +00:00
Erwin Douna
954ef7d1f5 Fix forced VERIFY_SSL in Portainer (#165079) 2026-03-16 09:56:32 +00:00
Joakim Plate
b091299320 Update pychromecast to 14.0.10 (#165069) 2026-03-16 09:56:31 +00:00
J. Nick Koston
52483e18b2 Bump yalexs-ble to 3.2.8 (#165018) 2026-03-16 09:56:29 +00:00
AlCalzone
57e8683ed7 Fix cover state updates for legacy Multilevel Switch based Z-Wave covers (#165003) 2026-03-16 09:56:28 +00:00
Simone Chemelli
67faace978 Fix dnd switch status for Alexa Devices (#164953) 2026-03-16 09:56:26 +00:00
Simone Chemelli
e4be64fcb1 Fix wifi switch status and add 100% coverage for Fritz (#164696) 2026-03-16 09:56:25 +00:00
Franck Nijhof
f552b8221f 2026.3.1 (#165001) 2026-03-06 22:10:34 +01:00
Franck Nijhof
55dc5392f9 Bump version to 2026.3.1 2026-03-06 20:37:19 +00:00
Karl Beecken
5b93aeae38 Bump teltasync to 0.2.0 (#164995) 2026-03-06 20:37:03 +00:00
Shay Levy
33610bb1a1 Bump aioswitcher to 6.1.1 (#164981) 2026-03-06 20:37:01 +00:00
Manu
6c3cebe413 Change setpoint step size in IronOS integration (#164979) 2026-03-06 20:37:00 +00:00
Willem-Jan van Rootselaar
5346895d9b Bump python-bsblan to 5.1.2 (#164963) 2026-03-06 20:36:58 +00:00
Willem-Jan van Rootselaar
05c3f08c6c Bump python-bsblan to 5.1.1 (#164591) 2026-03-06 20:36:57 +00:00
Daniel Hjelseth Høyer
1ce025733d Fix energy unit in Homevolt (#164959)
Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>
2026-03-06 20:35:22 +00:00
Simone Chemelli
1537ea86b8 Bump aiovodafone to 3.1.3 (#164955) 2026-03-06 20:35:21 +00:00
Luke Lashley
ec137870fa Pass in Base Url during Roborock reauth (#164903) 2026-03-06 20:35:20 +00:00
Josef Zweck
816ee7f53e Bump onedrive-personal-sdk to 0.1.5 (#164880) 2026-03-06 20:35:18 +00:00
Petro31
6e7eeec827 Fix 'this' variable in template options flow (#164866) 2026-03-06 20:35:17 +00:00
Marc Mueller
d100477a22 Fix volvo test RuntimeWarning (#164845) 2026-03-06 20:35:16 +00:00
Matthias Alphart
98ac6dd2c1 Fix KNX sensor default attributes for energy and volume DPTs (#164838)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-06 20:35:14 +00:00
John O'Nolan
6b30969f60 Fix Ghost config flow using wrong field name for site UUID (#164836) 2026-03-06 20:35:13 +00:00
Joshua Leaper
e9a6b5d662 Update ness_alarm scan interval to 5 secs (#164835) 2026-03-06 20:35:11 +00:00
Glenn de Haan
f95f3f9982 Add device class to active_liter_lpm sensor (#164809) 2026-03-06 20:35:10 +00:00
epenet
3f884a8cd1 Remove caio from licenses exception list (#164806) 2026-03-06 20:35:09 +00:00
Raphael Hehl
10f284932e Enforce SSRF redirect protection only for connector allowed_protocol_schema_set (#164769)
Co-authored-by: RaHehl <rahehl@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-03-06 20:35:07 +00:00
Sean O'Keeffe
e1c4e6dc42 more programs for Miele steam ovens (#164768)
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-03-06 20:35:06 +00:00
Ian Foster
0976e7de4e Update keyboard_remote dependencies (#164755) 2026-03-06 20:35:05 +00:00
Antonio Mello
ae1012b2f0 Fix IntesisHome outdoor_temp not reported when value is 0.0 (#164703)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 20:35:03 +00:00
TimL
bb7c4faca5 Fix button entity creation for devices with more than two radios (#164699) 2026-03-06 20:35:02 +00:00
Tucker Kern
0b1be61336 Ensure Snapcast client has a valid current group before accessing group attributes. (#164683) 2026-03-06 20:35:00 +00:00
Glenn Waters
3ec44024a2 Hunter Douglas Powerview: Fix missing class in hierarchy. (#164264)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-03-06 20:34:59 +00:00
Joost Lekkerkerker
1200cc5779 Bump spotifyaio to 2.0.2 (#164114)
Co-authored-by: Robert Resch <robert@resch.dev>
2026-03-06 20:34:58 +00:00
Blake Messer
d632931f74 Fix Rain Bird controllers updated by Rain Bird 2.x (#163915) 2026-03-06 20:34:56 +00:00
Franck Nijhof
2f9faa53a1 2026.3.0 (#164757) 2026-03-04 20:17:05 +01:00
Joost Lekkerkerker
718607a758 Revert "Add diagnostics platform to AWS S3 (#164118)" (#164759) 2026-03-04 19:01:47 +01:00
Franck Nijhof
3789156559 Revert "Add diagnostics platform to AWS S3 (#164118)"
This reverts commit 37d2c946e8.
2026-03-04 17:53:29 +00:00
Franck Nijhof
042ce6f2de Bump version to 2026.3.0 2026-03-04 17:30:58 +00:00
Franck Nijhof
0a5908002f Bump version to 2026.3.0b4 2026-03-04 17:09:32 +00:00
Petro31
3a5f71e10a Fix this variable preview issue with template entities from the UI (#164740) 2026-03-04 17:09:18 +00:00
rappenze
04e4b05ab0 Fix handling of several thermostat QuickApp's in fibaro (#164344)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-04 17:09:17 +00:00
Franck Nijhof
c2c5232899 Bump version to 2026.3.0b3 2026-03-04 14:30:26 +00:00
Stefan Agner
593610094e Ignore transient empty segments in Matter vacuum (#164737)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-04 14:25:12 +00:00
Bram Kragten
47cb7870ea Update frontend to 20260304.0 (#164736) 2026-03-04 14:25:11 +00:00
Joakim Plate
045b626e24 Restore handling of is active input for chromecast (#164735) 2026-03-04 14:25:09 +00:00
Artur Pragacz
bea5468dee Add backup integration to recovery mode (#164734) 2026-03-04 14:25:08 +00:00
Erwin Douna
04fc12cc26 Bump pyportainer 1.0.31 (#164733) 2026-03-04 14:25:07 +00:00
starkillerOG
fec33ad42b Bump reolink-aio to 0.19.1 (#164732) 2026-03-04 14:25:06 +00:00
TheJulianJES
07e323f1e9 Bump ZHA to 1.0.1 (#164709) 2026-03-04 14:25:04 +00:00
Ariel Ebersberger
ebe2612713 Influxdb repair issue follow up (#164684) 2026-03-04 14:25:03 +00:00
Michael Hansen
88ca668562 Bump intents to 2026.3.3 (#164676) 2026-03-04 14:25:01 +00:00
Robert Resch
1d46ac0b64 Fix wheels building by using arch dependent requirements_all file (#164675) 2026-03-04 14:25:00 +00:00
starkillerOG
13a5e6e85f Fix Reolink entity unique_id migration when unique_id already exists (#164667) 2026-03-04 14:24:58 +00:00
TimL
d2665f03ff Bump pysmlight to v0.2.16 (#164665)
Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
2026-03-04 14:24:56 +00:00
hanwg
80412e4973 Update subentry description for Telegram bot (#164642)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-04 14:24:55 +00:00
Matthias Alphart
818d9f774e Update knx-frontend to 2026.3.2.183756 (#164623) 2026-03-04 14:24:54 +00:00
starkillerOG
012e78d625 Fix key error in Reolink DHCP if still setting up (#164619) 2026-03-04 14:24:53 +00:00
Simone Chemelli
74abedbcd2 Bump aioamazondevices to 13.0.0 (#164618) 2026-03-04 14:24:51 +00:00
Tom
e16fb6b5a5 Add informative errors to Proxmox VE buttons (#164417) 2026-03-04 14:24:50 +00:00
Artur Pragacz
8906e5dcb5 Trigger recovery mode on registry major version downgrade (#164340) 2026-03-04 14:24:49 +00:00
Abílio Costa
10067c208a Add Ubisys virtual integration (#164314) 2026-03-04 14:24:48 +00:00
Ariel Ebersberger
d4143205e9 Add repair issue after importing influxdb yaml config (#164145)
Co-authored-by: Norbert Rittel <norbert@rittel.de>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-04 14:24:46 +00:00
Miguel Angel Nubla
a4da363ff2 Fix infinite loop in esphome assist_satellite (#163097)
Co-authored-by: Artur Pragacz <artur@pragacz.com>
2026-03-04 14:24:45 +00:00
Christian Lackas
bc9ae3dad6 Fix HomematicIP heating group availability with unreachable members (#162571) 2026-03-04 14:24:44 +00:00
J. Diego Rodríguez Royo
9e5daaa784 Improve mobile_app notify.notify with not connected targets (#161855) 2026-03-04 14:24:42 +00:00
Daniel Schneider
ff0a6757cd Bump ring-doorbell to 0.9.14 (#158074)
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-03-04 14:24:41 +00:00
192 changed files with 4768 additions and 1493 deletions

View File

@@ -209,4 +209,4 @@ jobs:
skip-binary: aiohttp;charset-normalizer;grpcio;multidict;SQLAlchemy;propcache;protobuf;pymicro-vad;yarl
constraints: "homeassistant/package_constraints.txt"
requirements-diff: "requirements_diff.txt"
requirements: "requirements_all.txt"
requirements: "requirements_all_wheels_${{ matrix.arch }}.txt"

View File

@@ -70,7 +70,7 @@ from .const import (
SIGNAL_BOOTSTRAP_INTEGRATIONS,
)
from .core_config import async_process_ha_core_config
from .exceptions import HomeAssistantError
from .exceptions import HomeAssistantError, UnsupportedStorageVersionError
from .helpers import (
area_registry,
category_registry,
@@ -239,6 +239,8 @@ DEFAULT_INTEGRATIONS = {
}
DEFAULT_INTEGRATIONS_RECOVERY_MODE = {
# These integrations are set up if recovery mode is activated.
"backup",
"cloud",
"frontend",
}
DEFAULT_INTEGRATIONS_SUPERVISOR = {
@@ -433,32 +435,56 @@ def _init_blocking_io_modules_in_executor() -> None:
is_docker_env()
async def async_load_base_functionality(hass: core.HomeAssistant) -> None:
"""Load the registries and modules that will do blocking I/O."""
async def async_load_base_functionality(hass: core.HomeAssistant) -> bool:
"""Load the registries and modules that will do blocking I/O.
Return whether loading succeeded.
"""
if DATA_REGISTRIES_LOADED in hass.data:
return
return True
hass.data[DATA_REGISTRIES_LOADED] = None
entity.async_setup(hass)
frame.async_setup(hass)
template.async_setup(hass)
translation.async_setup(hass)
await asyncio.gather(
create_eager_task(get_internal_store_manager(hass).async_initialize()),
create_eager_task(area_registry.async_load(hass)),
create_eager_task(category_registry.async_load(hass)),
create_eager_task(device_registry.async_load(hass)),
create_eager_task(entity_registry.async_load(hass)),
create_eager_task(floor_registry.async_load(hass)),
create_eager_task(issue_registry.async_load(hass)),
create_eager_task(label_registry.async_load(hass)),
hass.async_add_executor_job(_init_blocking_io_modules_in_executor),
create_eager_task(template.async_load_custom_templates(hass)),
create_eager_task(restore_state.async_load(hass)),
create_eager_task(hass.config_entries.async_initialize()),
create_eager_task(async_get_system_info(hass)),
create_eager_task(condition.async_setup(hass)),
create_eager_task(trigger.async_setup(hass)),
)
recovery = hass.config.recovery_mode
try:
await asyncio.gather(
create_eager_task(get_internal_store_manager(hass).async_initialize()),
create_eager_task(area_registry.async_load(hass, load_empty=recovery)),
create_eager_task(category_registry.async_load(hass, load_empty=recovery)),
create_eager_task(device_registry.async_load(hass, load_empty=recovery)),
create_eager_task(entity_registry.async_load(hass, load_empty=recovery)),
create_eager_task(floor_registry.async_load(hass, load_empty=recovery)),
create_eager_task(issue_registry.async_load(hass, load_empty=recovery)),
create_eager_task(label_registry.async_load(hass, load_empty=recovery)),
hass.async_add_executor_job(_init_blocking_io_modules_in_executor),
create_eager_task(template.async_load_custom_templates(hass)),
create_eager_task(restore_state.async_load(hass, load_empty=recovery)),
create_eager_task(hass.config_entries.async_initialize()),
create_eager_task(async_get_system_info(hass)),
create_eager_task(condition.async_setup(hass)),
create_eager_task(trigger.async_setup(hass)),
)
except UnsupportedStorageVersionError as err:
# If we're already in recovery mode, we don't want to handle the exception
# and activate recovery mode again, as that would lead to an infinite loop.
if recovery:
raise
_LOGGER.error(
"Storage file %s was created by a newer version of Home Assistant"
" (storage version %s > %s); activating recovery mode; on-disk data"
" is preserved; upgrade Home Assistant or restore from a backup",
err.storage_key,
err.found_version,
err.max_supported_version,
)
return False
return True
async def async_from_config_dict(
@@ -475,7 +501,9 @@ async def async_from_config_dict(
# Prime custom component cache early so we know if registry entries are tied
# to a custom integration
await loader.async_get_custom_components(hass)
await async_load_base_functionality(hass)
if not await async_load_base_functionality(hass):
return None
# Set up core.
_LOGGER.debug("Setting up %s", CORE_INTEGRATIONS)

View File

@@ -0,0 +1,5 @@
{
"domain": "ubisys",
"name": "Ubisys",
"iot_standards": ["zigbee"]
}

View File

@@ -1,6 +1,5 @@
"""Defines a base Alexa Devices entity."""
from aioamazondevices.const.devices import SPEAKER_GROUP_MODEL
from aioamazondevices.structures import AmazonDevice
from homeassistant.helpers.device_registry import DeviceInfo
@@ -25,19 +24,15 @@ class AmazonEntity(CoordinatorEntity[AmazonDevicesCoordinator]):
"""Initialize the entity."""
super().__init__(coordinator)
self._serial_num = serial_num
model_details = coordinator.api.get_model_details(self.device) or {}
model = model_details.get("model")
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, serial_num)},
name=self.device.account_name,
model=model,
model=self.device.model,
model_id=self.device.device_type,
manufacturer=model_details.get("manufacturer", "Amazon"),
hw_version=model_details.get("hw_version"),
sw_version=(
self.device.software_version if model != SPEAKER_GROUP_MODEL else None
),
serial_number=serial_num if model != SPEAKER_GROUP_MODEL else None,
manufacturer=self.device.manufacturer or "Amazon",
hw_version=self.device.hardware_version,
sw_version=self.device.software_version,
serial_number=serial_num,
)
self.entity_description = description
self._attr_unique_id = f"{serial_num}-{description.key}"

View File

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

View File

@@ -101,7 +101,10 @@ class AmazonSwitchEntity(AmazonEntity, SwitchEntity):
assert method is not None
await method(self.device, state)
await self.coordinator.async_request_refresh()
self.coordinator.data[self.device.serial_number].sensors[
self.entity_description.key
].value = state
self.async_write_ha_state()
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the switch on."""

View File

@@ -9,5 +9,5 @@
"iot_class": "cloud_polling",
"loggers": ["pyanglianwater"],
"quality_scale": "bronze",
"requirements": ["pyanglianwater==3.1.0"]
"requirements": ["pyanglianwater==3.1.1"]
}

View File

@@ -5,7 +5,7 @@ from __future__ import annotations
from pathlib import Path
from typing import cast
from aiohttp import ClientResponseError
from aiohttp import ClientError
from yalexs.exceptions import AugustApiAIOHTTPError
from yalexs.manager.exceptions import CannotConnect, InvalidAuth, RequireValidation
from yalexs.manager.gateway import Config as YaleXSConfig
@@ -13,7 +13,12 @@ from yalexs.manager.gateway import Config as YaleXSConfig
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.exceptions import (
ConfigEntryAuthFailed,
ConfigEntryNotReady,
OAuth2TokenRequestError,
OAuth2TokenRequestReauthError,
)
from homeassistant.helpers import device_registry as dr, issue_registry as ir
from homeassistant.helpers.config_entry_oauth2_flow import (
ImplementationUnavailableError,
@@ -45,11 +50,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: AugustConfigEntry) -> bo
august_gateway = AugustGateway(Path(hass.config.config_dir), session, oauth_session)
try:
await async_setup_august(hass, entry, august_gateway)
except OAuth2TokenRequestReauthError as err:
raise ConfigEntryAuthFailed from err
except (RequireValidation, InvalidAuth) as err:
raise ConfigEntryAuthFailed from err
except TimeoutError as err:
raise ConfigEntryNotReady("Timed out connecting to august api") from err
except (AugustApiAIOHTTPError, ClientResponseError, CannotConnect) as err:
except (
AugustApiAIOHTTPError,
OAuth2TokenRequestError,
ClientError,
CannotConnect,
) as err:
raise ConfigEntryNotReady from err
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True

View File

@@ -30,5 +30,5 @@
"integration_type": "hub",
"iot_class": "cloud_push",
"loggers": ["pubnub", "yalexs"],
"requirements": ["yalexs==9.2.0", "yalexs-ble==3.2.7"]
"requirements": ["yalexs==9.2.0", "yalexs-ble==3.2.8"]
}

View File

@@ -1,55 +0,0 @@
"""Diagnostics support for AWS S3."""
from __future__ import annotations
import dataclasses
from typing import Any
from homeassistant.components.backup import (
DATA_MANAGER as BACKUP_DATA_MANAGER,
BackupManager,
)
from homeassistant.components.diagnostics import async_redact_data
from homeassistant.core import HomeAssistant
from .const import (
CONF_ACCESS_KEY_ID,
CONF_BUCKET,
CONF_PREFIX,
CONF_SECRET_ACCESS_KEY,
DOMAIN,
)
from .coordinator import S3ConfigEntry
from .helpers import async_list_backups_from_s3
TO_REDACT = (CONF_ACCESS_KEY_ID, CONF_SECRET_ACCESS_KEY)
async def async_get_config_entry_diagnostics(
hass: HomeAssistant,
entry: S3ConfigEntry,
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
coordinator = entry.runtime_data
backup_manager: BackupManager = hass.data[BACKUP_DATA_MANAGER]
backups = await async_list_backups_from_s3(
coordinator.client,
bucket=entry.data[CONF_BUCKET],
prefix=entry.data.get(CONF_PREFIX, ""),
)
data = {
"coordinator_data": dataclasses.asdict(coordinator.data),
"config": {
**entry.data,
**entry.options,
},
"backup_agents": [
{"name": agent.name}
for agent in backup_manager.backup_agents.values()
if agent.domain == DOMAIN
],
"backup": [backup.as_dict() for backup in backups],
}
return async_redact_data(data, TO_REDACT)

View File

@@ -43,7 +43,7 @@ rules:
# Gold
devices: done
diagnostics: done
diagnostics: todo
discovery-update-info:
status: exempt
comment: S3 is a cloud service that is not discovered on the network.

View File

@@ -29,12 +29,17 @@ class StoredBackupData(TypedDict):
class _BackupStore(Store[StoredBackupData]):
"""Class to help storing backup data."""
# Maximum version we support reading for forward compatibility.
# This allows reading data written by a newer HA version after downgrade.
_MAX_READABLE_VERSION = 2
def __init__(self, hass: HomeAssistant) -> None:
"""Initialize storage class."""
super().__init__(
hass,
STORAGE_VERSION,
STORAGE_KEY,
max_readable_version=self._MAX_READABLE_VERSION,
minor_version=STORAGE_VERSION_MINOR,
)
@@ -86,8 +91,8 @@ class _BackupStore(Store[StoredBackupData]):
# data["config"]["schedule"]["state"] will be removed. The bump to 2 is
# planned to happen after a 6 month quiet period with no minor version
# changes.
# Reject if major version is higher than 2.
if old_major_version > 2:
# Reject if major version is higher than _MAX_READABLE_VERSION.
if old_major_version > self._MAX_READABLE_VERSION:
raise NotImplementedError
return data

View File

@@ -8,7 +8,7 @@
"iot_class": "local_polling",
"loggers": ["bsblan"],
"quality_scale": "silver",
"requirements": ["python-bsblan==5.1.0"],
"requirements": ["python-bsblan==5.1.2"],
"zeroconf": [
{
"name": "bsb-lan*",

View File

@@ -15,7 +15,7 @@
"integration_type": "hub",
"iot_class": "local_polling",
"loggers": ["casttube", "pychromecast"],
"requirements": ["PyChromecast==14.0.9"],
"requirements": ["PyChromecast==14.0.10"],
"single_config_entry": true,
"zeroconf": ["_googlecast._tcp.local."]
}

View File

@@ -804,8 +804,22 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity):
@property
def state(self) -> MediaPlayerState | None:
"""Return the state of the player."""
# The lovelace app loops media to prevent timing out, don't show that
if (chromecast := self._chromecast) is None or (
cast_status := self.cast_status
) is None:
# Not connected to any chromecast, or not yet got any status
return None
if (
chromecast.cast_type == pychromecast.const.CAST_TYPE_CHROMECAST
and not chromecast.ignore_cec
and cast_status.is_active_input is False
):
# The display interface for the device has been turned off or switched away
return MediaPlayerState.OFF
if self.app_id == CAST_APP_ID_HOMEASSISTANT_LOVELACE:
# The lovelace app loops media to prevent timing out, don't show that
return MediaPlayerState.PLAYING
if (media_status := self._media_status()[0]) is not None:
@@ -822,16 +836,12 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity):
# Some apps don't report media status, show the player as playing
return MediaPlayerState.PLAYING
if self.app_id is not None and self.app_id != pychromecast.config.APP_BACKDROP:
# We have an active app
return MediaPlayerState.IDLE
if self._chromecast is not None and self._chromecast.is_idle:
# If library consider us idle, that is our off state
# it takes HDMI status into account for cast devices.
if self.app_id in (pychromecast.IDLE_APP_ID, None):
# We have no active app or the home screen app. This is
# same app as APP_BACKDROP.
return MediaPlayerState.OFF
return None
return MediaPlayerState.IDLE
@property
def media_content_id(self) -> str | None:

View File

@@ -8,5 +8,5 @@
"iot_class": "local_polling",
"loggers": ["aiocomelit"],
"quality_scale": "platinum",
"requirements": ["aiocomelit==2.0.0"]
"requirements": ["aiocomelit==2.0.1"]
}

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.2.13"]
"requirements": ["hassil==3.5.0", "home-assistant-intents==2026.3.3"]
}

View File

@@ -524,14 +524,10 @@ class EsphomeAssistSatellite(
self._active_pipeline_index = 0
maybe_pipeline_index = 0
while True:
if not (ww_entity_id := self.get_wake_word_entity(maybe_pipeline_index)):
break
if not (ww_state := self.hass.states.get(ww_entity_id)):
continue
if ww_state.state == wake_word_phrase:
while ww_entity_id := self.get_wake_word_entity(maybe_pipeline_index):
if (
ww_state := self.hass.states.get(ww_entity_id)
) and ww_state.state == wake_word_phrase:
# First match
self._active_pipeline_index = maybe_pipeline_index
break

View File

@@ -275,8 +275,11 @@ class FibaroController:
# otherwise add the first visible device in the group
# which is a hack, but solves a problem with FGT having
# hidden compatibility devices before the real device
if last_climate_parent != device.parent_fibaro_id or (
device.has_endpoint_id and last_endpoint != device.endpoint_id
# Second hack is for quickapps which have parent id 0 and no children
if (
last_climate_parent != device.parent_fibaro_id
or (device.has_endpoint_id and last_endpoint != device.endpoint_id)
or device.parent_fibaro_id == 0
):
_LOGGER.debug("Handle separately")
self.fibaro_devices[platform].append(device)

View File

@@ -133,26 +133,20 @@ async def _async_wifi_entities_list(
]
)
_LOGGER.debug("WiFi networks count: %s", wifi_count)
networks: dict = {}
networks: dict[int, dict[str, Any]] = {}
for i in range(1, wifi_count + 1):
network_info = await avm_wrapper.async_get_wlan_configuration(i)
# Devices with 4 WLAN services, use the 2nd for internal communications
if not (wifi_count == 4 and i == 2):
networks[i] = {
"ssid": network_info["NewSSID"],
"bssid": network_info["NewBSSID"],
"standard": network_info["NewStandard"],
"enabled": network_info["NewEnable"],
"status": network_info["NewStatus"],
}
networks[i] = network_info
for i, network in networks.copy().items():
networks[i]["switch_name"] = network["ssid"]
networks[i]["switch_name"] = network["NewSSID"]
if (
len(
[
j
for j, n in networks.items()
if slugify(n["ssid"]) == slugify(network["ssid"])
if slugify(n["NewSSID"]) == slugify(network["NewSSID"])
]
)
> 1
@@ -434,13 +428,11 @@ class FritzBoxPortSwitch(FritzBoxBaseSwitch):
for key, attr in attributes_dict.items():
self._attributes[attr] = self.port_mapping[key]
async def _async_switch_on_off_executor(self, turn_on: bool) -> bool:
async def _async_switch_on_off_executor(self, turn_on: bool) -> None:
self.port_mapping["NewEnabled"] = "1" if turn_on else "0"
resp = await self._avm_wrapper.async_add_port_mapping(
await self._avm_wrapper.async_add_port_mapping(
self.connection_type, self.port_mapping
)
return bool(resp is not None)
class FritzBoxDeflectionSwitch(FritzBoxBaseCoordinatorSwitch):
@@ -525,12 +517,11 @@ class FritzBoxProfileSwitch(FritzDeviceBase, SwitchEntity):
"""Turn off switch."""
await self._async_handle_turn_on_off(turn_on=False)
async def _async_handle_turn_on_off(self, turn_on: bool) -> bool:
async def _async_handle_turn_on_off(self, turn_on: bool) -> None:
"""Handle switch state change request."""
await self._avm_wrapper.async_set_allow_wan_access(self.ip_address, turn_on)
self._avm_wrapper.devices[self._mac].wan_access = turn_on
self.async_write_ha_state()
return True
class FritzBoxWifiSwitch(FritzBoxBaseSwitch):
@@ -541,10 +532,11 @@ class FritzBoxWifiSwitch(FritzBoxBaseSwitch):
avm_wrapper: AvmWrapper,
device_friendly_name: str,
network_num: int,
network_data: dict,
network_data: dict[str, Any],
) -> None:
"""Init Fritz Wifi switch."""
self._avm_wrapper = avm_wrapper
self._wifi_info = network_data
self._attributes = {}
self._attr_entity_category = EntityCategory.CONFIG
@@ -560,7 +552,7 @@ class FritzBoxWifiSwitch(FritzBoxBaseSwitch):
type=SWITCH_TYPE_WIFINETWORK,
callback_update=self._async_fetch_update,
callback_switch=self._async_switch_on_off_executor,
init_state=network_data["enabled"],
init_state=network_data["NewEnable"],
)
super().__init__(self._avm_wrapper, device_friendly_name, switch_info)
@@ -587,7 +579,9 @@ class FritzBoxWifiSwitch(FritzBoxBaseSwitch):
self._attributes["mac_address_control"] = wifi_info[
"NewMACAddressControlEnabled"
]
self._wifi_info = wifi_info
async def _async_switch_on_off_executor(self, turn_on: bool) -> None:
"""Handle wifi switch."""
self._wifi_info["NewEnable"] = turn_on
await self._avm_wrapper.async_set_wlan_configuration(self._network_num, turn_on)

View File

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

View File

@@ -89,7 +89,7 @@ class GhostConfigFlow(ConfigFlow, domain=DOMAIN):
site_title = site["title"]
await self.async_set_unique_id(site["uuid"])
await self.async_set_unique_id(site["site_uuid"])
self._abort_if_unique_id_configured()
return self.async_create_entry(

View File

@@ -8,5 +8,5 @@
"integration_type": "service",
"iot_class": "cloud_polling",
"loggers": ["googleapiclient"],
"requirements": ["gcal-sync==8.0.0", "oauth2client==4.1.3", "ical==13.2.0"]
"requirements": ["gcal-sync==8.0.0", "oauth2client==4.1.3", "ical==13.2.2"]
}

View File

@@ -6,5 +6,5 @@
"dependencies": ["network"],
"documentation": "https://www.home-assistant.io/integrations/govee_light_local",
"iot_class": "local_push",
"requirements": ["govee-local-api==2.3.0"]
"requirements": ["govee-local-api==2.4.0"]
}

View File

@@ -88,6 +88,17 @@ class HomematicipHeatingGroup(HomematicipGenericEntity, ClimateEntity):
if device.actualTemperature is None:
self._simple_heating = self._first_radiator_thermostat
@property
def available(self) -> bool:
"""Heating group available.
A heating group must be available, and should not be affected by the
individual availability of group members.
This allows controlling the temperature even when individual group
members are not available.
"""
return True
@property
def device_info(self) -> DeviceInfo:
"""Return device specific attributes."""

View File

@@ -312,6 +312,17 @@ class HomematicipCoverShutterGroup(HomematicipGenericEntity, CoverEntity):
device.modelType = f"HmIP-{post}"
super().__init__(hap, device, post, is_multi_channel=False)
@property
def available(self) -> bool:
"""Cover shutter group available.
A cover shutter group must be available, and should not be affected by
the individual availability of group members.
This allows controlling the shutters even when individual group
members are not available.
"""
return True
@property
def current_cover_position(self) -> int | None:
"""Return current position of cover."""

View File

@@ -91,14 +91,14 @@ SENSORS: tuple[SensorEntityDescription, ...] = (
translation_key="energy_exported",
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
),
SensorEntityDescription(
key="energy_imported",
translation_key="energy_imported",
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
),
SensorEntityDescription(
key="frequency",

View File

@@ -610,6 +610,7 @@ SENSORS: Final[tuple[HomeWizardSensorEntityDescription, ...]] = (
key="active_liter_lpm",
translation_key="active_liter_lpm",
native_unit_of_measurement=UnitOfVolumeFlowRate.LITERS_PER_MINUTE,
device_class=SensorDeviceClass.VOLUME_FLOW_RATE,
state_class=SensorStateClass.MEASUREMENT,
has_fn=lambda data: data.measurement.active_liter_lpm is not None,
value_fn=lambda data: data.measurement.active_liter_lpm,

View File

@@ -901,7 +901,9 @@ class PowerViewShadeDualOverlappedRear(PowerViewShadeDualOverlappedBase):
)
class PowerViewShadeDualOverlappedCombinedTilt(PowerViewShadeDualOverlappedCombined):
class PowerViewShadeDualOverlappedCombinedTilt(
PowerViewShadeDualOverlappedCombined, PowerViewShadeWithTiltBase
):
"""Represent a shade that has a front sheer and rear opaque panel.
This equates to two shades being controlled by one motor.
@@ -915,26 +917,6 @@ class PowerViewShadeDualOverlappedCombinedTilt(PowerViewShadeDualOverlappedCombi
Type 10 - Duolite with 180° Tilt
"""
# type
def __init__(
self,
coordinator: PowerviewShadeUpdateCoordinator,
device_info: PowerviewDeviceInfo,
room_name: str,
shade: BaseShade,
name: str,
) -> None:
"""Initialize the shade."""
super().__init__(coordinator, device_info, room_name, shade, name)
self._attr_supported_features |= (
CoverEntityFeature.OPEN_TILT
| CoverEntityFeature.CLOSE_TILT
| CoverEntityFeature.SET_TILT_POSITION
)
if self._shade.is_supported(MOTION_STOP):
self._attr_supported_features |= CoverEntityFeature.STOP_TILT
self._max_tilt = self._shade.shade_limits.tilt_max
@property
def transition_steps(self) -> int:
"""Return the steps to make a move."""
@@ -949,26 +931,6 @@ class PowerViewShadeDualOverlappedCombinedTilt(PowerViewShadeDualOverlappedCombi
tilt = self.positions.tilt
return ceil(primary + secondary + tilt)
@callback
def _get_shade_tilt(self, target_hass_tilt_position: int) -> ShadePosition:
"""Return a ShadePosition."""
return ShadePosition(
tilt=target_hass_tilt_position,
velocity=self.positions.velocity,
)
@property
def open_tilt_position(self) -> ShadePosition:
"""Return the open tilt position and required additional positions."""
return replace(self._shade.open_position_tilt, velocity=self.positions.velocity)
@property
def close_tilt_position(self) -> ShadePosition:
"""Return the open tilt position and required additional positions."""
return replace(
self._shade.close_position_tilt, velocity=self.positions.velocity
)
TYPE_TO_CLASSES = {
0: (PowerViewShade,),

View File

@@ -43,6 +43,7 @@ from homeassistant.const import (
STATE_UNKNOWN,
)
from homeassistant.core import Event, HomeAssistant, State, callback
from homeassistant.data_entry_flow import FlowResultType
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import config_validation as cv, state as state_helper
from homeassistant.helpers.entity_values import EntityValues
@@ -61,6 +62,7 @@ from .const import (
CLIENT_ERROR_V2,
CODE_INVALID_INPUTS,
COMPONENT_CONFIG_SCHEMA_CONNECTION,
COMPONENT_CONFIG_SCHEMA_CONNECTION_VALIDATORS,
CONF_API_VERSION,
CONF_BUCKET,
CONF_COMPONENT_CONFIG,
@@ -79,7 +81,6 @@ from .const import (
CONF_TAGS_ATTRIBUTES,
CONNECTION_ERROR,
DEFAULT_API_VERSION,
DEFAULT_HOST,
DEFAULT_HOST_V2,
DEFAULT_MEASUREMENT_ATTR,
DEFAULT_SSL_V2,
@@ -104,6 +105,7 @@ from .const import (
WRITE_ERROR,
WROTE_MESSAGE,
)
from .issue import async_create_deprecated_yaml_issue
_LOGGER = logging.getLogger(__name__)
@@ -137,7 +139,7 @@ def create_influx_url(conf: dict) -> dict:
def validate_version_specific_config(conf: dict) -> dict:
"""Ensure correct config fields are provided based on API version used."""
if conf[CONF_API_VERSION] == API_VERSION_2:
if conf.get(CONF_API_VERSION, DEFAULT_API_VERSION) == API_VERSION_2:
if CONF_TOKEN not in conf:
raise vol.Invalid(
f"{CONF_TOKEN} and {CONF_BUCKET} are required when"
@@ -193,32 +195,13 @@ _INFLUX_BASE_SCHEMA = INCLUDE_EXCLUDE_BASE_FILTER_SCHEMA.extend(
}
)
INFLUX_SCHEMA = vol.All(
_INFLUX_BASE_SCHEMA.extend(COMPONENT_CONFIG_SCHEMA_CONNECTION),
validate_version_specific_config,
create_influx_url,
INFLUX_SCHEMA = _INFLUX_BASE_SCHEMA.extend(
COMPONENT_CONFIG_SCHEMA_CONNECTION_VALIDATORS
)
CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: vol.All(
cv.deprecated(CONF_API_VERSION),
cv.deprecated(CONF_HOST),
cv.deprecated(CONF_PATH),
cv.deprecated(CONF_PORT),
cv.deprecated(CONF_SSL),
cv.deprecated(CONF_VERIFY_SSL),
cv.deprecated(CONF_SSL_CA_CERT),
cv.deprecated(CONF_USERNAME),
cv.deprecated(CONF_PASSWORD),
cv.deprecated(CONF_DB_NAME),
cv.deprecated(CONF_TOKEN),
cv.deprecated(CONF_ORG),
cv.deprecated(CONF_BUCKET),
INFLUX_SCHEMA,
)
},
{DOMAIN: vol.All(INFLUX_SCHEMA, validate_version_specific_config)},
extra=vol.ALLOW_EXTRA,
)
@@ -499,23 +482,35 @@ def get_influx_connection( # noqa: C901
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the InfluxDB component."""
conf = config.get(DOMAIN)
if DOMAIN not in config:
return True
if conf is not None:
if CONF_HOST not in conf and conf[CONF_API_VERSION] == DEFAULT_API_VERSION:
conf[CONF_HOST] = DEFAULT_HOST
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_IMPORT},
data=conf,
)
)
hass.async_create_task(_async_setup(hass, config[DOMAIN]))
return True
async def _async_setup(hass: HomeAssistant, config: dict[str, Any]) -> None:
"""Import YAML configuration into a config entry."""
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_IMPORT},
data=config,
)
if (
result.get("type") is FlowResultType.ABORT
and (reason := result["reason"]) != "single_instance_allowed"
):
async_create_deprecated_yaml_issue(hass, error=reason)
return
# If we are here, the entry already exists (single instance allowed)
if config.keys() & (
{k.schema for k in COMPONENT_CONFIG_SCHEMA_CONNECTION} - {CONF_PRECISION}
):
async_create_deprecated_yaml_issue(hass)
async def async_setup_entry(hass: HomeAssistant, entry: InfluxDBConfigEntry) -> bool:
"""Set up InfluxDB from a config entry."""
data = entry.data

View File

@@ -31,7 +31,7 @@ from homeassistant.helpers.selector import (
)
from homeassistant.helpers.storage import STORAGE_DIR
from . import DOMAIN, get_influx_connection
from . import DOMAIN, create_influx_url, get_influx_connection
from .const import (
API_VERSION_2,
CONF_API_VERSION,
@@ -40,8 +40,11 @@ from .const import (
CONF_ORG,
CONF_SSL_CA_CERT,
DEFAULT_API_VERSION,
DEFAULT_BUCKET,
DEFAULT_DATABASE,
DEFAULT_HOST,
DEFAULT_PORT,
DEFAULT_VERIFY_SSL,
)
_LOGGER = logging.getLogger(__name__)
@@ -240,14 +243,17 @@ class InfluxDBConfigFlow(ConfigFlow, domain=DOMAIN):
async def async_step_import(self, import_data: dict[str, Any]) -> ConfigFlowResult:
"""Handle the initial step."""
host = import_data.get(CONF_HOST)
database = import_data.get(CONF_DB_NAME)
bucket = import_data.get(CONF_BUCKET)
import_data = {**import_data}
import_data.setdefault(CONF_API_VERSION, DEFAULT_API_VERSION)
import_data.setdefault(CONF_VERIFY_SSL, DEFAULT_VERIFY_SSL)
import_data.setdefault(CONF_DB_NAME, DEFAULT_DATABASE)
import_data.setdefault(CONF_BUCKET, DEFAULT_BUCKET)
api_version = import_data.get(CONF_API_VERSION)
ssl = import_data.get(CONF_SSL)
api_version = import_data[CONF_API_VERSION]
if api_version == DEFAULT_API_VERSION:
host = import_data.get(CONF_HOST, DEFAULT_HOST)
database = import_data[CONF_DB_NAME]
title = f"{database} ({host})"
data = {
CONF_API_VERSION: api_version,
@@ -256,21 +262,23 @@ class InfluxDBConfigFlow(ConfigFlow, domain=DOMAIN):
CONF_USERNAME: import_data.get(CONF_USERNAME),
CONF_PASSWORD: import_data.get(CONF_PASSWORD),
CONF_DB_NAME: database,
CONF_SSL: ssl,
CONF_SSL: import_data.get(CONF_SSL),
CONF_PATH: import_data.get(CONF_PATH),
CONF_VERIFY_SSL: import_data.get(CONF_VERIFY_SSL),
CONF_VERIFY_SSL: import_data[CONF_VERIFY_SSL],
CONF_SSL_CA_CERT: import_data.get(CONF_SSL_CA_CERT),
}
else:
create_influx_url(import_data) # Only modifies dict for api_version == 2
bucket = import_data[CONF_BUCKET]
url = import_data.get(CONF_URL)
title = f"{bucket} ({url})"
data = {
CONF_API_VERSION: api_version,
CONF_URL: import_data.get(CONF_URL),
CONF_URL: url,
CONF_TOKEN: import_data.get(CONF_TOKEN),
CONF_ORG: import_data.get(CONF_ORG),
CONF_BUCKET: bucket,
CONF_VERIFY_SSL: import_data.get(CONF_VERIFY_SSL),
CONF_VERIFY_SSL: import_data[CONF_VERIFY_SSL],
CONF_SSL_CA_CERT: import_data.get(CONF_SSL_CA_CERT),
}

View File

@@ -154,3 +154,14 @@ COMPONENT_CONFIG_SCHEMA_CONNECTION = {
vol.Inclusive(CONF_ORG, "v2_authentication"): cv.string,
vol.Optional(CONF_BUCKET, default=DEFAULT_BUCKET): cv.string,
}
# Same keys without defaults, used in CONFIG_SCHEMA to validate
# without injecting default values (so we can detect explicit keys).
COMPONENT_CONFIG_SCHEMA_CONNECTION_VALIDATORS = {
(
vol.Optional(k.schema)
if isinstance(k, vol.Optional) and k.default is not vol.UNDEFINED
else k
): v
for k, v in COMPONENT_CONFIG_SCHEMA_CONNECTION.items()
}

View File

@@ -0,0 +1,34 @@
"""Issues for InfluxDB integration."""
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
from .const import DOMAIN
@callback
def async_create_deprecated_yaml_issue(
hass: HomeAssistant, *, error: str | None = None
) -> None:
"""Create a repair issue for deprecated YAML connection configuration."""
if error is None:
issue_id = "deprecated_yaml"
severity = IssueSeverity.WARNING
else:
issue_id = f"deprecated_yaml_import_issue_{error}"
severity = IssueSeverity.ERROR
async_create_issue(
hass,
DOMAIN,
issue_id,
is_fixable=False,
issue_domain=DOMAIN,
breaks_in_ha_version="2026.9.0",
severity=severity,
translation_key=issue_id,
translation_placeholders={
"domain": DOMAIN,
"url": f"/config/integrations/dashboard/add?domain={DOMAIN}",
},
)

View File

@@ -7,7 +7,6 @@
"documentation": "https://www.home-assistant.io/integrations/influxdb",
"iot_class": "local_push",
"loggers": ["influxdb", "influxdb_client"],
"quality_scale": "legacy",
"requirements": ["influxdb==5.3.1", "influxdb-client==1.50.0"],
"single_config_entry": true
}

View File

@@ -54,5 +54,31 @@
"title": "Choose InfluxDB version"
}
}
},
"issues": {
"deprecated_yaml": {
"description": "Configuring InfluxDB connection settings using YAML is being removed. Your existing YAML connection configuration has been imported into the UI automatically.\n\nRemove the `{domain}` connection and authentication keys from your `configuration.yaml` file and restart Home Assistant to fix this issue. Other options like `include`, `exclude`, and `tags` remain in YAML for now. \n\nThe following keys should be removed:\n- `api_version`\n- `host`\n- `port`\n- `ssl`\n- `verify_ssl`\n- `ssl_ca_cert`\n- `username`\n- `password`\n- `database`\n- `token`\n- `organization`\n- `bucket`\n- `path`",
"title": "The InfluxDB YAML configuration is being removed"
},
"deprecated_yaml_import_issue_cannot_connect": {
"description": "Configuring InfluxDB connection settings using YAML is being removed but the import failed because Home Assistant could not connect to the InfluxDB server.\n\nPlease correct your YAML configuration and restart Home Assistant.\n\nAlternatively you can remove the `{domain}` connection and authentication keys from your `configuration.yaml` file and continue to [set up the integration]({url}) manually. \n\nThe following keys should be removed:\n- `api_version`\n- `host`\n- `port`\n- `ssl`\n- `verify_ssl`\n- `ssl_ca_cert`\n- `username`\n- `password`\n- `database`\n- `token`\n- `organization`\n- `bucket`\n- `path`",
"title": "Failed to import InfluxDB YAML configuration"
},
"deprecated_yaml_import_issue_invalid_auth": {
"description": "Configuring InfluxDB connection settings using YAML is being removed but the import failed because the provided credentials are invalid.\n\nPlease correct your YAML configuration and restart Home Assistant.\n\nAlternatively you can remove the `{domain}` connection and authentication keys from your `configuration.yaml` file and continue to [set up the integration]({url}) manually. \n\nThe following keys should be removed:\n- `api_version`\n- `host`\n- `port`\n- `ssl`\n- `verify_ssl`\n- `ssl_ca_cert`\n- `username`\n- `password`\n- `database`\n- `token`\n- `organization`\n- `bucket`\n- `path`",
"title": "[%key:component::influxdb::issues::deprecated_yaml_import_issue_cannot_connect::title%]"
},
"deprecated_yaml_import_issue_invalid_database": {
"description": "Configuring InfluxDB connection settings using YAML is being removed but the import failed because the specified database was not found.\n\nPlease correct your YAML configuration and restart Home Assistant.\n\nAlternatively you can remove the `{domain}` connection and authentication keys from your `configuration.yaml` file and continue to [set up the integration]({url}) manually. \n\nThe following keys should be removed:\n- `api_version`\n- `host`\n- `port`\n- `ssl`\n- `verify_ssl`\n- `ssl_ca_cert`\n- `username`\n- `password`\n- `database`\n- `token`\n- `organization`\n- `bucket`\n- `path`",
"title": "[%key:component::influxdb::issues::deprecated_yaml_import_issue_cannot_connect::title%]"
},
"deprecated_yaml_import_issue_ssl_error": {
"description": "Configuring InfluxDB connection settings using YAML is being removed but the import failed due to an SSL certificate error.\n\nPlease correct your YAML configuration and restart Home Assistant.\n\nAlternatively you can remove the `{domain}` connection and authentication keys from your `configuration.yaml` file and continue to [set up the integration]({url}) manually. \n\nThe following keys should be removed:\n- `api_version`\n- `host`\n- `port`\n- `ssl`\n- `verify_ssl`\n- `ssl_ca_cert`\n- `username`\n- `password`\n- `database`\n- `token`\n- `organization`\n- `bucket`\n- `path`",
"title": "[%key:component::influxdb::issues::deprecated_yaml_import_issue_cannot_connect::title%]"
},
"deprecated_yaml_import_issue_unknown": {
"description": "Configuring InfluxDB connection settings using YAML is being removed but the import failed due to an unknown error.\n\nPlease correct your YAML configuration and restart Home Assistant.\n\nAlternatively you can remove the `{domain}` connection and authentication keys from your `configuration.yaml` file and continue to [set up the integration]({url}) manually. \n\nThe following keys should be removed:\n- `api_version`\n- `host`\n- `port`\n- `ssl`\n- `verify_ssl`\n- `ssl_ca_cert`\n- `username`\n- `password`\n- `database`\n- `token`\n- `organization`\n- `bucket`\n- `path`",
"title": "[%key:component::influxdb::issues::deprecated_yaml_import_issue_cannot_connect::title%]"
}
}
}

View File

@@ -97,7 +97,6 @@ INTELLIFIRE_SENSORS: tuple[IntellifireSensorEntityDescription, ...] = (
IntellifireSensorEntityDescription(
key="timer_end_timestamp",
translation_key="timer_end_timestamp",
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.TIMESTAMP,
value_fn=_time_remaining_to_timestamp,
),

View File

@@ -221,13 +221,13 @@ class IntesisAC(ClimateEntity):
def extra_state_attributes(self) -> dict[str, Any]:
"""Return the device specific state attributes."""
attrs = {}
if self._outdoor_temp:
if self._outdoor_temp is not None:
attrs["outdoor_temp"] = self._outdoor_temp
if self._power_consumption_heat:
if self._power_consumption_heat is not None:
attrs["power_consumption_heat_kw"] = round(
self._power_consumption_heat / 1000, 1
)
if self._power_consumption_cool:
if self._power_consumption_cool is not None:
attrs["power_consumption_cool_kw"] = round(
self._power_consumption_cool / 1000, 1
)
@@ -244,7 +244,7 @@ class IntesisAC(ClimateEntity):
if hvac_mode := kwargs.get(ATTR_HVAC_MODE):
await self.async_set_hvac_mode(hvac_mode)
if temperature := kwargs.get(ATTR_TEMPERATURE):
if (temperature := kwargs.get(ATTR_TEMPERATURE)) is not None:
_LOGGER.debug("Setting %s to %s degrees", self._device_type, temperature)
await self._controller.set_temperature(self._device_id, temperature)
self._attr_target_temperature = temperature
@@ -271,7 +271,7 @@ class IntesisAC(ClimateEntity):
await self._controller.set_mode(self._device_id, MAP_HVAC_MODE_TO_IH[hvac_mode])
# Send the temperature again in case changing modes has changed it
if self._attr_target_temperature:
if self._attr_target_temperature is not None:
await self._controller.set_temperature(
self._device_id, self._attr_target_temperature
)

View File

@@ -358,7 +358,7 @@ PINECIL_SETPOINT_NUMBER_DESCRIPTION = IronOSNumberEntityDescription(
native_max_value=MAX_TEMP,
native_min_value_f=MIN_TEMP_F,
native_max_value_f=MAX_TEMP_F,
native_step=5,
native_step=1,
)

View File

@@ -7,5 +7,5 @@
"integration_type": "device",
"iot_class": "local_polling",
"loggers": ["jvcprojector"],
"requirements": ["pyjvcprojector==2.0.1"]
"requirements": ["pyjvcprojector==2.0.3"]
}

View File

@@ -7,5 +7,5 @@
"iot_class": "local_push",
"loggers": ["aionotify", "evdev"],
"quality_scale": "legacy",
"requirements": ["evdev==1.6.1", "asyncinotify==4.2.0"]
"requirements": ["evdev==1.9.3", "asyncinotify==4.4.0"]
}

View File

@@ -8,6 +8,7 @@ from xknx.dpt import DPTBase, DPTComplex, DPTEnum, DPTNumeric
from xknx.dpt.dpt_16 import DPTString
from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass
from homeassistant.const import UnitOfReactiveEnergy
HaDptClass = Literal["numeric", "enum", "complex", "string"]
@@ -36,7 +37,7 @@ def get_supported_dpts() -> Mapping[str, DPTInfo]:
main=dpt_class.dpt_main_number, # type: ignore[typeddict-item] # checked in xknx unit tests
sub=dpt_class.dpt_sub_number,
name=dpt_class.value_type,
unit=dpt_class.unit,
unit=_sensor_unit_overrides.get(dpt_number_str, dpt_class.unit),
sensor_device_class=_sensor_device_classes.get(dpt_number_str),
sensor_state_class=_get_sensor_state_class(ha_dpt_class, dpt_number_str),
)
@@ -77,13 +78,13 @@ _sensor_device_classes: Mapping[str, SensorDeviceClass] = {
"12.1200": SensorDeviceClass.VOLUME,
"12.1201": SensorDeviceClass.VOLUME,
"13.002": SensorDeviceClass.VOLUME_FLOW_RATE,
"13.010": SensorDeviceClass.ENERGY,
"13.012": SensorDeviceClass.REACTIVE_ENERGY,
"13.013": SensorDeviceClass.ENERGY,
"13.015": SensorDeviceClass.REACTIVE_ENERGY,
"13.016": SensorDeviceClass.ENERGY,
"13.1200": SensorDeviceClass.VOLUME,
"13.1201": SensorDeviceClass.VOLUME,
"13.010": SensorDeviceClass.ENERGY, # DPTActiveEnergy
"13.012": SensorDeviceClass.REACTIVE_ENERGY, # DPTReactiveEnergy
"13.013": SensorDeviceClass.ENERGY, # DPTActiveEnergykWh
"13.015": SensorDeviceClass.REACTIVE_ENERGY, # DPTReactiveEnergykVARh
"13.016": SensorDeviceClass.ENERGY, # DPTActiveEnergyMWh
"13.1200": SensorDeviceClass.VOLUME, # DPTDeltaVolumeLiquidLitre
"13.1201": SensorDeviceClass.VOLUME, # DPTDeltaVolumeM3
"14.010": SensorDeviceClass.AREA,
"14.019": SensorDeviceClass.CURRENT,
"14.027": SensorDeviceClass.VOLTAGE,
@@ -91,7 +92,7 @@ _sensor_device_classes: Mapping[str, SensorDeviceClass] = {
"14.030": SensorDeviceClass.VOLTAGE,
"14.031": SensorDeviceClass.ENERGY,
"14.033": SensorDeviceClass.FREQUENCY,
"14.037": SensorDeviceClass.ENERGY_STORAGE,
"14.037": SensorDeviceClass.ENERGY_STORAGE, # DPTHeatQuantity
"14.039": SensorDeviceClass.DISTANCE,
"14.051": SensorDeviceClass.WEIGHT,
"14.056": SensorDeviceClass.POWER,
@@ -101,7 +102,7 @@ _sensor_device_classes: Mapping[str, SensorDeviceClass] = {
"14.068": SensorDeviceClass.TEMPERATURE,
"14.069": SensorDeviceClass.TEMPERATURE,
"14.070": SensorDeviceClass.TEMPERATURE_DELTA,
"14.076": SensorDeviceClass.VOLUME,
"14.076": SensorDeviceClass.VOLUME, # DPTVolume
"14.077": SensorDeviceClass.VOLUME_FLOW_RATE,
"14.080": SensorDeviceClass.APPARENT_POWER,
"14.1200": SensorDeviceClass.VOLUME_FLOW_RATE,
@@ -121,17 +122,28 @@ _sensor_state_class_overrides: Mapping[str, SensorStateClass | None] = {
"13.010": SensorStateClass.TOTAL, # DPTActiveEnergy
"13.011": SensorStateClass.TOTAL, # DPTApparantEnergy
"13.012": SensorStateClass.TOTAL, # DPTReactiveEnergy
"13.013": SensorStateClass.TOTAL, # DPTActiveEnergykWh
"13.015": SensorStateClass.TOTAL, # DPTReactiveEnergykVARh
"13.016": SensorStateClass.TOTAL, # DPTActiveEnergyMWh
"13.1200": SensorStateClass.TOTAL, # DPTDeltaVolumeLiquidLitre
"13.1201": SensorStateClass.TOTAL, # DPTDeltaVolumeM3
"14.007": SensorStateClass.MEASUREMENT_ANGLE, # DPTAngleDeg
"14.037": SensorStateClass.TOTAL, # DPTHeatQuantity
"14.051": SensorStateClass.TOTAL, # DPTMass
"14.055": SensorStateClass.MEASUREMENT_ANGLE, # DPTPhaseAngleDeg
"14.031": SensorStateClass.TOTAL_INCREASING, # DPTEnergy
"14.076": SensorStateClass.TOTAL, # DPTVolume
"17.001": None, # DPTSceneNumber
"29.010": SensorStateClass.TOTAL, # DPTActiveEnergy8Byte
"29.011": SensorStateClass.TOTAL, # DPTApparantEnergy8Byte
"29.012": SensorStateClass.TOTAL, # DPTReactiveEnergy8Byte
}
_sensor_unit_overrides: Mapping[str, str] = {
"13.012": UnitOfReactiveEnergy.VOLT_AMPERE_REACTIVE_HOUR, # DPTReactiveEnergy (VARh in KNX)
"13.015": UnitOfReactiveEnergy.KILO_VOLT_AMPERE_REACTIVE_HOUR, # DPTReactiveEnergykVARh (kVARh in KNX)
"29.012": UnitOfReactiveEnergy.VOLT_AMPERE_REACTIVE_HOUR, # DPTReactiveEnergy8Byte (VARh in KNX)
}
def _get_sensor_state_class(
ha_dpt_class: HaDptClass, dpt_number_str: str

View File

@@ -13,7 +13,7 @@
"requirements": [
"xknx==3.15.0",
"xknxproject==3.8.2",
"knx-frontend==2026.2.25.165736"
"knx-frontend==2026.3.2.183756"
],
"single_config_entry": true
}

View File

@@ -7,5 +7,5 @@
"documentation": "https://www.home-assistant.io/integrations/local_calendar",
"iot_class": "local_polling",
"loggers": ["ical"],
"requirements": ["ical==13.2.0"]
"requirements": ["ical==13.2.2"]
}

View File

@@ -5,5 +5,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/local_todo",
"iot_class": "local_polling",
"requirements": ["ical==13.2.0"]
"requirements": ["ical==13.2.2"]
}

View File

@@ -4,6 +4,7 @@ from __future__ import annotations
from dataclasses import dataclass
from enum import IntEnum
import logging
from typing import TYPE_CHECKING, Any
from chip.clusters import Objects as clusters
@@ -26,6 +27,8 @@ from .entity import MatterEntity, MatterEntityDescription
from .helpers import get_matter
from .models import MatterDiscoverySchema
_LOGGER = logging.getLogger(__name__)
class OperationalState(IntEnum):
"""Operational State of the vacuum cleaner.
@@ -254,9 +257,18 @@ class MatterVacuum(MatterEntity, StateVacuumEntity):
VacuumEntityFeature.CLEAN_AREA in self.supported_features
and self.registry_entry is not None
and (last_seen_segments := self.last_seen_segments) is not None
and self._current_segments != {s.id: s for s in last_seen_segments}
# Ignore empty segments; some devices transiently
# report an empty list before sending the real one.
and (current_segments := self._current_segments)
):
self.async_create_segments_issue()
last_seen_by_id = {s.id: s for s in last_seen_segments}
if current_segments != last_seen_by_id:
_LOGGER.debug(
"Vacuum segments changed: last_seen=%s, current=%s",
last_seen_by_id,
current_segments,
)
self.async_create_segments_issue()
@callback
def _calculate_features(self) -> None:

View File

@@ -188,6 +188,7 @@ class ProgramPhaseTumbleDryer(MieleEnum, missing_to_none=True):
finished = 522, 11012
extra_dry = 523
hand_iron = 524
hygiene_drying = 525
moisten = 526
thermo_spin = 527
timed_drying = 528
@@ -617,11 +618,11 @@ class OvenProgramId(MieleEnum, missing_to_none=True):
evaporate_water = 327
shabbat_program = 335
yom_tov = 336
drying = 357
drying = 357, 2028
heat_crockery = 358
prove_dough = 359
prove_dough = 359, 2023
low_temperature_cooking = 360
steam_cooking = 361
steam_cooking = 8, 361
keeping_warm = 362
apple_sponge = 364
apple_pie = 365
@@ -668,9 +669,9 @@ class OvenProgramId(MieleEnum, missing_to_none=True):
saddle_of_roebuck = 456
salmon_fillet = 461
potato_cheese_gratin = 464
trout = 486
carp = 491
salmon_trout = 492
trout = 486, 2224
carp = 491, 2233
salmon_trout = 492, 2241
springform_tin_15cm = 496
springform_tin_20cm = 497
springform_tin_25cm = 498
@@ -736,137 +737,15 @@ class OvenProgramId(MieleEnum, missing_to_none=True):
pork_belly = 701
pikeperch_fillet_with_vegetables = 702
steam_bake = 99001
class DishWarmerProgramId(MieleEnum, missing_to_none=True):
"""Program Id codes for dish warmers."""
no_program = 0, -1
warm_cups_glasses = 1
warm_dishes_plates = 2
keep_warm = 3
slow_roasting = 4
class RobotVacuumCleanerProgramId(MieleEnum, missing_to_none=True):
"""Program Id codes for robot vacuum cleaners."""
no_program = 0, -1
auto = 1
spot = 2
turbo = 3
silent = 4
class CoffeeSystemProgramId(MieleEnum, missing_to_none=True):
"""Program Id codes for coffee systems."""
no_program = 0, -1
check_appliance = 17004
# profile 1
ristretto = 24000, 24032, 24064, 24096, 24128
espresso = 24001, 24033, 24065, 24097, 24129
coffee = 24002, 24034, 24066, 24098, 24130
long_coffee = 24003, 24035, 24067, 24099, 24131
cappuccino = 24004, 24036, 24068, 24100, 24132
cappuccino_italiano = 24005, 24037, 24069, 24101, 24133
latte_macchiato = 24006, 24038, 24070, 24102, 24134
espresso_macchiato = 24007, 24039, 24071, 24135
cafe_au_lait = 24008, 24040, 24072, 24104, 24136
caffe_latte = 24009, 24041, 24073, 24105, 24137
flat_white = 24012, 24044, 24076, 24108, 24140
very_hot_water = 24013, 24045, 24077, 24109, 24141
hot_water = 24014, 24046, 24078, 24110, 24142
hot_milk = 24015, 24047, 24079, 24111, 24143
milk_foam = 24016, 24048, 24080, 24112, 24144
black_tea = 24017, 24049, 24081, 24113, 24145
herbal_tea = 24018, 24050, 24082, 24114, 24146
fruit_tea = 24019, 24051, 24083, 24115, 24147
green_tea = 24020, 24052, 24084, 24116, 24148
white_tea = 24021, 24053, 24085, 24117, 24149
japanese_tea = 24022, 29054, 24086, 24118, 24150
# special programs
coffee_pot = 24400
barista_assistant = 24407
# machine settings menu
appliance_settings = (
16016, # display brightness
16018, # volume
16019, # buttons volume
16020, # child lock
16021, # water hardness
16027, # welcome sound
16033, # connection status
16035, # remote control
16037, # remote update
24500, # total dispensed
24502, # lights appliance on
24503, # lights appliance off
24504, # turn off lights after
24506, # altitude
24513, # performance mode
24516, # turn off after
24537, # advanced mode
24542, # tea timer
24549, # total coffee dispensed
24550, # total tea dispensed
24551, # total ristretto
24552, # total cappuccino
24553, # total espresso
24554, # total coffee
24555, # total long coffee
24556, # total italian cappuccino
24557, # total latte macchiato
24558, # total caffe latte
24560, # total espresso macchiato
24562, # total flat white
24563, # total coffee with milk
24564, # total black tea
24565, # total herbal tea
24566, # total fruit tea
24567, # total green tea
24568, # total white tea
24569, # total japanese tea
24571, # total milk foam
24572, # total hot milk
24573, # total hot water
24574, # total very hot water
24575, # counter to descaling
24576, # counter to brewing unit degreasing
24800, # maintenance
24801, # profiles settings menu
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
milk_pipework_clean = 24789
class SteamOvenMicroProgramId(MieleEnum, missing_to_none=True):
"""Program Id codes for steam oven micro combo."""
no_program = 0, -1
steam_cooking = 8
microwave = 19
popcorn = 53
quick_mw = 54
sous_vide = 72
eco_steam_cooking = 75
rapid_steam_cooking = 77
descale = 326
menu_cooking = 330
reheating_with_steam = 2018
defrosting_with_steam = 2019
blanching = 2020
bottling = 2021
sterilize_crockery = 2022
prove_dough = 2023
soak = 2027
reheating_with_microwave = 2029
defrosting_with_microwave = 2030
@@ -1020,18 +899,15 @@ class SteamOvenMicroProgramId(MieleEnum, missing_to_none=True):
gilt_head_bream_fillet = 2220
codfish_piece = 2221, 2232
codfish_fillet = 2222, 2231
trout = 2224
pike_fillet = 2225
pike_piece = 2226
halibut_fillet_2_cm = 2227
halibut_fillet_3_cm = 2230
carp = 2233
salmon_fillet_2_cm = 2234
salmon_fillet_3_cm = 2235
salmon_steak_2_cm = 2238
salmon_steak_3_cm = 2239
salmon_piece = 2240
salmon_trout = 2241
iridescent_shark_fillet = 2244
red_snapper_fillet_2_cm = 2245
red_snapper_fillet_3_cm = 2248
@@ -1268,6 +1144,116 @@ class SteamOvenMicroProgramId(MieleEnum, missing_to_none=True):
round_grain_rice_general_rapid_steam_cooking = 3411
class DishWarmerProgramId(MieleEnum, missing_to_none=True):
"""Program Id codes for dish warmers."""
no_program = 0, -1
warm_cups_glasses = 1
warm_dishes_plates = 2
keep_warm = 3
slow_roasting = 4
class RobotVacuumCleanerProgramId(MieleEnum, missing_to_none=True):
"""Program Id codes for robot vacuum cleaners."""
no_program = 0, -1
auto = 1
spot = 2
turbo = 3
silent = 4
class CoffeeSystemProgramId(MieleEnum, missing_to_none=True):
"""Program Id codes for coffee systems."""
no_program = 0, -1
check_appliance = 17004
# profile 1
ristretto = 24000, 24032, 24064, 24096, 24128
espresso = 24001, 24033, 24065, 24097, 24129
coffee = 24002, 24034, 24066, 24098, 24130
long_coffee = 24003, 24035, 24067, 24099, 24131
cappuccino = 24004, 24036, 24068, 24100, 24132
cappuccino_italiano = 24005, 24037, 24069, 24101, 24133
latte_macchiato = 24006, 24038, 24070, 24102, 24134
espresso_macchiato = 24007, 24039, 24071, 24135
cafe_au_lait = 24008, 24040, 24072, 24104, 24136
caffe_latte = 24009, 24041, 24073, 24105, 24137
flat_white = 24012, 24044, 24076, 24108, 24140
very_hot_water = 24013, 24045, 24077, 24109, 24141
hot_water = 24014, 24046, 24078, 24110, 24142
hot_milk = 24015, 24047, 24079, 24111, 24143
milk_foam = 24016, 24048, 24080, 24112, 24144
black_tea = 24017, 24049, 24081, 24113, 24145
herbal_tea = 24018, 24050, 24082, 24114, 24146
fruit_tea = 24019, 24051, 24083, 24115, 24147
green_tea = 24020, 24052, 24084, 24116, 24148
white_tea = 24021, 24053, 24085, 24117, 24149
japanese_tea = 24022, 29054, 24086, 24118, 24150
# special programs
coffee_pot = 24400
barista_assistant = 24407
# machine settings menu
appliance_settings = (
16016, # display brightness
16018, # volume
16019, # buttons volume
16020, # child lock
16021, # water hardness
16027, # welcome sound
16033, # connection status
16035, # remote control
16037, # remote update
24500, # total dispensed
24502, # lights appliance on
24503, # lights appliance off
24504, # turn off lights after
24506, # altitude
24513, # performance mode
24516, # turn off after
24537, # advanced mode
24542, # tea timer
24549, # total coffee dispensed
24550, # total tea dispensed
24551, # total ristretto
24552, # total cappuccino
24553, # total espresso
24554, # total coffee
24555, # total long coffee
24556, # total italian cappuccino
24557, # total latte macchiato
24558, # total caffe latte
24560, # total espresso macchiato
24562, # total flat white
24563, # total coffee with milk
24564, # total black tea
24565, # total herbal tea
24566, # total fruit tea
24567, # total green tea
24568, # total white tea
24569, # total japanese tea
24571, # total milk foam
24572, # total hot milk
24573, # total hot water
24574, # total very hot water
24575, # counter to descaling
24576, # counter to brewing unit degreasing
24800, # maintenance
24801, # profiles settings menu
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
milk_pipework_clean = 24789
PROGRAM_IDS: dict[int, type[MieleEnum]] = {
MieleAppliance.WASHING_MACHINE: WashingMachineProgramId,
MieleAppliance.TUMBLE_DRYER: TumbleDryerProgramId,
@@ -1278,7 +1264,7 @@ PROGRAM_IDS: dict[int, type[MieleEnum]] = {
MieleAppliance.STEAM_OVEN_MK2: OvenProgramId,
MieleAppliance.STEAM_OVEN: OvenProgramId,
MieleAppliance.STEAM_OVEN_COMBI: OvenProgramId,
MieleAppliance.STEAM_OVEN_MICRO: SteamOvenMicroProgramId,
MieleAppliance.STEAM_OVEN_MICRO: OvenProgramId,
MieleAppliance.WASHER_DRYER: WashingMachineProgramId,
MieleAppliance.ROBOT_VACUUM_CLEANER: RobotVacuumCleanerProgramId,
MieleAppliance.COFFEE_SYSTEM: CoffeeSystemProgramId,

View File

@@ -474,6 +474,7 @@
"drain_spin": "Drain/spin",
"drop_cookies_1_tray": "Drop cookies (1 tray)",
"drop_cookies_2_trays": "Drop cookies (2 trays)",
"drying": "Drying",
"duck": "Duck",
"dutch_hash": "Dutch hash",
"easy_care": "Easy care",
@@ -1005,6 +1006,7 @@
"heating_up_phase": "Heating up phase",
"hot_milk": "Hot milk",
"hygiene": "Hygiene",
"hygiene_drying": "Hygiene drying",
"interim_rinse": "Interim rinse",
"keep_warm": "Keep warm",
"keeping_warm": "Keeping warm",

View File

@@ -120,6 +120,7 @@ class MobileAppNotificationService(BaseNotificationService):
local_push_channels = self.hass.data[DOMAIN][DATA_PUSH_CHANNEL]
failed_targets = []
for target in targets:
registration = self.hass.data[DOMAIN][DATA_CONFIG_ENTRIES][target].data
@@ -134,12 +135,16 @@ class MobileAppNotificationService(BaseNotificationService):
# Test if local push only.
if ATTR_PUSH_URL not in registration[ATTR_APP_DATA]:
raise HomeAssistantError(
"Device not connected to local push notifications"
)
failed_targets.append(target)
continue
await self._async_send_remote_message_target(target, registration, data)
if failed_targets:
raise HomeAssistantError(
f"Device(s) with webhook id(s) {', '.join(failed_targets)} not connected to local push notifications"
)
async def _async_send_remote_message_target(self, target, registration, data):
"""Send a message to a target."""
app_data = registration[ATTR_APP_DATA]

View File

@@ -163,8 +163,6 @@ class MqttDeviceTracker(MqttEntity, TrackerEntity):
latitude: float | None
longitude: float | None
gps_accuracy: float
# Reset manually set location to allow automatic zone detection
self._attr_location_name = None
if isinstance(
latitude := extra_state_attributes.get(ATTR_LATITUDE), (int, float)
) and isinstance(

View File

@@ -24,7 +24,7 @@ SUBENTRY_TYPE_ZONE = "zone"
# Defaults
DEFAULT_PORT = 4999
DEFAULT_SCAN_INTERVAL = timedelta(minutes=1)
DEFAULT_SCAN_INTERVAL = timedelta(seconds=5)
DEFAULT_INFER_ARMING_STATE = False
DEFAULT_ZONE_TYPE = BinarySensorDeviceClass.MOTION

View File

@@ -19,6 +19,5 @@ async def async_get_config_entry_diagnostics(
return {
"device_info": client.device_info,
"vehicles": client.vehicles,
"ct_connected": client.ct_connected,
"cap_available": client.cap_available,
}

View File

@@ -7,5 +7,5 @@
"integration_type": "device",
"iot_class": "cloud_polling",
"quality_scale": "platinum",
"requirements": ["ohme==1.6.0"]
"requirements": ["ohme==1.7.0"]
}

View File

@@ -10,5 +10,5 @@
"iot_class": "cloud_polling",
"loggers": ["onedrive_personal_sdk"],
"quality_scale": "platinum",
"requirements": ["onedrive-personal-sdk==0.1.4"]
"requirements": ["onedrive-personal-sdk==0.1.7"]
}

View File

@@ -10,5 +10,5 @@
"iot_class": "cloud_polling",
"loggers": ["onedrive_personal_sdk"],
"quality_scale": "platinum",
"requirements": ["onedrive-personal-sdk==0.1.4"]
"requirements": ["onedrive-personal-sdk==0.1.7"]
}

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.8.0"]
"requirements": ["python-otbr-api==2.9.0"]
}

View File

@@ -159,8 +159,12 @@ class PortainerConfigFlow(ConfigFlow, domain=DOMAIN):
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
await self.async_set_unique_id(user_input[CONF_API_TOKEN])
self._abort_if_unique_id_configured()
# Logic that can be reverted back once the new unique ID is in
existing_entry = await self.async_set_unique_id(
user_input[CONF_API_TOKEN]
)
if existing_entry and existing_entry.entry_id != reconf_entry.entry_id:
return self.async_abort(reason="already_configured")
return self.async_update_reload_and_abort(
reconf_entry,
data_updates={

View File

@@ -7,5 +7,5 @@
"integration_type": "service",
"iot_class": "local_polling",
"quality_scale": "bronze",
"requirements": ["pyportainer==1.0.28"]
"requirements": ["pyportainer==1.0.33"]
}

View File

@@ -19,12 +19,13 @@ from homeassistant.components.button import (
)
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN
from .coordinator import ProxmoxConfigEntry, ProxmoxCoordinator, ProxmoxNodeData
from .entity import ProxmoxContainerEntity, ProxmoxNodeEntity, ProxmoxVMEntity
from .helpers import is_granted
@dataclass(frozen=True, kw_only=True)
@@ -276,6 +277,11 @@ class ProxmoxNodeButtonEntity(ProxmoxNodeEntity, ProxmoxBaseButton):
async def _async_press_call(self) -> None:
"""Execute the node button action via executor."""
if not is_granted(self.coordinator.permissions, p_type="nodes"):
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="no_permission_node_power",
)
await self.hass.async_add_executor_job(
self.entity_description.press_action,
self.coordinator,
@@ -303,6 +309,11 @@ class ProxmoxVMButtonEntity(ProxmoxVMEntity, ProxmoxBaseButton):
async def _async_press_call(self) -> None:
"""Execute the VM button action via executor."""
if not is_granted(self.coordinator.permissions, p_type="vms"):
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="no_permission_vm_lxc_power",
)
await self.hass.async_add_executor_job(
self.entity_description.press_action,
self.coordinator,
@@ -331,6 +342,12 @@ class ProxmoxContainerButtonEntity(ProxmoxContainerEntity, ProxmoxBaseButton):
async def _async_press_call(self) -> None:
"""Execute the container button action via executor."""
# Container power actions fall under vms
if not is_granted(self.coordinator.permissions, p_type="vms"):
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="no_permission_vm_lxc_power",
)
await self.hass.async_add_executor_job(
self.entity_description.press_action,
self.coordinator,

View File

@@ -17,3 +17,5 @@ DEFAULT_VERIFY_SSL = True
TYPE_VM = 0
TYPE_CONTAINER = 1
UPDATE_INTERVAL = 60
PERM_POWER = "VM.PowerMgmt"

View File

@@ -70,6 +70,7 @@ class ProxmoxCoordinator(DataUpdateCoordinator[dict[str, ProxmoxNodeData]]):
self.known_nodes: set[str] = set()
self.known_vms: set[tuple[str, int]] = set()
self.known_containers: set[tuple[str, int]] = set()
self.permissions: dict[str, dict[str, int]] = {}
self.new_nodes_callbacks: list[Callable[[list[ProxmoxNodeData]], None]] = []
self.new_vms_callbacks: list[
@@ -101,11 +102,21 @@ class ProxmoxCoordinator(DataUpdateCoordinator[dict[str, ProxmoxNodeData]]):
translation_key="timeout_connect",
translation_placeholders={"error": repr(err)},
) from err
except ResourceException as err:
except ProxmoxServerError as err:
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="api_error_details",
translation_placeholders={"error": repr(err)},
) from err
except ProxmoxPermissionsError as err:
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN,
translation_key="permissions_error",
) from err
except ProxmoxNodesNotFoundError as err:
raise ConfigEntryError(
translation_domain=DOMAIN,
translation_key="no_nodes_found",
translation_placeholders={"error": repr(err)},
) from err
except requests.exceptions.ConnectionError as err:
raise ConfigEntryError(
@@ -143,7 +154,6 @@ class ProxmoxCoordinator(DataUpdateCoordinator[dict[str, ProxmoxNodeData]]):
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="no_nodes_found",
translation_placeholders={"error": repr(err)},
) from err
except requests.exceptions.ConnectionError as err:
raise UpdateFailed(
@@ -180,7 +190,19 @@ class ProxmoxCoordinator(DataUpdateCoordinator[dict[str, ProxmoxNodeData]]):
password=self.config_entry.data[CONF_PASSWORD],
verify_ssl=self.config_entry.data.get(CONF_VERIFY_SSL, DEFAULT_VERIFY_SSL),
)
self.proxmox.nodes.get()
try:
self.permissions = self.proxmox.access.permissions.get()
except ResourceException as err:
if 400 <= err.status_code < 500:
raise ProxmoxPermissionsError from err
raise ProxmoxServerError from err
try:
self.proxmox.nodes.get()
except ResourceException as err:
if 400 <= err.status_code < 500:
raise ProxmoxNodesNotFoundError from err
raise ProxmoxServerError from err
def _fetch_all_nodes(
self,
@@ -230,3 +252,19 @@ class ProxmoxCoordinator(DataUpdateCoordinator[dict[str, ProxmoxNodeData]]):
if new_containers:
_LOGGER.debug("New containers found: %s", new_containers)
self.known_containers.update(new_containers)
class ProxmoxSetupError(Exception):
"""Base exception for Proxmox setup issues."""
class ProxmoxNodesNotFoundError(ProxmoxSetupError):
"""Raised when the API works but no nodes are visible."""
class ProxmoxPermissionsError(ProxmoxSetupError):
"""Raised when failing to retrieve permissions."""
class ProxmoxServerError(ProxmoxSetupError):
"""Raised when the Proxmox server returns an error."""

View File

@@ -0,0 +1,13 @@
"""Helpers for Proxmox VE."""
from .const import PERM_POWER
def is_granted(
permissions: dict[str, dict[str, int]],
p_type: str = "vms",
permission: str = PERM_POWER,
) -> bool:
"""Validate user permissions for the given type and permission."""
path = f"/{p_type}"
return permissions.get(path, {}).get(permission) == 1

View File

@@ -175,6 +175,9 @@
}
},
"exceptions": {
"api_error_details": {
"message": "An error occurred while communicating with the Proxmox VE instance: {error}"
},
"api_error_no_details": {
"message": "An error occurred while communicating with the Proxmox VE instance."
},
@@ -193,6 +196,15 @@
"no_nodes_found": {
"message": "No active nodes were found on the Proxmox VE server."
},
"no_permission_node_power": {
"message": "The configured Proxmox VE user does not have permission to manage the power state of nodes. Please grant the user the 'VM.PowerMgmt' permission and try again."
},
"no_permission_vm_lxc_power": {
"message": "The configured Proxmox VE user does not have permission to manage the power state of VMs and containers. Please grant the user the 'VM.PowerMgmt' permission and try again."
},
"permissions_error": {
"message": "Failed to retrieve Proxmox VE permissions. Please check your credentials and try again."
},
"ssl_error": {
"message": "An SSL error occurred: {error}"
},

View File

@@ -2,11 +2,12 @@
from __future__ import annotations
import asyncio
import logging
from typing import Any
import aiohttp
from pyrainbird.async_client import AsyncRainbirdController, CreateController
from pyrainbird.async_client import AsyncRainbirdController, create_controller
from pyrainbird.exceptions import RainbirdApiException, RainbirdAuthException
from homeassistant.const import (
@@ -26,7 +27,7 @@ from homeassistant.helpers import (
from homeassistant.helpers.device_registry import format_mac
from homeassistant.helpers.typing import ConfigType
from .const import CONF_SERIAL_NUMBER, DOMAIN
from .const import CONF_SERIAL_NUMBER, DOMAIN, TIMEOUT_SECONDS
from .coordinator import (
RainbirdScheduleUpdateCoordinator,
RainbirdUpdateCoordinator,
@@ -77,11 +78,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: RainbirdConfigEntry) ->
clientsession = async_create_clientsession()
_async_register_clientsession_shutdown(hass, entry, clientsession)
controller = CreateController(
clientsession,
entry.data[CONF_HOST],
entry.data[CONF_PASSWORD],
)
try:
async with asyncio.timeout(TIMEOUT_SECONDS):
controller = await create_controller(
clientsession,
entry.data[CONF_HOST],
entry.data[CONF_PASSWORD],
)
except TimeoutError as err:
raise ConfigEntryNotReady from err
except RainbirdAuthException as err:
raise ConfigEntryAuthFailed from err
except RainbirdApiException as err:
raise ConfigEntryNotReady from err
if not (await _async_fix_unique_id(hass, controller, entry)):
return False

View File

@@ -7,7 +7,7 @@ from collections.abc import Mapping
import logging
from typing import Any
from pyrainbird.async_client import CreateController
from pyrainbird.async_client import create_controller
from pyrainbird.data import WifiParams
from pyrainbird.exceptions import RainbirdApiException, RainbirdAuthException
import voluptuous as vol
@@ -137,9 +137,9 @@ class RainbirdConfigFlowHandler(ConfigFlow, domain=DOMAIN):
Raises a ConfigFlowError on failure.
"""
clientsession = async_create_clientsession()
controller = CreateController(clientsession, host, password)
try:
async with asyncio.timeout(TIMEOUT_SECONDS):
controller = await create_controller(clientsession, host, password)
return await asyncio.gather(
controller.get_serial_number(),
controller.get_wifi_params(),

View File

@@ -3,7 +3,7 @@
"name": "Recovery Mode",
"codeowners": ["@home-assistant/core"],
"config_flow": false,
"dependencies": ["frontend", "persistent_notification", "cloud"],
"dependencies": ["persistent_notification"],
"documentation": "https://www.home-assistant.io/integrations/recovery_mode",
"integration_type": "system",
"quality_scale": "internal"

View File

@@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["ical"],
"quality_scale": "silver",
"requirements": ["ical==13.2.0"]
"requirements": ["ical==13.2.2"]
}

View File

@@ -543,7 +543,20 @@ def migrate_entity_ids(
entity.unique_id,
new_id,
)
entity_reg.async_update_entity(entity.entity_id, new_unique_id=new_id)
existing_entity = entity_reg.async_get_entity_id(
entity.domain, entity.platform, new_id
)
if existing_entity is None:
entity_reg.async_update_entity(entity.entity_id, new_unique_id=new_id)
else:
_LOGGER.warning(
"Reolink entity with unique_id %s already exists, "
"removing entity with unique_id %s",
new_id,
entity.unique_id,
)
entity_reg.async_remove(entity.entity_id)
continue
if entity.device_id in ch_device_ids:
ch = ch_device_ids[entity.device_id]
@@ -573,7 +586,7 @@ def migrate_entity_ids(
else:
_LOGGER.warning(
"Reolink entity with unique_id %s already exists, "
"removing device with unique_id %s",
"removing entity with unique_id %s",
new_id,
entity.unique_id,
)

View File

@@ -159,6 +159,15 @@ class ReolinkFlowHandler(ConfigFlow, domain=DOMAIN):
"""Handle discovery via dhcp."""
mac_address = format_mac(discovery_info.macaddress)
existing_entry = await self.async_set_unique_id(mac_address)
if existing_entry and CONF_HOST not in existing_entry.data:
_LOGGER.debug(
"Reolink DHCP discovered device with MAC '%s' and IP '%s', "
"but existing config entry does not have host, ignoring",
mac_address,
discovery_info.ip,
)
raise AbortFlow("already_configured")
if (
existing_entry
and CONF_PASSWORD in existing_entry.data

View File

@@ -20,5 +20,5 @@
"iot_class": "local_push",
"loggers": ["reolink_aio"],
"quality_scale": "platinum",
"requirements": ["reolink-aio==0.19.0"]
"requirements": ["reolink-aio==0.19.1"]
}

View File

@@ -31,5 +31,5 @@
"iot_class": "cloud_polling",
"loggers": ["ring_doorbell"],
"quality_scale": "bronze",
"requirements": ["ring-doorbell==0.9.13"]
"requirements": ["ring-doorbell==0.9.14"]
}

View File

@@ -188,7 +188,9 @@ class RoborockFlowHandler(ConfigFlow, domain=DOMAIN):
self._username = entry_data[CONF_USERNAME]
assert self._username
self._client = RoborockApiClient(
self._username, session=async_get_clientsession(self.hass)
self._username,
base_url=entry_data[CONF_BASE_URL],
session=async_get_clientsession(self.hass),
)
return await self.async_step_reauth_confirm()

View File

@@ -34,5 +34,5 @@
"iot_class": "cloud_push",
"loggers": ["pysmartthings"],
"quality_scale": "bronze",
"requirements": ["pysmartthings==3.6.0"]
"requirements": ["pysmartthings==3.7.0"]
}

View File

@@ -74,7 +74,7 @@ async def async_setup_entry(
radios = coordinator.data.info.radios
async_add_entities(SmButton(coordinator, button) for button in BUTTONS)
entity_created = [False, False]
entity_created = [False] * len(radios)
@callback
def _check_router(startup: bool = False) -> None:

View File

@@ -12,7 +12,7 @@
"integration_type": "device",
"iot_class": "local_push",
"quality_scale": "silver",
"requirements": ["pysmlight==0.2.14"],
"requirements": ["pysmlight==0.2.16"],
"zeroconf": [
{
"type": "_slzb-06._tcp.local."

View File

@@ -131,7 +131,7 @@ class SnapcastClientDevice(SnapcastCoordinatorEntity, MediaPlayerEntity):
return f"{CLIENT_PREFIX}{host}_{id}"
@property
def _current_group(self) -> Snapgroup:
def _current_group(self) -> Snapgroup | None:
"""Return the group the client is associated with."""
return self._device.group
@@ -158,12 +158,17 @@ class SnapcastClientDevice(SnapcastCoordinatorEntity, MediaPlayerEntity):
def state(self) -> MediaPlayerState | None:
"""Return the state of the player."""
if self._device.connected:
if self.is_volume_muted or self._current_group.muted:
if (
self.is_volume_muted
or self._current_group is None
or self._current_group.muted
):
return MediaPlayerState.IDLE
try:
return STREAM_STATUS.get(self._current_group.stream_status)
except KeyError:
pass
return MediaPlayerState.OFF
@property
@@ -182,15 +187,31 @@ class SnapcastClientDevice(SnapcastCoordinatorEntity, MediaPlayerEntity):
@property
def source(self) -> str | None:
"""Return the current input source."""
if self._current_group is None:
return None
return self._current_group.stream
@property
def source_list(self) -> list[str]:
"""List of available input sources."""
if self._current_group is None:
return []
return list(self._current_group.streams_by_name().keys())
async def async_select_source(self, source: str) -> None:
"""Set input source."""
if self._current_group is None:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="select_source_no_group",
translation_placeholders={
"entity_id": self.entity_id,
"source": source,
},
)
streams = self._current_group.streams_by_name()
if source in streams:
await self._current_group.set_stream(streams[source].identifier)
@@ -233,6 +254,9 @@ class SnapcastClientDevice(SnapcastCoordinatorEntity, MediaPlayerEntity):
@property
def group_members(self) -> list[str] | None:
"""List of player entities which are currently grouped together for synchronous playback."""
if self._current_group is None:
return None
entity_registry = er.async_get(self.hass)
return [
entity_id
@@ -248,6 +272,15 @@ class SnapcastClientDevice(SnapcastCoordinatorEntity, MediaPlayerEntity):
async def async_join_players(self, group_members: list[str]) -> None:
"""Add `group_members` to this client's current group."""
if self._current_group is None:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="join_players_no_group",
translation_placeholders={
"entity_id": self.entity_id,
},
)
# Get the client entity for each group member excluding self
entity_registry = er.async_get(self.hass)
clients = [
@@ -271,13 +304,25 @@ class SnapcastClientDevice(SnapcastCoordinatorEntity, MediaPlayerEntity):
self.async_write_ha_state()
async def async_unjoin_player(self) -> None:
"""Remove this client from it's current group."""
"""Remove this client from its current group."""
if self._current_group is None:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="unjoin_no_group",
translation_placeholders={
"entity_id": self.entity_id,
},
)
await self._current_group.remove_client(self._device.identifier)
self.async_write_ha_state()
@property
def metadata(self) -> Mapping[str, Any]:
"""Get metadata from the current stream."""
if self._current_group is None:
return {}
try:
if metadata := self.coordinator.server.stream(
self._current_group.stream
@@ -341,6 +386,9 @@ class SnapcastClientDevice(SnapcastCoordinatorEntity, MediaPlayerEntity):
@property
def media_position(self) -> int | None:
"""Position of current playing media in seconds."""
if self._current_group is None:
return None
try:
# Position is part of properties object, not metadata object
if properties := self.coordinator.server.stream(

View File

@@ -21,6 +21,17 @@
}
}
},
"exceptions": {
"join_players_no_group": {
"message": "Client {entity_id} has no group. Unable to join players."
},
"select_source_no_group": {
"message": "Client {entity_id} has no group. Unable to select source {source}."
},
"unjoin_no_group": {
"message": "Client {entity_id} has no group. Unable to unjoin player."
}
},
"services": {
"restore": {
"description": "Restores a previously taken snapshot of a media player.",

View File

@@ -118,7 +118,6 @@ class BrowsableMedia(StrEnum):
CURRENT_USER_RECENTLY_PLAYED = "current_user_recently_played"
CURRENT_USER_TOP_ARTISTS = "current_user_top_artists"
CURRENT_USER_TOP_TRACKS = "current_user_top_tracks"
NEW_RELEASES = "new_releases"
LIBRARY_MAP = {
@@ -130,7 +129,6 @@ LIBRARY_MAP = {
BrowsableMedia.CURRENT_USER_RECENTLY_PLAYED.value: "Recently played",
BrowsableMedia.CURRENT_USER_TOP_ARTISTS.value: "Top Artists",
BrowsableMedia.CURRENT_USER_TOP_TRACKS.value: "Top Tracks",
BrowsableMedia.NEW_RELEASES.value: "New Releases",
}
CONTENT_TYPE_MEDIA_CLASS: dict[str, Any] = {
@@ -166,10 +164,6 @@ CONTENT_TYPE_MEDIA_CLASS: dict[str, Any] = {
"parent": MediaClass.DIRECTORY,
"children": MediaClass.TRACK,
},
BrowsableMedia.NEW_RELEASES.value: {
"parent": MediaClass.DIRECTORY,
"children": MediaClass.ALBUM,
},
MediaType.PLAYLIST: {
"parent": MediaClass.PLAYLIST,
"children": MediaClass.TRACK,
@@ -356,14 +350,11 @@ async def build_item_response( # noqa: C901
elif media_content_type == BrowsableMedia.CURRENT_USER_TOP_TRACKS:
if top_tracks := await spotify.get_top_tracks():
items = [_get_track_item_payload(track) for track in top_tracks]
elif media_content_type == BrowsableMedia.NEW_RELEASES:
if new_releases := await spotify.get_new_releases():
items = [_get_album_item_payload(album) for album in new_releases]
elif media_content_type == MediaType.PLAYLIST:
if playlist := await spotify.get_playlist(media_content_id):
title = playlist.name
image = playlist.images[0].url if playlist.images else None
for playlist_item in playlist.tracks.items:
for playlist_item in playlist.items.items:
if playlist_item.track.type is ItemType.TRACK:
if TYPE_CHECKING:
assert isinstance(playlist_item.track, Track)

View File

@@ -6,7 +6,7 @@ from collections.abc import Mapping
import logging
from typing import Any
from spotifyaio import SpotifyClient
from spotifyaio import SpotifyClient, SpotifyForbiddenError
from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlowResult
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_NAME, CONF_TOKEN
@@ -41,6 +41,9 @@ class SpotifyFlowHandler(
try:
current_user = await spotify.get_current_user()
except SpotifyForbiddenError:
self.logger.exception("User is not subscribed to Spotify")
return self.async_abort(reason="user_not_premium")
except Exception:
self.logger.exception("Error while connecting to Spotify")
return self.async_abort(reason="connection_error")

View File

@@ -11,12 +11,15 @@ from spotifyaio import (
Playlist,
SpotifyClient,
SpotifyConnectionError,
SpotifyForbiddenError,
SpotifyNotFoundError,
UserProfile,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryError
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from homeassistant.util import dt as dt_util
@@ -33,6 +36,11 @@ type SpotifyConfigEntry = ConfigEntry[SpotifyData]
UPDATE_INTERVAL = timedelta(seconds=30)
FREE_API_BLOGPOST = (
"https://developer.spotify.com/blog/"
"2026-02-06-update-on-developer-access-and-platform-security"
)
@dataclass
class SpotifyCoordinatorData:
@@ -78,6 +86,19 @@ class SpotifyCoordinator(DataUpdateCoordinator[SpotifyCoordinatorData]):
"""Set up the coordinator."""
try:
self.current_user = await self.client.get_current_user()
except SpotifyForbiddenError as err:
async_create_issue(
self.hass,
DOMAIN,
f"user_not_premium_{self.config_entry.unique_id}",
is_fixable=False,
issue_domain=DOMAIN,
severity=IssueSeverity.ERROR,
translation_key="user_not_premium",
translation_placeholders={"entry_title": self.config_entry.title},
learn_more_url=FREE_API_BLOGPOST,
)
raise ConfigEntryError("User is not subscribed to Spotify") from err
except SpotifyConnectionError as err:
raise UpdateFailed("Error communicating with Spotify API") from err

View File

@@ -8,5 +8,5 @@
"integration_type": "service",
"iot_class": "cloud_polling",
"loggers": ["spotifyaio"],
"requirements": ["spotifyaio==1.0.0"]
"requirements": ["spotifyaio==2.0.2"]
}

View File

@@ -14,10 +14,10 @@ from spotifyaio import (
Item,
ItemType,
PlaybackState,
ProductType,
RepeatMode as SpotifyRepeatMode,
Track,
)
from spotifyaio.models import ProductType
from yarl import URL
from homeassistant.components.media_player import (
@@ -222,7 +222,7 @@ class SpotifyMediaPlayer(SpotifyEntity, MediaPlayerEntity):
if item.type == ItemType.EPISODE:
if TYPE_CHECKING:
assert isinstance(item, Episode)
return item.show.publisher
return item.show.name
if TYPE_CHECKING:
assert isinstance(item, Track)
@@ -230,12 +230,10 @@ class SpotifyMediaPlayer(SpotifyEntity, MediaPlayerEntity):
@property
@ensure_item
def media_album_name(self, item: Item) -> str: # noqa: PLR0206
def media_album_name(self, item: Item) -> str | None: # noqa: PLR0206
"""Return the media album."""
if item.type == ItemType.EPISODE:
if TYPE_CHECKING:
assert isinstance(item, Episode)
return item.show.name
return None
if TYPE_CHECKING:
assert isinstance(item, Track)

View File

@@ -12,7 +12,8 @@
"oauth_timeout": "[%key:common::config_flow::abort::oauth2_timeout%]",
"oauth_unauthorized": "[%key:common::config_flow::abort::oauth2_unauthorized%]",
"reauth_account_mismatch": "The Spotify account authenticated with does not match the account that needed re-authentication.",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
"user_not_premium": "The Spotify API has been changed and Developer applications created with a free account can no longer access the API. To continue using the Spotify integration, you should use an Spotify Developer application created with a Spotify Premium account, or upgrade to Spotify Premium."
},
"create_entry": {
"default": "Successfully authenticated with Spotify."
@@ -41,6 +42,12 @@
"message": "[%key:common::exceptions::oauth2_implementation_unavailable::message%]"
}
},
"issues": {
"user_not_premium": {
"description": "[%key:component::spotify::config::abort::user_not_premium%]",
"title": "Spotify integration requires a Spotify Premium account"
}
},
"system_health": {
"info": {
"api_endpoint_reachable": "Spotify API endpoint reachable"

View File

@@ -8,6 +8,6 @@
"iot_class": "local_push",
"loggers": ["aioswitcher"],
"quality_scale": "silver",
"requirements": ["aioswitcher==6.1.0"],
"requirements": ["aioswitcher==6.1.1"],
"single_config_entry": true
}

View File

@@ -62,8 +62,8 @@ _LOGGER = logging.getLogger(__name__)
DESCRIPTION_PLACEHOLDERS: dict[str, str] = {
"botfather_username": "@BotFather",
"botfather_url": "https://t.me/botfather",
"getidsbot_username": "@GetIDs Bot",
"getidsbot_url": "https://t.me/getidsbot",
"id_bot_username": "@id_bot",
"id_bot_url": "https://t.me/id_bot",
"socks_url": "socks5://username:password@proxy_ip:proxy_port",
# used in advanced settings section
"default_api_endpoint": DEFAULT_API_ENDPOINT,
@@ -611,10 +611,15 @@ class AllowedChatIdsSubEntryFlowHandler(ConfigSubentryFlow):
errors["base"] = "chat_not_found"
service: TelegramNotificationService = self._get_entry().runtime_data
description_placeholders = DESCRIPTION_PLACEHOLDERS.copy()
description_placeholders["bot_username"] = f"@{service.bot.username}"
description_placeholders["bot_url"] = f"https://t.me/{service.bot.username}"
return self.async_show_form(
step_id="user",
data_schema=vol.Schema({vol.Required(CONF_CHAT_ID): vol.Coerce(int)}),
description_placeholders=DESCRIPTION_PLACEHOLDERS,
description_placeholders=description_placeholders,
errors=errors,
)

View File

@@ -105,7 +105,7 @@
"data_description": {
"chat_id": "ID representing the user or group chat to which messages can be sent."
},
"description": "To get your chat ID, follow these steps:\n\n1. Open Telegram and start a chat with [{getidsbot_username}]({getidsbot_url}).\n1. Send any message to the bot.\n1. Your chat ID is in the `id` field of the bot's response.",
"description": "Before you proceed, send any message to your bot: [{bot_username}]({bot_url}). This is required because Telegram prevents bots from initiating chats with users.\n\nThen follow these steps to get your chat ID:\n\n1. Open Telegram and start a chat with [{id_bot_username}]({id_bot_url}).\n1. Send any message to the bot.\n1. Your chat ID is in the `ID` field of the bot's response.",
"title": "Add chat"
}
}

View File

@@ -15,5 +15,5 @@
"integration_type": "device",
"iot_class": "local_polling",
"quality_scale": "silver",
"requirements": ["teltasync==0.1.3"]
"requirements": ["teltasync==0.2.0"]
}

View File

@@ -29,7 +29,7 @@ from homeassistant.core import (
)
from homeassistant.exceptions import TemplateError
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity import Entity, async_generate_entity_id
from homeassistant.helpers.event import (
TrackTemplate,
TrackTemplateResult,
@@ -264,16 +264,30 @@ class TemplateEntity(AbstractTemplateEntity):
return None
return cast(str, self._blueprint_inputs[CONF_USE_BLUEPRINT][CONF_PATH])
def _get_this_variable(self) -> TemplateStateFromEntityId:
"""Create a this variable for the entity."""
entity_id = self.entity_id
if self._preview_callback:
# During config flow, the registry entry and entity_id will be None. In this scenario,
# a temporary entity_id is created.
# During option flow, the preview entity_id will be None, however the registry entry
# will contain the target entity_id.
if self.registry_entry:
entity_id = self.registry_entry.entity_id
else:
entity_id = async_generate_entity_id(
self._entity_id_format, self._attr_name or "preview", hass=self.hass
)
return TemplateStateFromEntityId(self.hass, entity_id)
def _render_script_variables(self) -> dict[str, Any]:
"""Render configured variables."""
if isinstance(self._run_variables, dict):
return self._run_variables
return self._run_variables.async_render(
self.hass,
{
"this": TemplateStateFromEntityId(self.hass, self.entity_id),
},
self.hass, {"this": self._get_this_variable()}
)
def setup_state_template(
@@ -451,7 +465,7 @@ class TemplateEntity(AbstractTemplateEntity):
has_availability_template = False
variables = {
"this": TemplateStateFromEntityId(self.hass, self.entity_id),
"this": self._get_this_variable(),
**self._render_script_variables(),
}

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.8.0", "pyroute2==0.7.5"],
"requirements": ["python-otbr-api==2.9.0", "pyroute2==0.7.5"],
"single_config_entry": true,
"zeroconf": ["_meshcop._udp.local."]
}

View File

@@ -81,6 +81,7 @@ clean_area:
selector:
area:
multiple: true
reorder: true
send_command:
target:

View File

@@ -398,7 +398,7 @@ SENSOR_DESCRIPTIONS = {
Keys.WARNING: VictronBLESensorEntityDescription(
key=Keys.WARNING,
device_class=SensorDeviceClass.ENUM,
translation_key="alarm",
translation_key="warning",
options=ALARM_OPTIONS,
),
Keys.YIELD_TODAY: VictronBLESensorEntityDescription(

View File

@@ -248,7 +248,24 @@
"name": "[%key:component::victron_ble::common::starter_voltage%]"
},
"warning": {
"name": "Warning"
"name": "Warning",
"state": {
"bms_lockout": "[%key:component::victron_ble::entity::sensor::alarm::state::bms_lockout%]",
"dc_ripple": "[%key:component::victron_ble::entity::sensor::alarm::state::dc_ripple%]",
"high_starter_voltage": "[%key:component::victron_ble::entity::sensor::alarm::state::high_starter_voltage%]",
"high_temperature": "[%key:component::victron_ble::entity::sensor::alarm::state::high_temperature%]",
"high_v_ac_out": "[%key:component::victron_ble::entity::sensor::alarm::state::high_v_ac_out%]",
"high_voltage": "[%key:component::victron_ble::entity::sensor::alarm::state::high_voltage%]",
"low_soc": "[%key:component::victron_ble::entity::sensor::alarm::state::low_soc%]",
"low_starter_voltage": "[%key:component::victron_ble::entity::sensor::alarm::state::low_starter_voltage%]",
"low_temperature": "[%key:component::victron_ble::entity::sensor::alarm::state::low_temperature%]",
"low_v_ac_out": "[%key:component::victron_ble::entity::sensor::alarm::state::low_v_ac_out%]",
"low_voltage": "[%key:component::victron_ble::entity::sensor::alarm::state::low_voltage%]",
"mid_voltage": "[%key:component::victron_ble::common::midpoint_voltage%]",
"no_alarm": "[%key:component::victron_ble::entity::sensor::alarm::state::no_alarm%]",
"overload": "[%key:component::victron_ble::entity::sensor::alarm::state::overload%]",
"short_circuit": "[%key:component::victron_ble::entity::sensor::alarm::state::short_circuit%]"
}
},
"yield_today": {
"name": "Yield today"

View File

@@ -78,6 +78,7 @@ class VodafoneStationRouter(DataUpdateCoordinator[UpdateCoordinatorDataType]):
data,
session,
)
self._session = session
# Last resort as no MAC or S/N can be retrieved via API
self._id = config_entry.unique_id
@@ -135,11 +136,15 @@ class VodafoneStationRouter(DataUpdateCoordinator[UpdateCoordinatorDataType]):
_LOGGER.debug("Polling Vodafone Station host: %s", self.api.base_url.host)
try:
await self.api.login()
if not self._session.cookie_jar.filter_cookies(self.api.base_url):
_LOGGER.debug(
"Session cookies missing for host %s, re-login",
self.api.base_url.host,
)
await self.api.login()
raw_data_devices = await self.api.get_devices_data()
data_sensors = await self.api.get_sensor_data()
data_wifi = await self.api.get_wifi_data()
await self.api.logout()
except exceptions.CannotAuthenticate as err:
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN,

View File

@@ -8,5 +8,5 @@
"iot_class": "local_polling",
"loggers": ["aiovodafone"],
"quality_scale": "platinum",
"requirements": ["aiovodafone==3.1.2"]
"requirements": ["aiovodafone==3.1.3"]
}

View File

@@ -104,6 +104,7 @@ class VodafoneSwitchEntity(CoordinatorEntity[VodafoneStationRouter], SwitchEntit
await self.coordinator.api.set_wifi_status(
status, self.entity_description.typology, self.entity_description.band
)
await self.coordinator.async_request_refresh()
except CannotAuthenticate as err:
self.coordinator.config_entry.async_start_reauth(self.hass)
raise HomeAssistantError(

View File

@@ -5,7 +5,7 @@ from __future__ import annotations
from pathlib import Path
from typing import cast
from aiohttp import ClientResponseError
from aiohttp import ClientError
from yalexs.const import Brand
from yalexs.exceptions import YaleApiError
from yalexs.manager.const import CONF_BRAND
@@ -15,7 +15,12 @@ from yalexs.manager.gateway import Config as YaleXSConfig
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.exceptions import (
ConfigEntryAuthFailed,
ConfigEntryNotReady,
OAuth2TokenRequestError,
OAuth2TokenRequestReauthError,
)
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.config_entry_oauth2_flow import (
ImplementationUnavailableError,
@@ -42,11 +47,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: YaleConfigEntry) -> bool
yale_gateway = YaleGateway(Path(hass.config.config_dir), session, oauth_session)
try:
await async_setup_yale(hass, entry, yale_gateway)
except OAuth2TokenRequestReauthError as err:
raise ConfigEntryAuthFailed from err
except (RequireValidation, InvalidAuth) as err:
raise ConfigEntryAuthFailed from err
except TimeoutError as err:
raise ConfigEntryNotReady("Timed out connecting to yale api") from err
except (YaleApiError, ClientResponseError, CannotConnect) as err:
except (
YaleApiError,
OAuth2TokenRequestError,
ClientError,
CannotConnect,
) as err:
raise ConfigEntryNotReady from err
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True

View File

@@ -14,5 +14,5 @@
"integration_type": "hub",
"iot_class": "cloud_push",
"loggers": ["socketio", "engineio", "yalexs"],
"requirements": ["yalexs==9.2.0", "yalexs-ble==3.2.7"]
"requirements": ["yalexs==9.2.0", "yalexs-ble==3.2.8"]
}

View File

@@ -13,5 +13,5 @@
"documentation": "https://www.home-assistant.io/integrations/yalexs_ble",
"integration_type": "device",
"iot_class": "local_push",
"requirements": ["yalexs-ble==3.2.7"]
"requirements": ["yalexs-ble==3.2.8"]
}

View File

@@ -67,8 +67,12 @@ class ZhaCover(ZHAEntity, CoverEntity):
self.entity_data.entity.info_object.device_class
)
@staticmethod
def _convert_supported_features(
zha_features: ZHACoverEntityFeature,
) -> CoverEntityFeature:
"""Convert ZHA cover features to HA cover features."""
features = CoverEntityFeature(0)
zha_features: ZHACoverEntityFeature = self.entity_data.entity.supported_features
if ZHACoverEntityFeature.OPEN in zha_features:
features |= CoverEntityFeature.OPEN
@@ -87,7 +91,13 @@ class ZhaCover(ZHAEntity, CoverEntity):
if ZHACoverEntityFeature.SET_TILT_POSITION in zha_features:
features |= CoverEntityFeature.SET_TILT_POSITION
self._attr_supported_features = features
return features
@property
def supported_features(self) -> CoverEntityFeature:
"""Return the supported features."""
zha_features: ZHACoverEntityFeature = self.entity_data.entity.supported_features
return self._convert_supported_features(zha_features)
@property
def is_closed(self) -> bool | None:

View File

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

View File

@@ -87,6 +87,9 @@ class CoverPositionMixin(ZWaveBaseEntity, CoverEntity):
_current_position_value: ZwaveValue | None = None
_target_position_value: ZwaveValue | None = None
_stop_position_value: ZwaveValue | None = None
# Keep track of the target position for legacy devices
# that don't include the targetValue in their reports.
_commanded_target_position: int | None = None
def _set_position_values(
self,
@@ -153,12 +156,19 @@ class CoverPositionMixin(ZWaveBaseEntity, CoverEntity):
if not self._attr_is_opening and not self._attr_is_closing:
return
if (
(current := self._current_position_value) is not None
and (target := self._target_position_value) is not None
and current.value is not None
and current.value == target.value
):
if (current := self._current_position_value) is None or current.value is None:
return
# Prefer the Z-Wave targetValue property when the device reports it.
# Legacy multilevel switches only report currentValue, so fall back to
# the target position we commanded when targetValue is not available.
target_val = (
t.value
if (t := self._target_position_value) is not None and t.value is not None
else self._commanded_target_position
)
if target_val is not None and current.value == target_val:
self._attr_is_opening = False
self._attr_is_closing = False
@@ -203,6 +213,8 @@ class CoverPositionMixin(ZWaveBaseEntity, CoverEntity):
else:
return
self._commanded_target_position = target_position
self.async_write_ha_state()
async def async_set_cover_position(self, **kwargs: Any) -> None:

Some files were not shown because too many files have changed in this diff Show More