Compare commits

...

61 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
114 changed files with 3026 additions and 996 deletions

View File

@@ -1,6 +1,5 @@
"""Defines a base Alexa Devices entity."""
from aioamazondevices.const.devices import SPEAKER_GROUP_DEVICE_TYPE
from aioamazondevices.structures import AmazonDevice
from homeassistant.helpers.device_registry import DeviceInfo
@@ -25,20 +24,15 @@ class AmazonEntity(CoordinatorEntity[AmazonDevicesCoordinator]):
"""Initialize the entity."""
super().__init__(coordinator)
self._serial_num = serial_num
model = self.device.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=self.device.manufacturer or "Amazon",
hw_version=self.device.hardware_version,
sw_version=(
self.device.software_version
if model != SPEAKER_GROUP_DEVICE_TYPE
else None
),
serial_number=serial_num if model != SPEAKER_GROUP_DEVICE_TYPE else None,
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==13.0.0"]
"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

@@ -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

@@ -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

@@ -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==20260304.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

@@ -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

@@ -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

@@ -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

@@ -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

@@ -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.31"]
"requirements": ["pyportainer==1.0.33"]
}

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

@@ -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

@@ -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

@@ -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

@@ -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

@@ -266,13 +266,20 @@ class TemplateEntity(AbstractTemplateEntity):
def _get_this_variable(self) -> TemplateStateFromEntityId:
"""Create a this variable for the entity."""
entity_id = self.entity_id
if self._preview_callback:
preview_entity_id = async_generate_entity_id(
self._entity_id_format, self._attr_name or "preview", hass=self.hass
)
return TemplateStateFromEntityId(self.hass, preview_entity_id)
# 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, self.entity_id)
return TemplateStateFromEntityId(self.hass, entity_id)
def _render_script_variables(self) -> dict[str, Any]:
"""Render configured 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

@@ -23,7 +23,7 @@
"universal_silabs_flasher",
"serialx"
],
"requirements": ["zha==1.0.1", "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:

View File

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

View File

@@ -87,6 +87,12 @@ async def _ssrf_redirect_middleware(
# Relative redirects stay on the same host - always safe
return resp
# Only schemes that aiohttp can open a network connection for need
# SSRF protection. Custom app URI schemes (e.g. weconnect://) are inert
# from a networking perspective and must not be blocked.
if connector and redirect_url.scheme not in connector.allowed_protocol_schema_set:
return resp
host = redirect_url.host
if await _async_is_blocked_host(host, connector):
resp.close()

View File

@@ -181,15 +181,24 @@ class RestoreStateData:
}
# Start with the currently registered states
stored_states = [
StoredState(
current_states_by_entity_id[entity_id],
entity.extra_restore_state_data,
now,
stored_states: list[StoredState] = []
for entity_id, entity in self.entities.items():
if entity_id not in current_states_by_entity_id:
continue
try:
extra_data = entity.extra_restore_state_data
except Exception:
_LOGGER.exception(
"Error getting extra restore state data for %s", entity_id
)
continue
stored_states.append(
StoredState(
current_states_by_entity_id[entity_id],
extra_data,
now,
)
)
for entity_id, entity in self.entities.items()
if entity_id in current_states_by_entity_id
]
expiration_time = now - STATE_EXPIRATION
for entity_id, stored_state in self.last_states.items():
@@ -219,6 +228,8 @@ class RestoreStateData:
)
except HomeAssistantError as exc:
_LOGGER.error("Error saving current states", exc_info=exc)
except Exception:
_LOGGER.exception("Unexpected error saving current states")
@callback
def async_setup_dump(self, *args: Any) -> None:
@@ -258,13 +269,15 @@ class RestoreStateData:
@callback
def async_restore_entity_removed(
self, entity_id: str, extra_data: ExtraStoredData | None
self,
entity_id: str,
state: State | None,
extra_data: ExtraStoredData | None,
) -> None:
"""Unregister this entity from saving state."""
# When an entity is being removed from hass, store its last state. This
# allows us to support state restoration if the entity is removed, then
# re-added while hass is still running.
state = self.hass.states.get(entity_id)
# To fully mimic all the attribute data types when loaded from storage,
# we're going to serialize it to JSON and then re-load it.
if state is not None:
@@ -287,8 +300,18 @@ class RestoreEntity(Entity):
async def async_internal_will_remove_from_hass(self) -> None:
"""Run when entity will be removed from hass."""
try:
extra_data = self.extra_restore_state_data
except Exception:
_LOGGER.exception(
"Error getting extra restore state data for %s", self.entity_id
)
state = None
extra_data = None
else:
state = self.hass.states.get(self.entity_id)
async_get(self.hass).async_restore_entity_removed(
self.entity_id, self.extra_restore_state_data
self.entity_id, state, extra_data
)
await super().async_internal_will_remove_from_hass()

View File

@@ -301,6 +301,7 @@ class AreaSelectorConfig(BaseSelectorConfig, total=False):
entity: EntityFilterSelectorConfig | list[EntityFilterSelectorConfig]
device: DeviceFilterSelectorConfig | list[DeviceFilterSelectorConfig]
multiple: bool
reorder: bool
@SELECTORS.register("area")
@@ -320,6 +321,7 @@ class AreaSelector(Selector[AreaSelectorConfig]):
[DEVICE_FILTER_SELECTOR_CONFIG_SCHEMA],
),
vol.Optional("multiple", default=False): cv.boolean,
vol.Optional("reorder", default=False): cv.boolean,
}
)

View File

@@ -40,7 +40,7 @@ habluetooth==5.8.0
hass-nabucasa==1.15.0
hassil==3.5.0
home-assistant-bluetooth==1.13.1
home-assistant-frontend==20260304.0
home-assistant-frontend==20260312.0
home-assistant-intents==2026.3.3
httpx==0.28.1
ifaddr==0.2.0
@@ -48,7 +48,7 @@ Jinja2==3.1.6
lru-dict==1.3.0
mutagen==1.47.0
openai==2.21.0
orjson==3.11.5
orjson==3.11.7
packaging>=23.1
paho-mqtt==2.1.0
Pillow==12.1.1

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "homeassistant"
version = "2026.3.0"
version = "2026.3.2"
license = "Apache-2.0"
license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"]
description = "Open-source home automation platform running on Python 3."
@@ -65,7 +65,7 @@ dependencies = [
"Pillow==12.1.1",
"propcache==0.4.1",
"pyOpenSSL==25.3.0",
"orjson==3.11.5",
"orjson==3.11.7",
"packaging>=23.1",
"psutil-home-assistant==0.0.1",
"python-slugify==8.0.4",

2
requirements.txt generated
View File

@@ -34,7 +34,7 @@ ifaddr==0.2.0
Jinja2==3.1.6
lru-dict==1.3.0
mutagen==1.47.0
orjson==3.11.5
orjson==3.11.7
packaging>=23.1
Pillow==12.1.1
propcache==0.4.1

44
requirements_all.txt generated
View File

@@ -47,7 +47,7 @@ PlexAPI==4.15.16
ProgettiHWSW==0.1.3
# homeassistant.components.cast
PyChromecast==14.0.9
PyChromecast==14.0.10
# homeassistant.components.flume
PyFlume==0.6.5
@@ -190,7 +190,7 @@ aioairzone-cloud==0.7.2
aioairzone==1.0.5
# homeassistant.components.alexa_devices
aioamazondevices==13.0.0
aioamazondevices==13.0.1
# homeassistant.components.ambient_network
# homeassistant.components.ambient_station
@@ -224,7 +224,7 @@ aiobafi6==0.9.0
aiobotocore==2.21.1
# homeassistant.components.comelit
aiocomelit==2.0.0
aiocomelit==2.0.1
# homeassistant.components.dhcp
aiodhcpwatcher==1.2.1
@@ -413,7 +413,7 @@ aiosteamist==1.0.1
aiostreammagic==2.13.0
# homeassistant.components.switcher_kis
aioswitcher==6.1.0
aioswitcher==6.1.1
# homeassistant.components.syncthing
aiosyncthing==0.7.1
@@ -437,7 +437,7 @@ aiousbwatcher==1.1.1
aiovlc==0.5.1
# homeassistant.components.vodafone_station
aiovodafone==3.1.2
aiovodafone==3.1.3
# homeassistant.components.waqi
aiowaqi==3.1.0
@@ -556,7 +556,7 @@ async-upnp-client==0.46.2
asyncarve==0.1.1
# homeassistant.components.keyboard_remote
asyncinotify==4.2.0
asyncinotify==4.4.0
# homeassistant.components.supla
asyncpysupla==0.0.5
@@ -939,7 +939,7 @@ eternalegypt==0.0.18
eufylife-ble-client==0.1.8
# homeassistant.components.keyboard_remote
# evdev==1.6.1
# evdev==1.9.3
# homeassistant.components.evohome
evohome-async==1.1.3
@@ -1122,7 +1122,7 @@ gotailwind==0.3.0
govee-ble==0.44.0
# homeassistant.components.govee_light_local
govee-local-api==2.3.0
govee-local-api==2.4.0
# homeassistant.components.remote_rpi_gpio
gpiozero==1.6.2
@@ -1226,7 +1226,7 @@ hole==0.9.0
holidays==0.84
# homeassistant.components.frontend
home-assistant-frontend==20260304.0
home-assistant-frontend==20260312.0
# homeassistant.components.conversation
home-assistant-intents==2026.3.3
@@ -1271,7 +1271,7 @@ ibeacon-ble==1.2.0
# homeassistant.components.local_calendar
# homeassistant.components.local_todo
# homeassistant.components.remote_calendar
ical==13.2.0
ical==13.2.2
# homeassistant.components.caldav
icalendar==6.3.1
@@ -1663,7 +1663,7 @@ odp-amsterdam==6.1.2
oemthermostat==1.1.1
# homeassistant.components.ohme
ohme==1.6.0
ohme==1.7.0
# homeassistant.components.ollama
ollama==0.5.1
@@ -1676,7 +1676,7 @@ ondilo==0.5.0
# homeassistant.components.onedrive
# homeassistant.components.onedrive_for_business
onedrive-personal-sdk==0.1.4
onedrive-personal-sdk==0.1.7
# homeassistant.components.onvif
onvif-zeep-async==4.0.4
@@ -1938,7 +1938,7 @@ pyairobotrest==0.3.0
pyairvisual==2023.08.1
# homeassistant.components.anglian_water
pyanglianwater==3.1.0
pyanglianwater==3.1.1
# homeassistant.components.aprilaire
pyaprilaire==0.9.1
@@ -2179,7 +2179,7 @@ pyitachip2ir==0.0.7
pyituran==0.1.5
# homeassistant.components.jvc_projector
pyjvcprojector==2.0.1
pyjvcprojector==2.0.3
# homeassistant.components.kaleidescape
pykaleidescape==1.1.3
@@ -2370,7 +2370,7 @@ pyplaato==0.0.19
pypoint==3.0.0
# homeassistant.components.portainer
pyportainer==1.0.31
pyportainer==1.0.33
# homeassistant.components.probe_plus
pyprobeplus==1.1.2
@@ -2473,7 +2473,7 @@ pysmappee==0.2.29
pysmarlaapi==1.0.1
# homeassistant.components.smartthings
pysmartthings==3.6.0
pysmartthings==3.7.0
# homeassistant.components.smarty
pysmarty2==0.10.3
@@ -2530,7 +2530,7 @@ python-awair==0.2.5
python-blockchain-api==0.0.2
# homeassistant.components.bsblan
python-bsblan==5.1.0
python-bsblan==5.1.2
# homeassistant.components.citybikes
python-citybikes==0.3.3
@@ -2612,7 +2612,7 @@ python-opensky==1.0.1
# homeassistant.components.otbr
# homeassistant.components.thread
python-otbr-api==2.8.0
python-otbr-api==2.9.0
# homeassistant.components.overseerr
python-overseerr==0.9.0
@@ -2969,7 +2969,7 @@ speak2mary==1.4.0
speedtest-cli==2.1.3
# homeassistant.components.spotify
spotifyaio==1.0.0
spotifyaio==2.0.2
# homeassistant.components.sql
sqlparse==0.5.5
@@ -3038,7 +3038,7 @@ tellcore-py==1.1.2
tellduslive==0.10.12
# homeassistant.components.teltonika
teltasync==0.1.3
teltasync==0.2.0
# homeassistant.components.lg_soundbar
temescal==0.5
@@ -3307,7 +3307,7 @@ yalesmartalarmclient==0.4.3
# homeassistant.components.august
# homeassistant.components.yale
# homeassistant.components.yalexs_ble
yalexs-ble==3.2.7
yalexs-ble==3.2.8
# homeassistant.components.august
# homeassistant.components.yale
@@ -3347,7 +3347,7 @@ zeroconf==0.148.0
zeversolar==0.3.2
# homeassistant.components.zha
zha==1.0.1
zha==1.0.2
# homeassistant.components.zhong_hong
zhong-hong-hvac==1.0.13

View File

@@ -47,7 +47,7 @@ PlexAPI==4.15.16
ProgettiHWSW==0.1.3
# homeassistant.components.cast
PyChromecast==14.0.9
PyChromecast==14.0.10
# homeassistant.components.flume
PyFlume==0.6.5
@@ -181,7 +181,7 @@ aioairzone-cloud==0.7.2
aioairzone==1.0.5
# homeassistant.components.alexa_devices
aioamazondevices==13.0.0
aioamazondevices==13.0.1
# homeassistant.components.ambient_network
# homeassistant.components.ambient_station
@@ -215,7 +215,7 @@ aiobafi6==0.9.0
aiobotocore==2.21.1
# homeassistant.components.comelit
aiocomelit==2.0.0
aiocomelit==2.0.1
# homeassistant.components.dhcp
aiodhcpwatcher==1.2.1
@@ -398,7 +398,7 @@ aiosteamist==1.0.1
aiostreammagic==2.13.0
# homeassistant.components.switcher_kis
aioswitcher==6.1.0
aioswitcher==6.1.1
# homeassistant.components.syncthing
aiosyncthing==0.7.1
@@ -422,7 +422,7 @@ aiousbwatcher==1.1.1
aiovlc==0.5.1
# homeassistant.components.vodafone_station
aiovodafone==3.1.2
aiovodafone==3.1.3
# homeassistant.components.waqi
aiowaqi==3.1.0
@@ -998,7 +998,7 @@ gotailwind==0.3.0
govee-ble==0.44.0
# homeassistant.components.govee_light_local
govee-local-api==2.3.0
govee-local-api==2.4.0
# homeassistant.components.gpsd
gps3==0.33.3
@@ -1087,7 +1087,7 @@ hole==0.9.0
holidays==0.84
# homeassistant.components.frontend
home-assistant-frontend==20260304.0
home-assistant-frontend==20260312.0
# homeassistant.components.conversation
home-assistant-intents==2026.3.3
@@ -1126,7 +1126,7 @@ ibeacon-ble==1.2.0
# homeassistant.components.local_calendar
# homeassistant.components.local_todo
# homeassistant.components.remote_calendar
ical==13.2.0
ical==13.2.2
# homeassistant.components.caldav
icalendar==6.3.1
@@ -1449,7 +1449,7 @@ objgraph==3.5.0
odp-amsterdam==6.1.2
# homeassistant.components.ohme
ohme==1.6.0
ohme==1.7.0
# homeassistant.components.ollama
ollama==0.5.1
@@ -1462,7 +1462,7 @@ ondilo==0.5.0
# homeassistant.components.onedrive
# homeassistant.components.onedrive_for_business
onedrive-personal-sdk==0.1.4
onedrive-personal-sdk==0.1.7
# homeassistant.components.onvif
onvif-zeep-async==4.0.4
@@ -1669,7 +1669,7 @@ pyairobotrest==0.3.0
pyairvisual==2023.08.1
# homeassistant.components.anglian_water
pyanglianwater==3.1.0
pyanglianwater==3.1.1
# homeassistant.components.aprilaire
pyaprilaire==0.9.1
@@ -1856,7 +1856,7 @@ pyisy==3.4.1
pyituran==0.1.5
# homeassistant.components.jvc_projector
pyjvcprojector==2.0.1
pyjvcprojector==2.0.3
# homeassistant.components.kaleidescape
pykaleidescape==1.1.3
@@ -2020,7 +2020,7 @@ pyplaato==0.0.19
pypoint==3.0.0
# homeassistant.components.portainer
pyportainer==1.0.31
pyportainer==1.0.33
# homeassistant.components.probe_plus
pyprobeplus==1.1.2
@@ -2102,7 +2102,7 @@ pysmappee==0.2.29
pysmarlaapi==1.0.1
# homeassistant.components.smartthings
pysmartthings==3.6.0
pysmartthings==3.7.0
# homeassistant.components.smarty
pysmarty2==0.10.3
@@ -2153,7 +2153,7 @@ python-MotionMount==2.3.0
python-awair==0.2.5
# homeassistant.components.bsblan
python-bsblan==5.1.0
python-bsblan==5.1.2
# homeassistant.components.ecobee
python-ecobee-api==0.3.2
@@ -2208,7 +2208,7 @@ python-opensky==1.0.1
# homeassistant.components.otbr
# homeassistant.components.thread
python-otbr-api==2.8.0
python-otbr-api==2.9.0
# homeassistant.components.overseerr
python-overseerr==0.9.0
@@ -2502,7 +2502,7 @@ speak2mary==1.4.0
speedtest-cli==2.1.3
# homeassistant.components.spotify
spotifyaio==1.0.0
spotifyaio==2.0.2
# homeassistant.components.sql
sqlparse==0.5.5
@@ -2550,7 +2550,7 @@ tailscale==0.6.2
tellduslive==0.10.12
# homeassistant.components.teltonika
teltasync==0.1.3
teltasync==0.2.0
# homeassistant.components.lg_soundbar
temescal==0.5
@@ -2783,7 +2783,7 @@ yalesmartalarmclient==0.4.3
# homeassistant.components.august
# homeassistant.components.yale
# homeassistant.components.yalexs_ble
yalexs-ble==3.2.7
yalexs-ble==3.2.8
# homeassistant.components.august
# homeassistant.components.yale
@@ -2817,7 +2817,7 @@ zeroconf==0.148.0
zeversolar==0.3.2
# homeassistant.components.zha
zha==1.0.1
zha==1.0.2
# homeassistant.components.zinvolt
zinvolt==0.3.0

View File

@@ -181,7 +181,6 @@ EXCEPTIONS = {
"PySwitchmate", # https://github.com/Danielhiversen/pySwitchmate/pull/16
"PyXiaomiGateway", # https://github.com/Danielhiversen/PyXiaomiGateway/pull/201
"chacha20poly1305", # LGPL
"caio", # Apache 2 https://github.com/mosquito/caio/?tab=Apache-2.0-1-ov-file#readme
"commentjson", # https://github.com/vaidik/commentjson/pull/55
"crownstone-cloud", # https://github.com/crownstone/crownstone-lib-python-cloud/pull/5
"crownstone-core", # https://github.com/crownstone/crownstone-lib-python-core/pull/6

View File

@@ -2,10 +2,7 @@
from unittest.mock import AsyncMock
from aioamazondevices.const.devices import (
SPEAKER_GROUP_DEVICE_TYPE,
SPEAKER_GROUP_FAMILY,
)
from aioamazondevices.const.devices import SPEAKER_GROUP_FAMILY
from aioamazondevices.exceptions import CannotConnect, CannotRetrieveData
import pytest
@@ -117,7 +114,7 @@ async def test_alexa_dnd_group_removal(
identifiers={(DOMAIN, mock_config_entry.entry_id)},
name=mock_config_entry.title,
manufacturer="Amazon",
model=SPEAKER_GROUP_DEVICE_TYPE,
model="Speaker Group",
entry_type=dr.DeviceEntryType.SERVICE,
)
@@ -156,7 +153,7 @@ async def test_alexa_unsupported_notification_sensor_removal(
identifiers={(DOMAIN, mock_config_entry.entry_id)},
name=mock_config_entry.title,
manufacturer="Amazon",
model=SPEAKER_GROUP_DEVICE_TYPE,
model="Speaker Group",
entry_type=dr.DeviceEntryType.SERVICE,
)

View File

@@ -2,7 +2,7 @@
from unittest.mock import Mock, patch
from aiohttp import ClientResponseError
from aiohttp import ClientError, ClientResponseError
import pytest
from yalexs.const import Brand
from yalexs.exceptions import AugustApiAIOHTTPError, InvalidAuth
@@ -18,7 +18,11 @@ from homeassistant.const import (
STATE_ON,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.exceptions import (
HomeAssistantError,
OAuth2TokenRequestReauthError,
OAuth2TokenRequestTransientError,
)
from homeassistant.helpers import (
device_registry as dr,
entity_registry as er,
@@ -304,3 +308,59 @@ async def test_oauth_implementation_not_available(hass: HomeAssistant) -> None:
await hass.async_block_till_done()
assert entry.state is ConfigEntryState.SETUP_RETRY
async def test_oauth_token_request_reauth_error(hass: HomeAssistant) -> None:
"""Test OAuth token request reauth error starts a reauth flow."""
entry = await mock_august_config_entry(hass)
with patch(
"homeassistant.helpers.config_entry_oauth2_flow.OAuth2Session.async_ensure_token_valid",
side_effect=OAuth2TokenRequestReauthError(
request_info=Mock(real_url="https://auth.august.com/access_token"),
status=401,
domain=DOMAIN,
),
):
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
assert entry.state is ConfigEntryState.SETUP_ERROR
flows = hass.config_entries.flow.async_progress()
assert len(flows) == 1
assert flows[0]["step_id"] == "pick_implementation"
assert flows[0]["context"]["source"] == "reauth"
async def test_oauth_token_request_transient_error_is_retryable(
hass: HomeAssistant,
) -> None:
"""Test OAuth token transient request error marks entry for setup retry."""
entry = await mock_august_config_entry(hass)
with patch(
"homeassistant.helpers.config_entry_oauth2_flow.OAuth2Session.async_ensure_token_valid",
side_effect=OAuth2TokenRequestTransientError(
request_info=Mock(real_url="https://auth.august.com/access_token"),
status=500,
domain=DOMAIN,
),
):
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
assert entry.state is ConfigEntryState.SETUP_RETRY
async def test_oauth_client_error_is_retryable(hass: HomeAssistant) -> None:
"""Test OAuth transport client errors mark entry for setup retry."""
entry = await mock_august_config_entry(hass)
with patch(
"homeassistant.helpers.config_entry_oauth2_flow.OAuth2Session.async_ensure_token_valid",
side_effect=ClientError("connection error"),
):
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
assert entry.state is ConfigEntryState.SETUP_RETRY

View File

@@ -396,6 +396,11 @@ async def test_switch_device_no_ip_address(
"async_set_deflection_enable",
STATE_ON,
),
(
"switch.mock_title_wi_fi_mywifi",
"async_set_wlan_configuration",
STATE_ON,
),
],
)
async def test_switch_turn_on_off(

View File

@@ -16,7 +16,7 @@ from tests.common import MockConfigEntry
API_URL = "https://test.ghost.io"
API_KEY = "650b7a9f8e8c1234567890ab:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"
SITE_UUID = "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
SITE_DATA = {"title": "Test Ghost", "url": API_URL, "uuid": SITE_UUID}
SITE_DATA = {"title": "Test Ghost", "url": API_URL, "site_uuid": SITE_UUID}
POSTS_DATA = {"published": 42, "drafts": 5, "scheduled": 2}
MEMBERS_DATA = {"total": 1000, "paid": 100, "free": 850, "comped": 50}
LATEST_POST_DATA = {

View File

@@ -414,7 +414,7 @@
'object_id_base': 'Energy exported',
'options': dict({
'sensor': dict({
'suggested_display_precision': 0,
'suggested_display_precision': 2,
}),
}),
'original_device_class': <SensorDeviceClass.ENERGY: 'energy'>,
@@ -426,7 +426,7 @@
'supported_features': 0,
'translation_key': 'energy_exported',
'unique_id': '40580137858664_energy_exported',
'unit_of_measurement': <UnitOfEnergy.WATT_HOUR: 'Wh'>,
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
})
# ---
# name: test_entities[sensor.homevolt_ems_energy_exported-state]
@@ -435,7 +435,7 @@
'device_class': 'energy',
'friendly_name': 'Homevolt EMS Energy exported',
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
'unit_of_measurement': <UnitOfEnergy.WATT_HOUR: 'Wh'>,
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
'context': <ANY>,
'entity_id': 'sensor.homevolt_ems_energy_exported',
@@ -471,7 +471,7 @@
'object_id_base': 'Energy imported',
'options': dict({
'sensor': dict({
'suggested_display_precision': 0,
'suggested_display_precision': 2,
}),
}),
'original_device_class': <SensorDeviceClass.ENERGY: 'energy'>,
@@ -483,7 +483,7 @@
'supported_features': 0,
'translation_key': 'energy_imported',
'unique_id': '40580137858664_energy_imported',
'unit_of_measurement': <UnitOfEnergy.WATT_HOUR: 'Wh'>,
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
})
# ---
# name: test_entities[sensor.homevolt_ems_energy_imported-state]
@@ -492,7 +492,7 @@
'device_class': 'energy',
'friendly_name': 'Homevolt EMS Energy imported',
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
'unit_of_measurement': <UnitOfEnergy.WATT_HOUR: 'Wh'>,
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
'context': <ANY>,
'entity_id': 'sensor.homevolt_ems_energy_imported',

View File

@@ -8007,8 +8007,11 @@
'name': None,
'object_id_base': 'Water usage',
'options': dict({
'sensor': dict({
'suggested_display_precision': 0,
}),
}),
'original_device_class': None,
'original_device_class': <SensorDeviceClass.VOLUME_FLOW_RATE: 'volume_flow_rate'>,
'original_icon': None,
'original_name': 'Water usage',
'platform': 'homewizard',
@@ -8023,6 +8026,7 @@
# name: test_sensors[HWE-P1-entity_ids0][sensor.device_water_usage:state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'volume_flow_rate',
'friendly_name': 'Device Water usage',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfVolumeFlowRate.LITERS_PER_MINUTE: 'L/min'>,
@@ -11927,8 +11931,11 @@
'name': None,
'object_id_base': 'Water usage',
'options': dict({
'sensor': dict({
'suggested_display_precision': 0,
}),
}),
'original_device_class': None,
'original_device_class': <SensorDeviceClass.VOLUME_FLOW_RATE: 'volume_flow_rate'>,
'original_icon': None,
'original_name': 'Water usage',
'platform': 'homewizard',
@@ -11943,6 +11950,7 @@
# name: test_sensors[HWE-P1-invalid-EAN-entity_ids9][sensor.device_water_usage:state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'volume_flow_rate',
'friendly_name': 'Device Water usage',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfVolumeFlowRate.LITERS_PER_MINUTE: 'L/min'>,
@@ -15408,8 +15416,11 @@
'name': None,
'object_id_base': 'Water usage',
'options': dict({
'sensor': dict({
'suggested_display_precision': 0,
}),
}),
'original_device_class': None,
'original_device_class': <SensorDeviceClass.VOLUME_FLOW_RATE: 'volume_flow_rate'>,
'original_icon': None,
'original_name': 'Water usage',
'platform': 'homewizard',
@@ -15424,6 +15435,7 @@
# name: test_sensors[HWE-P1-zero-values-entity_ids1][sensor.device_water_usage:state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'volume_flow_rate',
'friendly_name': 'Device Water usage',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfVolumeFlowRate.LITERS_PER_MINUTE: 'L/min'>,
@@ -17573,8 +17585,11 @@
'name': None,
'object_id_base': 'Water usage',
'options': dict({
'sensor': dict({
'suggested_display_precision': 0,
}),
}),
'original_device_class': None,
'original_device_class': <SensorDeviceClass.VOLUME_FLOW_RATE: 'volume_flow_rate'>,
'original_icon': None,
'original_name': 'Water usage',
'platform': 'homewizard',
@@ -17589,6 +17604,7 @@
# name: test_sensors[HWE-WTR-entity_ids4][sensor.device_water_usage:state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'volume_flow_rate',
'friendly_name': 'Device Water usage',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfVolumeFlowRate.LITERS_PER_MINUTE: 'L/min'>,

View File

@@ -427,9 +427,7 @@
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
@@ -466,7 +464,6 @@
'attribution': 'Data provided by unpublished Intellifire API',
'device_class': 'timestamp',
'friendly_name': 'IntelliFire Timer end',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'context': <ANY>,
'entity_id': 'sensor.intellifire_timer_end',

View File

@@ -833,7 +833,7 @@
'max': 450,
'min': 10,
'mode': <NumberMode.BOX: 'box'>,
'step': 5,
'step': 1,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -872,7 +872,7 @@
'max': 450,
'min': 10,
'mode': <NumberMode.BOX: 'box'>,
'step': 5,
'step': 1,
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
}),
'context': <ANY>,

View File

@@ -0,0 +1,33 @@
"""Test KNX DPT default attributes."""
import pytest
from homeassistant.components.knx.dpt import (
_sensor_device_classes,
_sensor_state_class_overrides,
_sensor_unit_overrides,
)
from homeassistant.components.knx.schema import _sensor_attribute_sub_validator
@pytest.mark.parametrize(
"dpt",
sorted(
{
*_sensor_device_classes,
*_sensor_state_class_overrides,
*_sensor_unit_overrides,
# add generic numeric DPTs without specific device and state class
"7",
"2byte_float",
}
),
)
def test_dpt_default_device_classes(dpt: str) -> None:
"""Test DPT default device and state classes and unit are valid."""
assert _sensor_attribute_sub_validator(
# YAML sensor config - only set type for this validation
# other keys are not required for this test
# UI validation works the same way, but uses different schema for config
{"type": dpt}
)

File diff suppressed because it is too large Load Diff

View File

@@ -644,6 +644,31 @@ async def test_setting_device_tracker_location_via_abbr_reset_message(
assert state.attributes["source_type"] == "gps"
assert state.state == STATE_HOME
# Override the GPS state via a direct state update
async_fire_mqtt_message(hass, "test-topic", "office")
state = hass.states.get("device_tracker.test")
assert state.state == "office"
# Test a GPS attributes update without a reset
async_fire_mqtt_message(
hass,
"attributes-topic",
'{"latitude":32.87336,"longitude": -117.22743, "gps_accuracy":1.5}',
)
state = hass.states.get("device_tracker.test")
assert state.state == "office"
# Reset the manual set location
# This should calculate the location from GPS attributes
async_fire_mqtt_message(hass, "test-topic", "reset")
state = hass.states.get("device_tracker.test")
assert state.attributes["latitude"] == 32.87336
assert state.attributes["longitude"] == -117.22743
assert state.attributes["gps_accuracy"] == 1.5
assert state.attributes["source_type"] == "gps"
assert state.state == STATE_HOME
async def test_setting_blocked_attribute_via_mqtt_json_message(
hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator

View File

@@ -2,7 +2,6 @@
# name: test_diagnostics
dict({
'cap_available': True,
'ct_connected': True,
'device_info': dict({
'model': 'Home Pro',
'name': 'Ohme Home Pro',

View File

@@ -263,20 +263,28 @@ async def test_full_flow_reconfigure_unique_id(
) -> None:
"""Test the full flow of the config flow, this time with a known unique ID."""
mock_config_entry.add_to_hass(hass)
other_entry = MockConfigEntry(
domain=DOMAIN,
title="Portainer other",
data=USER_INPUT_RECONFIGURE,
unique_id=USER_INPUT_RECONFIGURE[CONF_API_TOKEN],
)
other_entry.add_to_hass(hass)
result = await mock_config_entry.start_reconfigure_flow(hass)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "reconfigure"
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input=MOCK_USER_SETUP,
user_input=USER_INPUT_RECONFIGURE,
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "already_configured"
assert mock_config_entry.data[CONF_API_TOKEN] == "test_api_token"
assert mock_config_entry.data[CONF_URL] == "https://127.0.0.1:9000/"
assert mock_config_entry.data[CONF_VERIFY_SSL] is True
assert len(mock_setup_entry.mock_calls) == 0

View File

@@ -5,6 +5,7 @@ from __future__ import annotations
from collections.abc import Generator
from http import HTTPStatus
import json
import re
from typing import Any
from unittest.mock import patch
@@ -278,4 +279,4 @@ def handle_responses(
async def handle(method, url, data) -> AiohttpClientMockResponse:
return responses.pop(0)
aioclient_mock.post(URL, side_effect=handle)
aioclient_mock.post(re.compile(r"^https?://[^/]+/stick$"), side_effect=handle)

View File

@@ -70,7 +70,7 @@ async def test_no_unique_id(
"""Test rainsensor binary sensor with no unique id."""
# Failure to migrate config entry to a unique id
responses.insert(0, mock_response_error(HTTPStatus.SERVICE_UNAVAILABLE))
responses.insert(1, mock_response_error(HTTPStatus.SERVICE_UNAVAILABLE))
await hass.config_entries.async_setup(config_entry.entry_id)
assert config_entry.state is ConfigEntryState.LOADED

View File

@@ -292,7 +292,7 @@ async def test_no_unique_id(
"""Test calendar entity with no unique id."""
# Failure to migrate config entry to a unique id
responses.insert(0, mock_response_error(HTTPStatus.SERVICE_UNAVAILABLE))
responses.insert(1, mock_response_error(HTTPStatus.SERVICE_UNAVAILABLE))
await hass.config_entries.async_setup(config_entry.entry_id)
assert config_entry.state is ConfigEntryState.LOADED

View File

@@ -19,6 +19,7 @@ from .conftest import (
CONFIG_ENTRY_DATA,
HOST,
MAC_ADDRESS_UNIQUE_ID,
MODEL_AND_VERSION_RESPONSE,
PASSWORD,
SERIAL_NUMBER,
SERIAL_RESPONSE,
@@ -36,7 +37,11 @@ from tests.test_util.aiohttp import AiohttpClientMocker, AiohttpClientMockRespon
@pytest.fixture(name="responses")
def mock_responses() -> list[AiohttpClientMockResponse]:
"""Set up fake serial number response when testing the connection."""
return [mock_response(SERIAL_RESPONSE), mock_json_response(WIFI_PARAMS_RESPONSE)]
return [
mock_response(MODEL_AND_VERSION_RESPONSE),
mock_response(SERIAL_RESPONSE),
mock_json_response(WIFI_PARAMS_RESPONSE),
]
@pytest.fixture(autouse=True)
@@ -77,6 +82,7 @@ async def complete_flow(hass: HomeAssistant, password: str = PASSWORD) -> FlowRe
[
(
[
mock_response(MODEL_AND_VERSION_RESPONSE),
mock_response(SERIAL_RESPONSE),
mock_json_response(WIFI_PARAMS_RESPONSE),
],
@@ -85,6 +91,7 @@ async def complete_flow(hass: HomeAssistant, password: str = PASSWORD) -> FlowRe
),
(
[
mock_response(MODEL_AND_VERSION_RESPONSE),
mock_response(ZERO_SERIAL_RESPONSE),
mock_json_response(WIFI_PARAMS_RESPONSE),
],
@@ -123,7 +130,11 @@ async def test_controller_flow(
(
"other-serial-number",
{**CONFIG_ENTRY_DATA, "host": "other-host"},
[mock_response(SERIAL_RESPONSE), mock_json_response(WIFI_PARAMS_RESPONSE)],
[
mock_response(MODEL_AND_VERSION_RESPONSE),
mock_response(SERIAL_RESPONSE),
mock_json_response(WIFI_PARAMS_RESPONSE),
],
CONFIG_ENTRY_DATA,
),
(
@@ -133,6 +144,7 @@ async def test_controller_flow(
"host": "other-host",
},
[
mock_response(MODEL_AND_VERSION_RESPONSE),
mock_response(SERIAL_RESPONSE),
mock_json_response(WIFI_PARAMS_RESPONSE),
],
@@ -142,6 +154,7 @@ async def test_controller_flow(
None,
{**CONFIG_ENTRY_DATA, "serial_number": 0, "host": "other-host"},
[
mock_response(MODEL_AND_VERSION_RESPONSE),
mock_response(ZERO_SERIAL_RESPONSE),
mock_json_response(WIFI_PARAMS_RESPONSE),
],
@@ -185,6 +198,7 @@ async def test_multiple_config_entries(
MAC_ADDRESS_UNIQUE_ID,
CONFIG_ENTRY_DATA,
[
mock_response(MODEL_AND_VERSION_RESPONSE),
mock_response(SERIAL_RESPONSE),
mock_json_response(WIFI_PARAMS_RESPONSE),
],
@@ -194,7 +208,11 @@ async def test_multiple_config_entries(
(
SERIAL_NUMBER,
CONFIG_ENTRY_DATA,
[mock_response(SERIAL_RESPONSE), mock_json_response(WIFI_PARAMS_RESPONSE)],
[
mock_response(MODEL_AND_VERSION_RESPONSE),
mock_response(SERIAL_RESPONSE),
mock_json_response(WIFI_PARAMS_RESPONSE),
],
CONFIG_ENTRY_DATA,
),
# Old unique id with no serial, but same host
@@ -202,6 +220,7 @@ async def test_multiple_config_entries(
None,
{**CONFIG_ENTRY_DATA, "serial_number": 0},
[
mock_response(MODEL_AND_VERSION_RESPONSE),
mock_response(ZERO_SERIAL_RESPONSE),
mock_json_response(WIFI_PARAMS_RESPONSE),
],
@@ -214,7 +233,11 @@ async def test_multiple_config_entries(
**CONFIG_ENTRY_DATA,
"host": f"other-{HOST}",
},
[mock_response(SERIAL_RESPONSE), mock_json_response(WIFI_PARAMS_RESPONSE)],
[
mock_response(MODEL_AND_VERSION_RESPONSE),
mock_response(SERIAL_RESPONSE),
mock_json_response(WIFI_PARAMS_RESPONSE),
],
CONFIG_ENTRY_DATA, # Updated the host
),
],
@@ -281,8 +304,8 @@ async def test_controller_invalid_auth(
[
# Incorrect password response
AiohttpClientMockResponse("POST", URL, status=HTTPStatus.FORBIDDEN),
AiohttpClientMockResponse("POST", URL, status=HTTPStatus.FORBIDDEN),
# Second attempt with the correct password
mock_response(MODEL_AND_VERSION_RESPONSE),
mock_response(SERIAL_RESPONSE),
mock_json_response(WIFI_PARAMS_RESPONSE),
]
@@ -346,8 +369,8 @@ async def test_controller_timeout(
[
# First attempt simulate the wrong password
AiohttpClientMockResponse("POST", URL, status=HTTPStatus.FORBIDDEN),
AiohttpClientMockResponse("POST", URL, status=HTTPStatus.FORBIDDEN),
# Second attempt simulate the correct password
mock_response(MODEL_AND_VERSION_RESPONSE),
mock_response(SERIAL_RESPONSE),
mock_json_response(WIFI_PARAMS_RESPONSE),
],

View File

@@ -62,6 +62,7 @@ async def test_init_success(
(
CONFIG_ENTRY_DATA,
[
mock_response(MODEL_AND_VERSION_RESPONSE),
mock_response(MODEL_AND_VERSION_RESPONSE),
mock_response_error(HTTPStatus.SERVICE_UNAVAILABLE),
],
@@ -71,6 +72,7 @@ async def test_init_success(
(
CONFIG_ENTRY_DATA,
[
mock_response(MODEL_AND_VERSION_RESPONSE),
mock_response(MODEL_AND_VERSION_RESPONSE),
mock_response_error(HTTPStatus.INTERNAL_SERVER_ERROR),
],
@@ -123,7 +125,7 @@ async def test_fix_unique_id(
) -> None:
"""Test fix of a config entry with no unique id."""
responses.insert(0, mock_json_response(WIFI_PARAMS_RESPONSE))
responses.insert(1, mock_json_response(WIFI_PARAMS_RESPONSE))
entries = hass.config_entries.async_entries(DOMAIN)
assert len(entries) == 1
@@ -181,7 +183,7 @@ async def test_fix_unique_id_failure(
) -> None:
"""Test a failure during fix of a config entry with no unique id."""
responses.insert(0, initial_response)
responses.insert(1, initial_response)
await hass.config_entries.async_setup(config_entry.entry_id)
# Config entry is loaded, but not updated
@@ -212,11 +214,16 @@ async def test_fix_unique_id_duplicate(
)
other_entry.add_to_hass(hass)
# Responses for the second config entry. This first fetches wifi params
# to repair the unique id.
responses_copy = [*responses]
responses.append(mock_json_response(WIFI_PARAMS_RESPONSE))
responses.extend(responses_copy)
# Responses for the second config entry.
#
# `pyrainbird.async_client.create_controller` probes by calling
# `get_model_and_version()`, then `_async_fix_unique_id` fetches wifi params.
responses.extend(
[
mock_response(MODEL_AND_VERSION_RESPONSE),
mock_json_response(WIFI_PARAMS_RESPONSE),
]
)
await hass.config_entries.async_setup(config_entry.entry_id)
assert config_entry.state is ConfigEntryState.LOADED
@@ -451,10 +458,16 @@ async def test_fix_duplicate_device_ids(
assert device_entry.disabled_by == expected_disabled_by
@pytest.mark.parametrize(
("config_entry_data", "config_entry_unique_id"),
[(None, None)],
ids=["no_default_entry"],
)
async def test_reload_migration_with_leading_zero_mac(
hass: HomeAssistant,
device_registry: dr.DeviceRegistry,
entity_registry: er.EntityRegistry,
responses: list[AiohttpClientMockResponse],
) -> None:
"""Test migration and reload of a device with a mac address with a leading zero."""
mac_address = "01:02:03:04:05:06"
@@ -474,6 +487,10 @@ async def test_reload_migration_with_leading_zero_mac(
)
config_entry.add_to_hass(hass)
# This test sets up and then reloads the config entry, so we need a second
# copy of the default response sequence.
responses.extend([*responses])
# Create a device and entity with the old unique id format
device_entry = device_registry.async_get_or_create(
config_entry_id=config_entry.entry_id,

View File

@@ -152,7 +152,7 @@ async def test_no_unique_id(
"""Test number platform with no unique id."""
# Failure to migrate config entry to a unique id
responses.insert(0, mock_response_error(HTTPStatus.SERVICE_UNAVAILABLE))
responses.insert(1, mock_response_error(HTTPStatus.SERVICE_UNAVAILABLE))
await hass.config_entries.async_setup(config_entry.entry_id)
assert config_entry.state is ConfigEntryState.LOADED

View File

@@ -82,7 +82,7 @@ async def test_sensor_no_unique_id(
"""Test sensor platform with no unique id."""
# Failure to migrate config entry to a unique id
responses.insert(0, mock_response_error(HTTPStatus.SERVICE_UNAVAILABLE))
responses.insert(1, mock_response_error(HTTPStatus.SERVICE_UNAVAILABLE))
await hass.config_entries.async_setup(config_entry.entry_id)
assert config_entry.state is ConfigEntryState.LOADED

View File

@@ -293,7 +293,7 @@ async def test_no_unique_id(
"""Test an irrigation switch with no unique id due to migration failure."""
# Failure to migrate config entry to a unique id
responses.insert(0, mock_response_error(HTTPStatus.SERVICE_UNAVAILABLE))
responses.insert(1, mock_response_error(HTTPStatus.SERVICE_UNAVAILABLE))
await hass.config_entries.async_setup(config_entry.entry_id)
assert config_entry.state is ConfigEntryState.LOADED

View File

@@ -145,3 +145,31 @@ async def test_remove_router_reconnect(
entity = entity_registry.async_get("button.mock_title_reconnect_zigbee_router")
assert entity is None
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
async def test_router_button_with_3_radios(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
mock_config_entry: MockConfigEntry,
mock_smlight_client: MagicMock,
) -> None:
"""Test creation of router buttons for device with 3 radios."""
mock_smlight_client.get_info.side_effect = None
mock_smlight_client.get_info.return_value = Info(
MAC="AA:BB:CC:DD:EE:FF",
radios=[
Radio(zb_type=0, chip_index=0),
Radio(zb_type=1, chip_index=1),
Radio(zb_type=0, chip_index=2),
],
)
await setup_integration(hass, mock_config_entry)
entities = er.async_entries_for_config_entry(
entity_registry, mock_config_entry.entry_id
)
assert len(entities) == 4
entity = entity_registry.async_get("button.mock_title_reconnect_zigbee_router")
assert entity is not None

View File

@@ -7,9 +7,12 @@ from syrupy.assertion import SnapshotAssertion
from homeassistant.components.media_player import (
ATTR_GROUP_MEMBERS,
ATTR_INPUT_SOURCE,
DOMAIN as MEDIA_PLAYER_DOMAIN,
SERVICE_JOIN,
SERVICE_SELECT_SOURCE,
SERVICE_UNJOIN,
MediaPlayerState,
)
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import ATTR_ENTITY_ID
@@ -175,3 +178,116 @@ async def test_state_stream_not_found(
state = hass.states.get("media_player.test_client_1_snapcast_client")
assert state.state == "off"
async def test_attributes_group_is_none(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_create_server: AsyncMock,
mock_client_1: AsyncMock,
) -> None:
"""Test exceptions are not thrown when a client has no group."""
# Force nonexistent group
mock_client_1.group = None
# Setup and verify the integration is loaded
with patch("secrets.token_hex", return_value="mock_token"):
await setup_integration(hass, mock_config_entry)
assert mock_config_entry.state is ConfigEntryState.LOADED
state = hass.states.get("media_player.test_client_1_snapcast_client")
# Assert accessing state and attributes doesn't throw
assert state.state == MediaPlayerState.IDLE
assert state.attributes["group_members"] is None
assert "source" not in state.attributes
assert "source_list" not in state.attributes
assert "metadata" not in state.attributes
assert "media_position" not in state.attributes
async def test_select_source_group_is_none(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_create_server: AsyncMock,
mock_client_1: AsyncMock,
mock_group_1: AsyncMock,
) -> None:
"""Test the select source action throws a service validation error when a client has no group."""
# Force nonexistent group
mock_client_1.group = None
# Setup and verify the integration is loaded
with patch("secrets.token_hex", return_value="mock_token"):
await setup_integration(hass, mock_config_entry)
assert mock_config_entry.state is ConfigEntryState.LOADED
with pytest.raises(ServiceValidationError):
await hass.services.async_call(
MEDIA_PLAYER_DOMAIN,
SERVICE_SELECT_SOURCE,
{
ATTR_ENTITY_ID: "media_player.test_client_1_snapcast_client",
ATTR_INPUT_SOURCE: "fake_source",
},
blocking=True,
)
mock_group_1.set_stream.assert_not_awaited()
async def test_join_group_is_none(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_create_server: AsyncMock,
mock_group_1: AsyncMock,
mock_client_1: AsyncMock,
) -> None:
"""Test join action throws a service validation error when a client has no group."""
# Force nonexistent group
mock_client_1.group = None
# Setup and verify the integration is loaded
with patch("secrets.token_hex", return_value="mock_token"):
await setup_integration(hass, mock_config_entry)
assert mock_config_entry.state is ConfigEntryState.LOADED
with pytest.raises(ServiceValidationError):
await hass.services.async_call(
MEDIA_PLAYER_DOMAIN,
SERVICE_JOIN,
{
ATTR_ENTITY_ID: "media_player.test_client_1_snapcast_client",
ATTR_GROUP_MEMBERS: ["media_player.test_client_2_snapcast_client"],
},
blocking=True,
)
mock_group_1.add_client.assert_not_awaited()
async def test_unjoin_group_is_none(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_create_server: AsyncMock,
mock_client_1: AsyncMock,
mock_group_1: AsyncMock,
) -> None:
"""Test the unjoin action throws a service validation error when a client has no group."""
# Force nonexistent group
mock_client_1.group = None
# Setup and verify the integration is loaded
with patch("secrets.token_hex", return_value="mock_token"):
await setup_integration(hass, mock_config_entry)
assert mock_config_entry.state is ConfigEntryState.LOADED
with pytest.raises(ServiceValidationError):
await hass.services.async_call(
MEDIA_PLAYER_DOMAIN,
SERVICE_UNJOIN,
{
ATTR_ENTITY_ID: "media_player.test_client_1_snapcast_client",
},
blocking=True,
)
mock_group_1.remove_client.assert_not_awaited()

View File

@@ -10,7 +10,6 @@ from spotifyaio.models import (
Artist,
Devices,
FollowedArtistResponse,
NewReleasesResponse,
NewReleasesResponseInner,
PlaybackState,
PlayedTrackResponse,
@@ -142,9 +141,6 @@ def mock_spotify() -> Generator[AsyncMock]:
client.get_followed_artists.return_value = FollowedArtistResponse.from_json(
load_fixture("followed_artists.json", DOMAIN)
).artists.items
client.get_new_releases.return_value = NewReleasesResponse.from_json(
load_fixture("new_releases.json", DOMAIN)
).albums.items
client.get_devices.return_value = Devices.from_json(
load_fixture("devices.json", DOMAIN)
).devices

View File

@@ -1,469 +0,0 @@
{
"albums": {
"href": "https://api.spotify.com/v1/browse/new-releases?offset=0&limit=20&locale=en-US,en;q%3D0.5",
"items": [
{
"album_type": "album",
"artists": [
{
"external_urls": {
"spotify": "https://open.spotify.com/artist/4gzpq5DPGxSnKTe4SA8HAU"
},
"href": "https://api.spotify.com/v1/artists/4gzpq5DPGxSnKTe4SA8HAU",
"id": "4gzpq5DPGxSnKTe4SA8HAU",
"name": "Coldplay",
"type": "artist",
"uri": "spotify:artist:4gzpq5DPGxSnKTe4SA8HAU"
}
],
"available_markets": [
"AR",
"AU",
"AT",
"BE",
"BO",
"BR",
"BG",
"CA",
"CL",
"CO",
"CR",
"CY",
"CZ",
"DK",
"DO",
"DE",
"EC",
"EE",
"SV",
"FI",
"FR",
"GR",
"GT",
"HN",
"HK",
"HU",
"IS",
"IE",
"IT",
"LV",
"LT",
"LU",
"MY",
"MT",
"MX",
"NL",
"NZ",
"NI",
"NO",
"PA",
"PY",
"PE",
"PH",
"PL",
"PT",
"SG",
"SK",
"ES",
"SE",
"CH",
"TW",
"TR",
"UY",
"US",
"GB",
"AD",
"LI",
"MC",
"ID",
"JP",
"TH",
"VN",
"RO",
"IL",
"ZA",
"SA",
"AE",
"BH",
"QA",
"OM",
"KW",
"EG",
"MA",
"DZ",
"TN",
"LB",
"JO",
"PS",
"IN",
"KZ",
"MD",
"UA",
"AL",
"BA",
"HR",
"ME",
"MK",
"RS",
"SI",
"KR",
"BD",
"PK",
"LK",
"GH",
"KE",
"NG",
"TZ",
"UG",
"AG",
"AM",
"BS",
"BB",
"BZ",
"BT",
"BW",
"BF",
"CV",
"CW",
"DM",
"FJ",
"GM",
"GE",
"GD",
"GW",
"GY",
"HT",
"JM",
"KI",
"LS",
"LR",
"MW",
"MV",
"ML",
"MH",
"FM",
"NA",
"NR",
"NE",
"PW",
"PG",
"PR",
"WS",
"SM",
"ST",
"SN",
"SC",
"SL",
"SB",
"KN",
"LC",
"VC",
"SR",
"TL",
"TO",
"TT",
"TV",
"VU",
"AZ",
"BN",
"BI",
"KH",
"CM",
"TD",
"KM",
"GQ",
"SZ",
"GA",
"GN",
"KG",
"LA",
"MO",
"MR",
"MN",
"NP",
"RW",
"TG",
"UZ",
"ZW",
"BJ",
"MG",
"MU",
"MZ",
"AO",
"CI",
"DJ",
"ZM",
"CD",
"CG",
"IQ",
"LY",
"TJ",
"VE",
"ET",
"XK"
],
"external_urls": {
"spotify": "https://open.spotify.com/album/5SGtrmYbIo0Dsg4kJ4qjM6"
},
"href": "https://api.spotify.com/v1/albums/5SGtrmYbIo0Dsg4kJ4qjM6",
"id": "5SGtrmYbIo0Dsg4kJ4qjM6",
"images": [
{
"height": 300,
"url": "https://i.scdn.co/image/ab67616d00001e0209ba52a5116e0c3e8461f58b",
"width": 300
},
{
"height": 64,
"url": "https://i.scdn.co/image/ab67616d0000485109ba52a5116e0c3e8461f58b",
"width": 64
},
{
"height": 640,
"url": "https://i.scdn.co/image/ab67616d0000b27309ba52a5116e0c3e8461f58b",
"width": 640
}
],
"name": "Moon Music",
"release_date": "2024-10-04",
"release_date_precision": "day",
"total_tracks": 10,
"type": "album",
"uri": "spotify:album:5SGtrmYbIo0Dsg4kJ4qjM6"
},
{
"album_type": "album",
"artists": [
{
"external_urls": {
"spotify": "https://open.spotify.com/artist/4U9nsRTH2mr9L4UXEWqG5e"
},
"href": "https://api.spotify.com/v1/artists/4U9nsRTH2mr9L4UXEWqG5e",
"id": "4U9nsRTH2mr9L4UXEWqG5e",
"name": "Bente",
"type": "artist",
"uri": "spotify:artist:4U9nsRTH2mr9L4UXEWqG5e"
}
],
"available_markets": [
"AR",
"AU",
"AT",
"BE",
"BO",
"BR",
"BG",
"CA",
"CL",
"CO",
"CR",
"CY",
"CZ",
"DK",
"DO",
"DE",
"EC",
"EE",
"SV",
"FI",
"FR",
"GR",
"GT",
"HN",
"HK",
"HU",
"IS",
"IE",
"IT",
"LV",
"LT",
"LU",
"MY",
"MT",
"MX",
"NL",
"NZ",
"NI",
"NO",
"PA",
"PY",
"PE",
"PH",
"PL",
"PT",
"SG",
"SK",
"ES",
"SE",
"CH",
"TW",
"TR",
"UY",
"US",
"GB",
"AD",
"LI",
"MC",
"ID",
"JP",
"TH",
"VN",
"RO",
"IL",
"ZA",
"SA",
"AE",
"BH",
"QA",
"OM",
"KW",
"EG",
"MA",
"DZ",
"TN",
"LB",
"JO",
"PS",
"IN",
"KZ",
"MD",
"UA",
"AL",
"BA",
"HR",
"ME",
"MK",
"RS",
"SI",
"KR",
"BD",
"PK",
"LK",
"GH",
"KE",
"NG",
"TZ",
"UG",
"AG",
"AM",
"BS",
"BB",
"BZ",
"BT",
"BW",
"BF",
"CV",
"CW",
"DM",
"FJ",
"GM",
"GE",
"GD",
"GW",
"GY",
"HT",
"JM",
"KI",
"LS",
"LR",
"MW",
"MV",
"ML",
"MH",
"FM",
"NA",
"NR",
"NE",
"PW",
"PG",
"WS",
"SM",
"ST",
"SN",
"SC",
"SL",
"SB",
"KN",
"LC",
"VC",
"SR",
"TL",
"TO",
"TT",
"TV",
"VU",
"AZ",
"BN",
"BI",
"KH",
"CM",
"TD",
"KM",
"GQ",
"SZ",
"GA",
"GN",
"KG",
"LA",
"MO",
"MR",
"MN",
"NP",
"RW",
"TG",
"UZ",
"ZW",
"BJ",
"MG",
"MU",
"MZ",
"AO",
"CI",
"DJ",
"ZM",
"CD",
"CG",
"IQ",
"LY",
"TJ",
"VE",
"ET",
"XK"
],
"external_urls": {
"spotify": "https://open.spotify.com/album/713lZ7AF55fEFSQgcttj9y"
},
"href": "https://api.spotify.com/v1/albums/713lZ7AF55fEFSQgcttj9y",
"id": "713lZ7AF55fEFSQgcttj9y",
"images": [
{
"height": 300,
"url": "https://i.scdn.co/image/ab67616d00001e02ab9953b1d18f8233f6b26027",
"width": 300
},
{
"height": 64,
"url": "https://i.scdn.co/image/ab67616d00004851ab9953b1d18f8233f6b26027",
"width": 64
},
{
"height": 640,
"url": "https://i.scdn.co/image/ab67616d0000b273ab9953b1d18f8233f6b26027",
"width": 640
}
],
"name": "drift",
"release_date": "2024-10-03",
"release_date_precision": "day",
"total_tracks": 14,
"type": "album",
"uri": "spotify:album:713lZ7AF55fEFSQgcttj9y"
}
],
"limit": 20,
"next": "https://api.spotify.com/v1/browse/new-releases?offset=20&limit=20&locale=en-US,en;q%3D0.5",
"offset": 0,
"previous": null,
"total": 100
}
}

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