Compare commits

...

68 Commits

Author SHA1 Message Date
Franck Nijhof 2c47e83342 2026.3.2 (#165675) 2026-03-16 13:23:27 +01:00
Franck Nijhof e3c6a2184d Bump version to 2026.3.2 2026-03-16 10:27:01 +00:00
Simone Chemelli 0ba0829350 Bump aiocomelit to 2.0.1 (#165663) 2026-03-16 10:25:08 +00:00
Allen Porter 678048e681 Upgrade ical dependency to 13.2.2. (#165642) 2026-03-16 10:25:07 +00:00
Jan Bouwhuis 743eeeae53 Fix MQTT device tracker overrides via JSON state attributes without reset (#165529) 2026-03-16 10:25:05 +00:00
Raj Laud 46555c6d9a Fix victron_ble warning sensor using duplicate alarm translation key (#165502)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-16 10:25:04 +00:00
Simone Chemelli dbaca0a723 Bump aioamazondevices to 13.0.1 (#165476) 2026-03-16 10:25:02 +00:00
Joost Lekkerkerker 9bb2959029 Bump pySmartThings to 3.7.0 (#165468) 2026-03-16 10:25:01 +00:00
Robert Resch 0304781fa9 Bump orjson to 3.11.7 (#165443) 2026-03-16 10:25:00 +00:00
J. Nick Koston e081d28aa4 Handle OAuth token request exceptions in Yale setup (#165430) 2026-03-16 10:24:58 +00:00
TheJulianJES 34aa28c72f Bump ZHA to 1.0.2 (#165423) 2026-03-16 10:24:56 +00:00
Bram Kragten cfa2946db8 Update frontend to 20260312.0 (#165420) 2026-03-16 10:24:55 +00:00
Galorhallen 1b0779347c Update govee local api to 2.4.0 (#165418) 2026-03-16 10:24:54 +00:00
Joost Lekkerkerker 93a281e7af Remove stateclass from timestamp entity in Intellifire (#165403) 2026-03-16 10:24:53 +00:00
Josef Zweck 6b32e27fd3 Bump onedrive-personal-sdk to 0.1.7 (#165401) 2026-03-16 10:24:51 +00:00
Zach Feldman 79928a8c7c August oauth2 exception migration (#165397)
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-03-16 10:24:50 +00:00
Steve Easley 9146518e13 Bump pyjvcprojector to 2.0.3 (#165327) 2026-03-16 10:24:48 +00:00
Dan Raper e9c5172f43 Bump ohme to 1.7.0 (#165318) 2026-03-16 10:24:47 +00:00
TheJulianJES cce21ad4b9 Bump python-otbr-api to 2.9.0 (#165298) 2026-03-16 10:24:46 +00:00
Simone Chemelli 10ec02ca3c Fix switch set for Vodafone Station (#165273) 2026-03-16 10:18:26 +00:00
Josef Zweck bdf54491e5 Bump onedrive-personal-sdk to 0.1.6 (#165219) 2026-03-16 10:18:25 +00:00
Bram Kragten 0b05d34238 Add reorder support to area selector (#165211) 2026-03-16 10:18:24 +00:00
Åke Strandberg 4c69a1c5f7 Add missing code for Miele dryer (#165122) 2026-03-16 10:17:00 +00:00
Steve Easley 6f1f56dcaa Bump jvc_projector dependency to 2.0.2 (#165099) 2026-03-16 10:16:59 +00:00
Jordan Harvey d0b9991232 Bump pyanglianwater to 3.1.1 (#165097) 2026-03-16 10:16:58 +00:00
Artur Pragacz aacf39be8a Make restore state resilient to extra_restore_state_data errors (#165086) 2026-03-16 10:16:56 +00:00
Erwin Douna bf055da82c Bump pyportainer to 1.0.33 (#165080) 2026-03-16 10:12:26 +00:00
Erwin Douna 0fb118bcd9 Bump pyportainer 1.0.32 (#164803) 2026-03-16 10:12:25 +00:00
Erwin Douna 954ef7d1f5 Fix forced VERIFY_SSL in Portainer (#165079) 2026-03-16 09:56:32 +00:00
Joakim Plate b091299320 Update pychromecast to 14.0.10 (#165069) 2026-03-16 09:56:31 +00:00
J. Nick Koston 52483e18b2 Bump yalexs-ble to 3.2.8 (#165018) 2026-03-16 09:56:29 +00:00
AlCalzone 57e8683ed7 Fix cover state updates for legacy Multilevel Switch based Z-Wave covers (#165003) 2026-03-16 09:56:28 +00:00
Simone Chemelli 67faace978 Fix dnd switch status for Alexa Devices (#164953) 2026-03-16 09:56:26 +00:00
Simone Chemelli e4be64fcb1 Fix wifi switch status and add 100% coverage for Fritz (#164696) 2026-03-16 09:56:25 +00:00
Franck Nijhof f552b8221f 2026.3.1 (#165001) 2026-03-06 22:10:34 +01:00
Franck Nijhof 55dc5392f9 Bump version to 2026.3.1 2026-03-06 20:37:19 +00:00
Karl Beecken 5b93aeae38 Bump teltasync to 0.2.0 (#164995) 2026-03-06 20:37:03 +00:00
Shay Levy 33610bb1a1 Bump aioswitcher to 6.1.1 (#164981) 2026-03-06 20:37:01 +00:00
Manu 6c3cebe413 Change setpoint step size in IronOS integration (#164979) 2026-03-06 20:37:00 +00:00
Willem-Jan van Rootselaar 5346895d9b Bump python-bsblan to 5.1.2 (#164963) 2026-03-06 20:36:58 +00:00
Willem-Jan van Rootselaar 05c3f08c6c Bump python-bsblan to 5.1.1 (#164591) 2026-03-06 20:36:57 +00:00
Daniel Hjelseth Høyer 1ce025733d Fix energy unit in Homevolt (#164959)
Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>
2026-03-06 20:35:22 +00:00
Simone Chemelli 1537ea86b8 Bump aiovodafone to 3.1.3 (#164955) 2026-03-06 20:35:21 +00:00
Luke Lashley ec137870fa Pass in Base Url during Roborock reauth (#164903) 2026-03-06 20:35:20 +00:00
Josef Zweck 816ee7f53e Bump onedrive-personal-sdk to 0.1.5 (#164880) 2026-03-06 20:35:18 +00:00
Petro31 6e7eeec827 Fix 'this' variable in template options flow (#164866) 2026-03-06 20:35:17 +00:00
Marc Mueller d100477a22 Fix volvo test RuntimeWarning (#164845) 2026-03-06 20:35:16 +00:00
Matthias Alphart 98ac6dd2c1 Fix KNX sensor default attributes for energy and volume DPTs (#164838)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-06 20:35:14 +00:00
John O'Nolan 6b30969f60 Fix Ghost config flow using wrong field name for site UUID (#164836) 2026-03-06 20:35:13 +00:00
Joshua Leaper e9a6b5d662 Update ness_alarm scan interval to 5 secs (#164835) 2026-03-06 20:35:11 +00:00
Glenn de Haan f95f3f9982 Add device class to active_liter_lpm sensor (#164809) 2026-03-06 20:35:10 +00:00
epenet 3f884a8cd1 Remove caio from licenses exception list (#164806) 2026-03-06 20:35:09 +00:00
Raphael Hehl 10f284932e Enforce SSRF redirect protection only for connector allowed_protocol_schema_set (#164769)
Co-authored-by: RaHehl <rahehl@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-03-06 20:35:07 +00:00
Sean O'Keeffe e1c4e6dc42 more programs for Miele steam ovens (#164768)
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-03-06 20:35:06 +00:00
Ian Foster 0976e7de4e Update keyboard_remote dependencies (#164755) 2026-03-06 20:35:05 +00:00
Antonio Mello ae1012b2f0 Fix IntesisHome outdoor_temp not reported when value is 0.0 (#164703)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 20:35:03 +00:00
TimL bb7c4faca5 Fix button entity creation for devices with more than two radios (#164699) 2026-03-06 20:35:02 +00:00
Tucker Kern 0b1be61336 Ensure Snapcast client has a valid current group before accessing group attributes. (#164683) 2026-03-06 20:35:00 +00:00
Glenn Waters 3ec44024a2 Hunter Douglas Powerview: Fix missing class in hierarchy. (#164264)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-03-06 20:34:59 +00:00
Joost Lekkerkerker 1200cc5779 Bump spotifyaio to 2.0.2 (#164114)
Co-authored-by: Robert Resch <robert@resch.dev>
2026-03-06 20:34:58 +00:00
Blake Messer d632931f74 Fix Rain Bird controllers updated by Rain Bird 2.x (#163915) 2026-03-06 20:34:56 +00:00
Franck Nijhof 2f9faa53a1 2026.3.0 (#164757) 2026-03-04 20:17:05 +01:00
Joost Lekkerkerker 718607a758 Revert "Add diagnostics platform to AWS S3 (#164118)" (#164759) 2026-03-04 19:01:47 +01:00
Franck Nijhof 3789156559 Revert "Add diagnostics platform to AWS S3 (#164118)"
This reverts commit 37d2c946e8.
2026-03-04 17:53:29 +00:00
Franck Nijhof 042ce6f2de Bump version to 2026.3.0 2026-03-04 17:30:58 +00:00
Franck Nijhof 0a5908002f Bump version to 2026.3.0b4 2026-03-04 17:09:32 +00:00
Petro31 3a5f71e10a Fix this variable preview issue with template entities from the UI (#164740) 2026-03-04 17:09:18 +00:00
rappenze 04e4b05ab0 Fix handling of several thermostat QuickApp's in fibaro (#164344)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-04 17:09:17 +00:00
121 changed files with 3195 additions and 1156 deletions
@@ -1,6 +1,5 @@
"""Defines a base Alexa Devices entity.""" """Defines a base Alexa Devices entity."""
from aioamazondevices.const.devices import SPEAKER_GROUP_DEVICE_TYPE
from aioamazondevices.structures import AmazonDevice from aioamazondevices.structures import AmazonDevice
from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.device_registry import DeviceInfo
@@ -25,20 +24,15 @@ class AmazonEntity(CoordinatorEntity[AmazonDevicesCoordinator]):
"""Initialize the entity.""" """Initialize the entity."""
super().__init__(coordinator) super().__init__(coordinator)
self._serial_num = serial_num self._serial_num = serial_num
model = self.device.model
self._attr_device_info = DeviceInfo( self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, serial_num)}, identifiers={(DOMAIN, serial_num)},
name=self.device.account_name, name=self.device.account_name,
model=model, model=self.device.model,
model_id=self.device.device_type, model_id=self.device.device_type,
manufacturer=self.device.manufacturer or "Amazon", manufacturer=self.device.manufacturer or "Amazon",
hw_version=self.device.hardware_version, hw_version=self.device.hardware_version,
sw_version=( sw_version=self.device.software_version,
self.device.software_version serial_number=serial_num,
if model != SPEAKER_GROUP_DEVICE_TYPE
else None
),
serial_number=serial_num if model != SPEAKER_GROUP_DEVICE_TYPE else None,
) )
self.entity_description = description self.entity_description = description
self._attr_unique_id = f"{serial_num}-{description.key}" self._attr_unique_id = f"{serial_num}-{description.key}"
@@ -8,5 +8,5 @@
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["aioamazondevices"], "loggers": ["aioamazondevices"],
"quality_scale": "platinum", "quality_scale": "platinum",
"requirements": ["aioamazondevices==13.0.0"] "requirements": ["aioamazondevices==13.0.1"]
} }
@@ -101,7 +101,10 @@ class AmazonSwitchEntity(AmazonEntity, SwitchEntity):
assert method is not None assert method is not None
await method(self.device, state) 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: async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the switch on.""" """Turn the switch on."""
@@ -9,5 +9,5 @@
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["pyanglianwater"], "loggers": ["pyanglianwater"],
"quality_scale": "bronze", "quality_scale": "bronze",
"requirements": ["pyanglianwater==3.1.0"] "requirements": ["pyanglianwater==3.1.1"]
} }
+15 -3
View File
@@ -5,7 +5,7 @@ from __future__ import annotations
from pathlib import Path from pathlib import Path
from typing import cast from typing import cast
from aiohttp import ClientResponseError from aiohttp import ClientError
from yalexs.exceptions import AugustApiAIOHTTPError from yalexs.exceptions import AugustApiAIOHTTPError
from yalexs.manager.exceptions import CannotConnect, InvalidAuth, RequireValidation from yalexs.manager.exceptions import CannotConnect, InvalidAuth, RequireValidation
from yalexs.manager.gateway import Config as YaleXSConfig 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.config_entries import ConfigEntry
from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from homeassistant.core import HomeAssistant 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 import device_registry as dr, issue_registry as ir
from homeassistant.helpers.config_entry_oauth2_flow import ( from homeassistant.helpers.config_entry_oauth2_flow import (
ImplementationUnavailableError, 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) august_gateway = AugustGateway(Path(hass.config.config_dir), session, oauth_session)
try: try:
await async_setup_august(hass, entry, august_gateway) await async_setup_august(hass, entry, august_gateway)
except OAuth2TokenRequestReauthError as err:
raise ConfigEntryAuthFailed from err
except (RequireValidation, InvalidAuth) as err: except (RequireValidation, InvalidAuth) as err:
raise ConfigEntryAuthFailed from err raise ConfigEntryAuthFailed from err
except TimeoutError as err: except TimeoutError as err:
raise ConfigEntryNotReady("Timed out connecting to august api") from 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 raise ConfigEntryNotReady from err
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True return True
@@ -30,5 +30,5 @@
"integration_type": "hub", "integration_type": "hub",
"iot_class": "cloud_push", "iot_class": "cloud_push",
"loggers": ["pubnub", "yalexs"], "loggers": ["pubnub", "yalexs"],
"requirements": ["yalexs==9.2.0", "yalexs-ble==3.2.7"] "requirements": ["yalexs==9.2.0", "yalexs-ble==3.2.8"]
} }
@@ -1,55 +0,0 @@
"""Diagnostics support for AWS S3."""
from __future__ import annotations
import dataclasses
from typing import Any
from homeassistant.components.backup import (
DATA_MANAGER as BACKUP_DATA_MANAGER,
BackupManager,
)
from homeassistant.components.diagnostics import async_redact_data
from homeassistant.core import HomeAssistant
from .const import (
CONF_ACCESS_KEY_ID,
CONF_BUCKET,
CONF_PREFIX,
CONF_SECRET_ACCESS_KEY,
DOMAIN,
)
from .coordinator import S3ConfigEntry
from .helpers import async_list_backups_from_s3
TO_REDACT = (CONF_ACCESS_KEY_ID, CONF_SECRET_ACCESS_KEY)
async def async_get_config_entry_diagnostics(
hass: HomeAssistant,
entry: S3ConfigEntry,
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
coordinator = entry.runtime_data
backup_manager: BackupManager = hass.data[BACKUP_DATA_MANAGER]
backups = await async_list_backups_from_s3(
coordinator.client,
bucket=entry.data[CONF_BUCKET],
prefix=entry.data.get(CONF_PREFIX, ""),
)
data = {
"coordinator_data": dataclasses.asdict(coordinator.data),
"config": {
**entry.data,
**entry.options,
},
"backup_agents": [
{"name": agent.name}
for agent in backup_manager.backup_agents.values()
if agent.domain == DOMAIN
],
"backup": [backup.as_dict() for backup in backups],
}
return async_redact_data(data, TO_REDACT)
@@ -43,7 +43,7 @@ rules:
# Gold # Gold
devices: done devices: done
diagnostics: done diagnostics: todo
discovery-update-info: discovery-update-info:
status: exempt status: exempt
comment: S3 is a cloud service that is not discovered on the network. comment: S3 is a cloud service that is not discovered on the network.
@@ -8,7 +8,7 @@
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["bsblan"], "loggers": ["bsblan"],
"quality_scale": "silver", "quality_scale": "silver",
"requirements": ["python-bsblan==5.1.0"], "requirements": ["python-bsblan==5.1.2"],
"zeroconf": [ "zeroconf": [
{ {
"name": "bsb-lan*", "name": "bsb-lan*",
+1 -1
View File
@@ -15,7 +15,7 @@
"integration_type": "hub", "integration_type": "hub",
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["casttube", "pychromecast"], "loggers": ["casttube", "pychromecast"],
"requirements": ["PyChromecast==14.0.9"], "requirements": ["PyChromecast==14.0.10"],
"single_config_entry": true, "single_config_entry": true,
"zeroconf": ["_googlecast._tcp.local."] "zeroconf": ["_googlecast._tcp.local."]
} }
@@ -8,5 +8,5 @@
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["aiocomelit"], "loggers": ["aiocomelit"],
"quality_scale": "platinum", "quality_scale": "platinum",
"requirements": ["aiocomelit==2.0.0"] "requirements": ["aiocomelit==2.0.1"]
} }
+5 -2
View File
@@ -275,8 +275,11 @@ class FibaroController:
# otherwise add the first visible device in the group # otherwise add the first visible device in the group
# which is a hack, but solves a problem with FGT having # which is a hack, but solves a problem with FGT having
# hidden compatibility devices before the real device # hidden compatibility devices before the real device
if last_climate_parent != device.parent_fibaro_id or ( # Second hack is for quickapps which have parent id 0 and no children
device.has_endpoint_id and last_endpoint != device.endpoint_id if (
last_climate_parent != device.parent_fibaro_id
or (device.has_endpoint_id and last_endpoint != device.endpoint_id)
or device.parent_fibaro_id == 0
): ):
_LOGGER.debug("Handle separately") _LOGGER.debug("Handle separately")
self.fibaro_devices[platform].append(device) self.fibaro_devices[platform].append(device)
+12 -18
View File
@@ -133,26 +133,20 @@ async def _async_wifi_entities_list(
] ]
) )
_LOGGER.debug("WiFi networks count: %s", wifi_count) _LOGGER.debug("WiFi networks count: %s", wifi_count)
networks: dict = {} networks: dict[int, dict[str, Any]] = {}
for i in range(1, wifi_count + 1): for i in range(1, wifi_count + 1):
network_info = await avm_wrapper.async_get_wlan_configuration(i) network_info = await avm_wrapper.async_get_wlan_configuration(i)
# Devices with 4 WLAN services, use the 2nd for internal communications # Devices with 4 WLAN services, use the 2nd for internal communications
if not (wifi_count == 4 and i == 2): if not (wifi_count == 4 and i == 2):
networks[i] = { networks[i] = network_info
"ssid": network_info["NewSSID"],
"bssid": network_info["NewBSSID"],
"standard": network_info["NewStandard"],
"enabled": network_info["NewEnable"],
"status": network_info["NewStatus"],
}
for i, network in networks.copy().items(): for i, network in networks.copy().items():
networks[i]["switch_name"] = network["ssid"] networks[i]["switch_name"] = network["NewSSID"]
if ( if (
len( len(
[ [
j j
for j, n in networks.items() for j, n in networks.items()
if slugify(n["ssid"]) == slugify(network["ssid"]) if slugify(n["NewSSID"]) == slugify(network["NewSSID"])
] ]
) )
> 1 > 1
@@ -434,13 +428,11 @@ class FritzBoxPortSwitch(FritzBoxBaseSwitch):
for key, attr in attributes_dict.items(): for key, attr in attributes_dict.items():
self._attributes[attr] = self.port_mapping[key] 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" self.port_mapping["NewEnabled"] = "1" if turn_on else "0"
await self._avm_wrapper.async_add_port_mapping(
resp = await self._avm_wrapper.async_add_port_mapping(
self.connection_type, self.port_mapping self.connection_type, self.port_mapping
) )
return bool(resp is not None)
class FritzBoxDeflectionSwitch(FritzBoxBaseCoordinatorSwitch): class FritzBoxDeflectionSwitch(FritzBoxBaseCoordinatorSwitch):
@@ -525,12 +517,11 @@ class FritzBoxProfileSwitch(FritzDeviceBase, SwitchEntity):
"""Turn off switch.""" """Turn off switch."""
await self._async_handle_turn_on_off(turn_on=False) 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.""" """Handle switch state change request."""
await self._avm_wrapper.async_set_allow_wan_access(self.ip_address, turn_on) 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._avm_wrapper.devices[self._mac].wan_access = turn_on
self.async_write_ha_state() self.async_write_ha_state()
return True
class FritzBoxWifiSwitch(FritzBoxBaseSwitch): class FritzBoxWifiSwitch(FritzBoxBaseSwitch):
@@ -541,10 +532,11 @@ class FritzBoxWifiSwitch(FritzBoxBaseSwitch):
avm_wrapper: AvmWrapper, avm_wrapper: AvmWrapper,
device_friendly_name: str, device_friendly_name: str,
network_num: int, network_num: int,
network_data: dict, network_data: dict[str, Any],
) -> None: ) -> None:
"""Init Fritz Wifi switch.""" """Init Fritz Wifi switch."""
self._avm_wrapper = avm_wrapper self._avm_wrapper = avm_wrapper
self._wifi_info = network_data
self._attributes = {} self._attributes = {}
self._attr_entity_category = EntityCategory.CONFIG self._attr_entity_category = EntityCategory.CONFIG
@@ -560,7 +552,7 @@ class FritzBoxWifiSwitch(FritzBoxBaseSwitch):
type=SWITCH_TYPE_WIFINETWORK, type=SWITCH_TYPE_WIFINETWORK,
callback_update=self._async_fetch_update, callback_update=self._async_fetch_update,
callback_switch=self._async_switch_on_off_executor, 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) super().__init__(self._avm_wrapper, device_friendly_name, switch_info)
@@ -587,7 +579,9 @@ class FritzBoxWifiSwitch(FritzBoxBaseSwitch):
self._attributes["mac_address_control"] = wifi_info[ self._attributes["mac_address_control"] = wifi_info[
"NewMACAddressControlEnabled" "NewMACAddressControlEnabled"
] ]
self._wifi_info = wifi_info
async def _async_switch_on_off_executor(self, turn_on: bool) -> None: async def _async_switch_on_off_executor(self, turn_on: bool) -> None:
"""Handle wifi switch.""" """Handle wifi switch."""
self._wifi_info["NewEnable"] = turn_on
await self._avm_wrapper.async_set_wlan_configuration(self._network_num, turn_on) await self._avm_wrapper.async_set_wlan_configuration(self._network_num, turn_on)
@@ -21,5 +21,5 @@
"integration_type": "system", "integration_type": "system",
"preview_features": { "winter_mode": {} }, "preview_features": { "winter_mode": {} },
"quality_scale": "internal", "quality_scale": "internal",
"requirements": ["home-assistant-frontend==20260304.0"] "requirements": ["home-assistant-frontend==20260312.0"]
} }
@@ -89,7 +89,7 @@ class GhostConfigFlow(ConfigFlow, domain=DOMAIN):
site_title = site["title"] 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() self._abort_if_unique_id_configured()
return self.async_create_entry( return self.async_create_entry(
@@ -8,5 +8,5 @@
"integration_type": "service", "integration_type": "service",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["googleapiclient"], "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"]
} }
@@ -6,5 +6,5 @@
"dependencies": ["network"], "dependencies": ["network"],
"documentation": "https://www.home-assistant.io/integrations/govee_light_local", "documentation": "https://www.home-assistant.io/integrations/govee_light_local",
"iot_class": "local_push", "iot_class": "local_push",
"requirements": ["govee-local-api==2.3.0"] "requirements": ["govee-local-api==2.4.0"]
} }
+2 -2
View File
@@ -91,14 +91,14 @@ SENSORS: tuple[SensorEntityDescription, ...] = (
translation_key="energy_exported", translation_key="energy_exported",
device_class=SensorDeviceClass.ENERGY, device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING, state_class=SensorStateClass.TOTAL_INCREASING,
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
), ),
SensorEntityDescription( SensorEntityDescription(
key="energy_imported", key="energy_imported",
translation_key="energy_imported", translation_key="energy_imported",
device_class=SensorDeviceClass.ENERGY, device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING, state_class=SensorStateClass.TOTAL_INCREASING,
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
), ),
SensorEntityDescription( SensorEntityDescription(
key="frequency", key="frequency",
@@ -610,6 +610,7 @@ SENSORS: Final[tuple[HomeWizardSensorEntityDescription, ...]] = (
key="active_liter_lpm", key="active_liter_lpm",
translation_key="active_liter_lpm", translation_key="active_liter_lpm",
native_unit_of_measurement=UnitOfVolumeFlowRate.LITERS_PER_MINUTE, native_unit_of_measurement=UnitOfVolumeFlowRate.LITERS_PER_MINUTE,
device_class=SensorDeviceClass.VOLUME_FLOW_RATE,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
has_fn=lambda data: data.measurement.active_liter_lpm is not None, has_fn=lambda data: data.measurement.active_liter_lpm is not None,
value_fn=lambda data: data.measurement.active_liter_lpm, value_fn=lambda data: data.measurement.active_liter_lpm,
@@ -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. """Represent a shade that has a front sheer and rear opaque panel.
This equates to two shades being controlled by one motor. This equates to two shades being controlled by one motor.
@@ -915,26 +917,6 @@ class PowerViewShadeDualOverlappedCombinedTilt(PowerViewShadeDualOverlappedCombi
Type 10 - Duolite with 180° Tilt 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 @property
def transition_steps(self) -> int: def transition_steps(self) -> int:
"""Return the steps to make a move.""" """Return the steps to make a move."""
@@ -949,26 +931,6 @@ class PowerViewShadeDualOverlappedCombinedTilt(PowerViewShadeDualOverlappedCombi
tilt = self.positions.tilt tilt = self.positions.tilt
return ceil(primary + secondary + 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 = { TYPE_TO_CLASSES = {
0: (PowerViewShade,), 0: (PowerViewShade,),
@@ -97,7 +97,6 @@ INTELLIFIRE_SENSORS: tuple[IntellifireSensorEntityDescription, ...] = (
IntellifireSensorEntityDescription( IntellifireSensorEntityDescription(
key="timer_end_timestamp", key="timer_end_timestamp",
translation_key="timer_end_timestamp", translation_key="timer_end_timestamp",
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.TIMESTAMP, device_class=SensorDeviceClass.TIMESTAMP,
value_fn=_time_remaining_to_timestamp, value_fn=_time_remaining_to_timestamp,
), ),
@@ -221,13 +221,13 @@ class IntesisAC(ClimateEntity):
def extra_state_attributes(self) -> dict[str, Any]: def extra_state_attributes(self) -> dict[str, Any]:
"""Return the device specific state attributes.""" """Return the device specific state attributes."""
attrs = {} attrs = {}
if self._outdoor_temp: if self._outdoor_temp is not None:
attrs["outdoor_temp"] = self._outdoor_temp 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( attrs["power_consumption_heat_kw"] = round(
self._power_consumption_heat / 1000, 1 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( attrs["power_consumption_cool_kw"] = round(
self._power_consumption_cool / 1000, 1 self._power_consumption_cool / 1000, 1
) )
@@ -244,7 +244,7 @@ class IntesisAC(ClimateEntity):
if hvac_mode := kwargs.get(ATTR_HVAC_MODE): if hvac_mode := kwargs.get(ATTR_HVAC_MODE):
await self.async_set_hvac_mode(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) _LOGGER.debug("Setting %s to %s degrees", self._device_type, temperature)
await self._controller.set_temperature(self._device_id, temperature) await self._controller.set_temperature(self._device_id, temperature)
self._attr_target_temperature = 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]) 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 # 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( await self._controller.set_temperature(
self._device_id, self._attr_target_temperature self._device_id, self._attr_target_temperature
) )
+1 -1
View File
@@ -358,7 +358,7 @@ PINECIL_SETPOINT_NUMBER_DESCRIPTION = IronOSNumberEntityDescription(
native_max_value=MAX_TEMP, native_max_value=MAX_TEMP,
native_min_value_f=MIN_TEMP_F, native_min_value_f=MIN_TEMP_F,
native_max_value_f=MAX_TEMP_F, native_max_value_f=MAX_TEMP_F,
native_step=5, native_step=1,
) )
@@ -7,5 +7,5 @@
"integration_type": "device", "integration_type": "device",
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["jvcprojector"], "loggers": ["jvcprojector"],
"requirements": ["pyjvcprojector==2.0.1"] "requirements": ["pyjvcprojector==2.0.3"]
} }
@@ -7,5 +7,5 @@
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["aionotify", "evdev"], "loggers": ["aionotify", "evdev"],
"quality_scale": "legacy", "quality_scale": "legacy",
"requirements": ["evdev==1.6.1", "asyncinotify==4.2.0"] "requirements": ["evdev==1.9.3", "asyncinotify==4.4.0"]
} }
+23 -11
View File
@@ -8,6 +8,7 @@ from xknx.dpt import DPTBase, DPTComplex, DPTEnum, DPTNumeric
from xknx.dpt.dpt_16 import DPTString from xknx.dpt.dpt_16 import DPTString
from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass
from homeassistant.const import UnitOfReactiveEnergy
HaDptClass = Literal["numeric", "enum", "complex", "string"] 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 main=dpt_class.dpt_main_number, # type: ignore[typeddict-item] # checked in xknx unit tests
sub=dpt_class.dpt_sub_number, sub=dpt_class.dpt_sub_number,
name=dpt_class.value_type, 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_device_class=_sensor_device_classes.get(dpt_number_str),
sensor_state_class=_get_sensor_state_class(ha_dpt_class, 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.1200": SensorDeviceClass.VOLUME,
"12.1201": SensorDeviceClass.VOLUME, "12.1201": SensorDeviceClass.VOLUME,
"13.002": SensorDeviceClass.VOLUME_FLOW_RATE, "13.002": SensorDeviceClass.VOLUME_FLOW_RATE,
"13.010": SensorDeviceClass.ENERGY, "13.010": SensorDeviceClass.ENERGY, # DPTActiveEnergy
"13.012": SensorDeviceClass.REACTIVE_ENERGY, "13.012": SensorDeviceClass.REACTIVE_ENERGY, # DPTReactiveEnergy
"13.013": SensorDeviceClass.ENERGY, "13.013": SensorDeviceClass.ENERGY, # DPTActiveEnergykWh
"13.015": SensorDeviceClass.REACTIVE_ENERGY, "13.015": SensorDeviceClass.REACTIVE_ENERGY, # DPTReactiveEnergykVARh
"13.016": SensorDeviceClass.ENERGY, "13.016": SensorDeviceClass.ENERGY, # DPTActiveEnergyMWh
"13.1200": SensorDeviceClass.VOLUME, "13.1200": SensorDeviceClass.VOLUME, # DPTDeltaVolumeLiquidLitre
"13.1201": SensorDeviceClass.VOLUME, "13.1201": SensorDeviceClass.VOLUME, # DPTDeltaVolumeM3
"14.010": SensorDeviceClass.AREA, "14.010": SensorDeviceClass.AREA,
"14.019": SensorDeviceClass.CURRENT, "14.019": SensorDeviceClass.CURRENT,
"14.027": SensorDeviceClass.VOLTAGE, "14.027": SensorDeviceClass.VOLTAGE,
@@ -91,7 +92,7 @@ _sensor_device_classes: Mapping[str, SensorDeviceClass] = {
"14.030": SensorDeviceClass.VOLTAGE, "14.030": SensorDeviceClass.VOLTAGE,
"14.031": SensorDeviceClass.ENERGY, "14.031": SensorDeviceClass.ENERGY,
"14.033": SensorDeviceClass.FREQUENCY, "14.033": SensorDeviceClass.FREQUENCY,
"14.037": SensorDeviceClass.ENERGY_STORAGE, "14.037": SensorDeviceClass.ENERGY_STORAGE, # DPTHeatQuantity
"14.039": SensorDeviceClass.DISTANCE, "14.039": SensorDeviceClass.DISTANCE,
"14.051": SensorDeviceClass.WEIGHT, "14.051": SensorDeviceClass.WEIGHT,
"14.056": SensorDeviceClass.POWER, "14.056": SensorDeviceClass.POWER,
@@ -101,7 +102,7 @@ _sensor_device_classes: Mapping[str, SensorDeviceClass] = {
"14.068": SensorDeviceClass.TEMPERATURE, "14.068": SensorDeviceClass.TEMPERATURE,
"14.069": SensorDeviceClass.TEMPERATURE, "14.069": SensorDeviceClass.TEMPERATURE,
"14.070": SensorDeviceClass.TEMPERATURE_DELTA, "14.070": SensorDeviceClass.TEMPERATURE_DELTA,
"14.076": SensorDeviceClass.VOLUME, "14.076": SensorDeviceClass.VOLUME, # DPTVolume
"14.077": SensorDeviceClass.VOLUME_FLOW_RATE, "14.077": SensorDeviceClass.VOLUME_FLOW_RATE,
"14.080": SensorDeviceClass.APPARENT_POWER, "14.080": SensorDeviceClass.APPARENT_POWER,
"14.1200": SensorDeviceClass.VOLUME_FLOW_RATE, "14.1200": SensorDeviceClass.VOLUME_FLOW_RATE,
@@ -121,17 +122,28 @@ _sensor_state_class_overrides: Mapping[str, SensorStateClass | None] = {
"13.010": SensorStateClass.TOTAL, # DPTActiveEnergy "13.010": SensorStateClass.TOTAL, # DPTActiveEnergy
"13.011": SensorStateClass.TOTAL, # DPTApparantEnergy "13.011": SensorStateClass.TOTAL, # DPTApparantEnergy
"13.012": SensorStateClass.TOTAL, # DPTReactiveEnergy "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.007": SensorStateClass.MEASUREMENT_ANGLE, # DPTAngleDeg
"14.037": SensorStateClass.TOTAL, # DPTHeatQuantity
"14.051": SensorStateClass.TOTAL, # DPTMass "14.051": SensorStateClass.TOTAL, # DPTMass
"14.055": SensorStateClass.MEASUREMENT_ANGLE, # DPTPhaseAngleDeg "14.055": SensorStateClass.MEASUREMENT_ANGLE, # DPTPhaseAngleDeg
"14.031": SensorStateClass.TOTAL_INCREASING, # DPTEnergy "14.031": SensorStateClass.TOTAL_INCREASING, # DPTEnergy
"14.076": SensorStateClass.TOTAL, # DPTVolume
"17.001": None, # DPTSceneNumber "17.001": None, # DPTSceneNumber
"29.010": SensorStateClass.TOTAL, # DPTActiveEnergy8Byte "29.010": SensorStateClass.TOTAL, # DPTActiveEnergy8Byte
"29.011": SensorStateClass.TOTAL, # DPTApparantEnergy8Byte "29.011": SensorStateClass.TOTAL, # DPTApparantEnergy8Byte
"29.012": SensorStateClass.TOTAL, # DPTReactiveEnergy8Byte "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( def _get_sensor_state_class(
ha_dpt_class: HaDptClass, dpt_number_str: str ha_dpt_class: HaDptClass, dpt_number_str: str
@@ -7,5 +7,5 @@
"documentation": "https://www.home-assistant.io/integrations/local_calendar", "documentation": "https://www.home-assistant.io/integrations/local_calendar",
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["ical"], "loggers": ["ical"],
"requirements": ["ical==13.2.0"] "requirements": ["ical==13.2.2"]
} }
@@ -5,5 +5,5 @@
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/local_todo", "documentation": "https://www.home-assistant.io/integrations/local_todo",
"iot_class": "local_polling", "iot_class": "local_polling",
"requirements": ["ical==13.2.0"] "requirements": ["ical==13.2.2"]
} }
+118 -132
View File
@@ -188,6 +188,7 @@ class ProgramPhaseTumbleDryer(MieleEnum, missing_to_none=True):
finished = 522, 11012 finished = 522, 11012
extra_dry = 523 extra_dry = 523
hand_iron = 524 hand_iron = 524
hygiene_drying = 525
moisten = 526 moisten = 526
thermo_spin = 527 thermo_spin = 527
timed_drying = 528 timed_drying = 528
@@ -617,11 +618,11 @@ class OvenProgramId(MieleEnum, missing_to_none=True):
evaporate_water = 327 evaporate_water = 327
shabbat_program = 335 shabbat_program = 335
yom_tov = 336 yom_tov = 336
drying = 357 drying = 357, 2028
heat_crockery = 358 heat_crockery = 358
prove_dough = 359 prove_dough = 359, 2023
low_temperature_cooking = 360 low_temperature_cooking = 360
steam_cooking = 361 steam_cooking = 8, 361
keeping_warm = 362 keeping_warm = 362
apple_sponge = 364 apple_sponge = 364
apple_pie = 365 apple_pie = 365
@@ -668,9 +669,9 @@ class OvenProgramId(MieleEnum, missing_to_none=True):
saddle_of_roebuck = 456 saddle_of_roebuck = 456
salmon_fillet = 461 salmon_fillet = 461
potato_cheese_gratin = 464 potato_cheese_gratin = 464
trout = 486 trout = 486, 2224
carp = 491 carp = 491, 2233
salmon_trout = 492 salmon_trout = 492, 2241
springform_tin_15cm = 496 springform_tin_15cm = 496
springform_tin_20cm = 497 springform_tin_20cm = 497
springform_tin_25cm = 498 springform_tin_25cm = 498
@@ -736,137 +737,15 @@ class OvenProgramId(MieleEnum, missing_to_none=True):
pork_belly = 701 pork_belly = 701
pikeperch_fillet_with_vegetables = 702 pikeperch_fillet_with_vegetables = 702
steam_bake = 99001 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 sous_vide = 72
eco_steam_cooking = 75 eco_steam_cooking = 75
rapid_steam_cooking = 77 rapid_steam_cooking = 77
descale = 326
menu_cooking = 330 menu_cooking = 330
reheating_with_steam = 2018 reheating_with_steam = 2018
defrosting_with_steam = 2019 defrosting_with_steam = 2019
blanching = 2020 blanching = 2020
bottling = 2021 bottling = 2021
sterilize_crockery = 2022 sterilize_crockery = 2022
prove_dough = 2023
soak = 2027 soak = 2027
reheating_with_microwave = 2029 reheating_with_microwave = 2029
defrosting_with_microwave = 2030 defrosting_with_microwave = 2030
@@ -1020,18 +899,15 @@ class SteamOvenMicroProgramId(MieleEnum, missing_to_none=True):
gilt_head_bream_fillet = 2220 gilt_head_bream_fillet = 2220
codfish_piece = 2221, 2232 codfish_piece = 2221, 2232
codfish_fillet = 2222, 2231 codfish_fillet = 2222, 2231
trout = 2224
pike_fillet = 2225 pike_fillet = 2225
pike_piece = 2226 pike_piece = 2226
halibut_fillet_2_cm = 2227 halibut_fillet_2_cm = 2227
halibut_fillet_3_cm = 2230 halibut_fillet_3_cm = 2230
carp = 2233
salmon_fillet_2_cm = 2234 salmon_fillet_2_cm = 2234
salmon_fillet_3_cm = 2235 salmon_fillet_3_cm = 2235
salmon_steak_2_cm = 2238 salmon_steak_2_cm = 2238
salmon_steak_3_cm = 2239 salmon_steak_3_cm = 2239
salmon_piece = 2240 salmon_piece = 2240
salmon_trout = 2241
iridescent_shark_fillet = 2244 iridescent_shark_fillet = 2244
red_snapper_fillet_2_cm = 2245 red_snapper_fillet_2_cm = 2245
red_snapper_fillet_3_cm = 2248 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 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]] = { PROGRAM_IDS: dict[int, type[MieleEnum]] = {
MieleAppliance.WASHING_MACHINE: WashingMachineProgramId, MieleAppliance.WASHING_MACHINE: WashingMachineProgramId,
MieleAppliance.TUMBLE_DRYER: TumbleDryerProgramId, MieleAppliance.TUMBLE_DRYER: TumbleDryerProgramId,
@@ -1278,7 +1264,7 @@ PROGRAM_IDS: dict[int, type[MieleEnum]] = {
MieleAppliance.STEAM_OVEN_MK2: OvenProgramId, MieleAppliance.STEAM_OVEN_MK2: OvenProgramId,
MieleAppliance.STEAM_OVEN: OvenProgramId, MieleAppliance.STEAM_OVEN: OvenProgramId,
MieleAppliance.STEAM_OVEN_COMBI: OvenProgramId, MieleAppliance.STEAM_OVEN_COMBI: OvenProgramId,
MieleAppliance.STEAM_OVEN_MICRO: SteamOvenMicroProgramId, MieleAppliance.STEAM_OVEN_MICRO: OvenProgramId,
MieleAppliance.WASHER_DRYER: WashingMachineProgramId, MieleAppliance.WASHER_DRYER: WashingMachineProgramId,
MieleAppliance.ROBOT_VACUUM_CLEANER: RobotVacuumCleanerProgramId, MieleAppliance.ROBOT_VACUUM_CLEANER: RobotVacuumCleanerProgramId,
MieleAppliance.COFFEE_SYSTEM: CoffeeSystemProgramId, MieleAppliance.COFFEE_SYSTEM: CoffeeSystemProgramId,
@@ -474,6 +474,7 @@
"drain_spin": "Drain/spin", "drain_spin": "Drain/spin",
"drop_cookies_1_tray": "Drop cookies (1 tray)", "drop_cookies_1_tray": "Drop cookies (1 tray)",
"drop_cookies_2_trays": "Drop cookies (2 trays)", "drop_cookies_2_trays": "Drop cookies (2 trays)",
"drying": "Drying",
"duck": "Duck", "duck": "Duck",
"dutch_hash": "Dutch hash", "dutch_hash": "Dutch hash",
"easy_care": "Easy care", "easy_care": "Easy care",
@@ -1005,6 +1006,7 @@
"heating_up_phase": "Heating up phase", "heating_up_phase": "Heating up phase",
"hot_milk": "Hot milk", "hot_milk": "Hot milk",
"hygiene": "Hygiene", "hygiene": "Hygiene",
"hygiene_drying": "Hygiene drying",
"interim_rinse": "Interim rinse", "interim_rinse": "Interim rinse",
"keep_warm": "Keep warm", "keep_warm": "Keep warm",
"keeping_warm": "Keeping warm", "keeping_warm": "Keeping warm",
@@ -163,8 +163,6 @@ class MqttDeviceTracker(MqttEntity, TrackerEntity):
latitude: float | None latitude: float | None
longitude: float | None longitude: float | None
gps_accuracy: float gps_accuracy: float
# Reset manually set location to allow automatic zone detection
self._attr_location_name = None
if isinstance( if isinstance(
latitude := extra_state_attributes.get(ATTR_LATITUDE), (int, float) latitude := extra_state_attributes.get(ATTR_LATITUDE), (int, float)
) and isinstance( ) and isinstance(
+1 -1
View File
@@ -24,7 +24,7 @@ SUBENTRY_TYPE_ZONE = "zone"
# Defaults # Defaults
DEFAULT_PORT = 4999 DEFAULT_PORT = 4999
DEFAULT_SCAN_INTERVAL = timedelta(minutes=1) DEFAULT_SCAN_INTERVAL = timedelta(seconds=5)
DEFAULT_INFER_ARMING_STATE = False DEFAULT_INFER_ARMING_STATE = False
DEFAULT_ZONE_TYPE = BinarySensorDeviceClass.MOTION DEFAULT_ZONE_TYPE = BinarySensorDeviceClass.MOTION
@@ -19,6 +19,5 @@ async def async_get_config_entry_diagnostics(
return { return {
"device_info": client.device_info, "device_info": client.device_info,
"vehicles": client.vehicles, "vehicles": client.vehicles,
"ct_connected": client.ct_connected,
"cap_available": client.cap_available, "cap_available": client.cap_available,
} }
+1 -1
View File
@@ -7,5 +7,5 @@
"integration_type": "device", "integration_type": "device",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"quality_scale": "platinum", "quality_scale": "platinum",
"requirements": ["ohme==1.6.0"] "requirements": ["ohme==1.7.0"]
} }
@@ -10,5 +10,5 @@
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["onedrive_personal_sdk"], "loggers": ["onedrive_personal_sdk"],
"quality_scale": "platinum", "quality_scale": "platinum",
"requirements": ["onedrive-personal-sdk==0.1.4"] "requirements": ["onedrive-personal-sdk==0.1.7"]
} }
@@ -10,5 +10,5 @@
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["onedrive_personal_sdk"], "loggers": ["onedrive_personal_sdk"],
"quality_scale": "platinum", "quality_scale": "platinum",
"requirements": ["onedrive-personal-sdk==0.1.4"] "requirements": ["onedrive-personal-sdk==0.1.7"]
} }
+1 -1
View File
@@ -8,5 +8,5 @@
"documentation": "https://www.home-assistant.io/integrations/otbr", "documentation": "https://www.home-assistant.io/integrations/otbr",
"integration_type": "service", "integration_type": "service",
"iot_class": "local_polling", "iot_class": "local_polling",
"requirements": ["python-otbr-api==2.8.0"] "requirements": ["python-otbr-api==2.9.0"]
} }
@@ -159,8 +159,12 @@ class PortainerConfigFlow(ConfigFlow, domain=DOMAIN):
_LOGGER.exception("Unexpected exception") _LOGGER.exception("Unexpected exception")
errors["base"] = "unknown" errors["base"] = "unknown"
else: else:
await self.async_set_unique_id(user_input[CONF_API_TOKEN]) # Logic that can be reverted back once the new unique ID is in
self._abort_if_unique_id_configured() 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( return self.async_update_reload_and_abort(
reconf_entry, reconf_entry,
data_updates={ data_updates={
@@ -7,5 +7,5 @@
"integration_type": "service", "integration_type": "service",
"iot_class": "local_polling", "iot_class": "local_polling",
"quality_scale": "bronze", "quality_scale": "bronze",
"requirements": ["pyportainer==1.0.31"] "requirements": ["pyportainer==1.0.33"]
} }
+16 -7
View File
@@ -2,11 +2,12 @@
from __future__ import annotations from __future__ import annotations
import asyncio
import logging import logging
from typing import Any from typing import Any
import aiohttp import aiohttp
from pyrainbird.async_client import AsyncRainbirdController, CreateController from pyrainbird.async_client import AsyncRainbirdController, create_controller
from pyrainbird.exceptions import RainbirdApiException, RainbirdAuthException from pyrainbird.exceptions import RainbirdApiException, RainbirdAuthException
from homeassistant.const import ( from homeassistant.const import (
@@ -26,7 +27,7 @@ from homeassistant.helpers import (
from homeassistant.helpers.device_registry import format_mac from homeassistant.helpers.device_registry import format_mac
from homeassistant.helpers.typing import ConfigType 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 ( from .coordinator import (
RainbirdScheduleUpdateCoordinator, RainbirdScheduleUpdateCoordinator,
RainbirdUpdateCoordinator, RainbirdUpdateCoordinator,
@@ -77,11 +78,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: RainbirdConfigEntry) ->
clientsession = async_create_clientsession() clientsession = async_create_clientsession()
_async_register_clientsession_shutdown(hass, entry, clientsession) _async_register_clientsession_shutdown(hass, entry, clientsession)
controller = CreateController( try:
clientsession, async with asyncio.timeout(TIMEOUT_SECONDS):
entry.data[CONF_HOST], controller = await create_controller(
entry.data[CONF_PASSWORD], 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)): if not (await _async_fix_unique_id(hass, controller, entry)):
return False return False
@@ -7,7 +7,7 @@ from collections.abc import Mapping
import logging import logging
from typing import Any from typing import Any
from pyrainbird.async_client import CreateController from pyrainbird.async_client import create_controller
from pyrainbird.data import WifiParams from pyrainbird.data import WifiParams
from pyrainbird.exceptions import RainbirdApiException, RainbirdAuthException from pyrainbird.exceptions import RainbirdApiException, RainbirdAuthException
import voluptuous as vol import voluptuous as vol
@@ -137,9 +137,9 @@ class RainbirdConfigFlowHandler(ConfigFlow, domain=DOMAIN):
Raises a ConfigFlowError on failure. Raises a ConfigFlowError on failure.
""" """
clientsession = async_create_clientsession() clientsession = async_create_clientsession()
controller = CreateController(clientsession, host, password)
try: try:
async with asyncio.timeout(TIMEOUT_SECONDS): async with asyncio.timeout(TIMEOUT_SECONDS):
controller = await create_controller(clientsession, host, password)
return await asyncio.gather( return await asyncio.gather(
controller.get_serial_number(), controller.get_serial_number(),
controller.get_wifi_params(), controller.get_wifi_params(),
@@ -8,5 +8,5 @@
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["ical"], "loggers": ["ical"],
"quality_scale": "silver", "quality_scale": "silver",
"requirements": ["ical==13.2.0"] "requirements": ["ical==13.2.2"]
} }
@@ -188,7 +188,9 @@ class RoborockFlowHandler(ConfigFlow, domain=DOMAIN):
self._username = entry_data[CONF_USERNAME] self._username = entry_data[CONF_USERNAME]
assert self._username assert self._username
self._client = RoborockApiClient( 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() return await self.async_step_reauth_confirm()
@@ -34,5 +34,5 @@
"iot_class": "cloud_push", "iot_class": "cloud_push",
"loggers": ["pysmartthings"], "loggers": ["pysmartthings"],
"quality_scale": "bronze", "quality_scale": "bronze",
"requirements": ["pysmartthings==3.6.0"] "requirements": ["pysmartthings==3.7.0"]
} }
+1 -1
View File
@@ -74,7 +74,7 @@ async def async_setup_entry(
radios = coordinator.data.info.radios radios = coordinator.data.info.radios
async_add_entities(SmButton(coordinator, button) for button in BUTTONS) async_add_entities(SmButton(coordinator, button) for button in BUTTONS)
entity_created = [False, False] entity_created = [False] * len(radios)
@callback @callback
def _check_router(startup: bool = False) -> None: def _check_router(startup: bool = False) -> None:
@@ -131,7 +131,7 @@ class SnapcastClientDevice(SnapcastCoordinatorEntity, MediaPlayerEntity):
return f"{CLIENT_PREFIX}{host}_{id}" return f"{CLIENT_PREFIX}{host}_{id}"
@property @property
def _current_group(self) -> Snapgroup: def _current_group(self) -> Snapgroup | None:
"""Return the group the client is associated with.""" """Return the group the client is associated with."""
return self._device.group return self._device.group
@@ -158,12 +158,17 @@ class SnapcastClientDevice(SnapcastCoordinatorEntity, MediaPlayerEntity):
def state(self) -> MediaPlayerState | None: def state(self) -> MediaPlayerState | None:
"""Return the state of the player.""" """Return the state of the player."""
if self._device.connected: 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 return MediaPlayerState.IDLE
try: try:
return STREAM_STATUS.get(self._current_group.stream_status) return STREAM_STATUS.get(self._current_group.stream_status)
except KeyError: except KeyError:
pass pass
return MediaPlayerState.OFF return MediaPlayerState.OFF
@property @property
@@ -182,15 +187,31 @@ class SnapcastClientDevice(SnapcastCoordinatorEntity, MediaPlayerEntity):
@property @property
def source(self) -> str | None: def source(self) -> str | None:
"""Return the current input source.""" """Return the current input source."""
if self._current_group is None:
return None
return self._current_group.stream return self._current_group.stream
@property @property
def source_list(self) -> list[str]: def source_list(self) -> list[str]:
"""List of available input sources.""" """List of available input sources."""
if self._current_group is None:
return []
return list(self._current_group.streams_by_name().keys()) return list(self._current_group.streams_by_name().keys())
async def async_select_source(self, source: str) -> None: async def async_select_source(self, source: str) -> None:
"""Set input source.""" """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() streams = self._current_group.streams_by_name()
if source in streams: if source in streams:
await self._current_group.set_stream(streams[source].identifier) await self._current_group.set_stream(streams[source].identifier)
@@ -233,6 +254,9 @@ class SnapcastClientDevice(SnapcastCoordinatorEntity, MediaPlayerEntity):
@property @property
def group_members(self) -> list[str] | None: def group_members(self) -> list[str] | None:
"""List of player entities which are currently grouped together for synchronous playback.""" """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) entity_registry = er.async_get(self.hass)
return [ return [
entity_id entity_id
@@ -248,6 +272,15 @@ class SnapcastClientDevice(SnapcastCoordinatorEntity, MediaPlayerEntity):
async def async_join_players(self, group_members: list[str]) -> None: async def async_join_players(self, group_members: list[str]) -> None:
"""Add `group_members` to this client's current group.""" """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 # Get the client entity for each group member excluding self
entity_registry = er.async_get(self.hass) entity_registry = er.async_get(self.hass)
clients = [ clients = [
@@ -271,13 +304,25 @@ class SnapcastClientDevice(SnapcastCoordinatorEntity, MediaPlayerEntity):
self.async_write_ha_state() self.async_write_ha_state()
async def async_unjoin_player(self) -> None: 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) await self._current_group.remove_client(self._device.identifier)
self.async_write_ha_state() self.async_write_ha_state()
@property @property
def metadata(self) -> Mapping[str, Any]: def metadata(self) -> Mapping[str, Any]:
"""Get metadata from the current stream.""" """Get metadata from the current stream."""
if self._current_group is None:
return {}
try: try:
if metadata := self.coordinator.server.stream( if metadata := self.coordinator.server.stream(
self._current_group.stream self._current_group.stream
@@ -341,6 +386,9 @@ class SnapcastClientDevice(SnapcastCoordinatorEntity, MediaPlayerEntity):
@property @property
def media_position(self) -> int | None: def media_position(self) -> int | None:
"""Position of current playing media in seconds.""" """Position of current playing media in seconds."""
if self._current_group is None:
return None
try: try:
# Position is part of properties object, not metadata object # Position is part of properties object, not metadata object
if properties := self.coordinator.server.stream( if properties := self.coordinator.server.stream(
@@ -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": { "services": {
"restore": { "restore": {
"description": "Restores a previously taken snapshot of a media player.", "description": "Restores a previously taken snapshot of a media player.",
@@ -118,7 +118,6 @@ class BrowsableMedia(StrEnum):
CURRENT_USER_RECENTLY_PLAYED = "current_user_recently_played" CURRENT_USER_RECENTLY_PLAYED = "current_user_recently_played"
CURRENT_USER_TOP_ARTISTS = "current_user_top_artists" CURRENT_USER_TOP_ARTISTS = "current_user_top_artists"
CURRENT_USER_TOP_TRACKS = "current_user_top_tracks" CURRENT_USER_TOP_TRACKS = "current_user_top_tracks"
NEW_RELEASES = "new_releases"
LIBRARY_MAP = { LIBRARY_MAP = {
@@ -130,7 +129,6 @@ LIBRARY_MAP = {
BrowsableMedia.CURRENT_USER_RECENTLY_PLAYED.value: "Recently played", BrowsableMedia.CURRENT_USER_RECENTLY_PLAYED.value: "Recently played",
BrowsableMedia.CURRENT_USER_TOP_ARTISTS.value: "Top Artists", BrowsableMedia.CURRENT_USER_TOP_ARTISTS.value: "Top Artists",
BrowsableMedia.CURRENT_USER_TOP_TRACKS.value: "Top Tracks", BrowsableMedia.CURRENT_USER_TOP_TRACKS.value: "Top Tracks",
BrowsableMedia.NEW_RELEASES.value: "New Releases",
} }
CONTENT_TYPE_MEDIA_CLASS: dict[str, Any] = { CONTENT_TYPE_MEDIA_CLASS: dict[str, Any] = {
@@ -166,10 +164,6 @@ CONTENT_TYPE_MEDIA_CLASS: dict[str, Any] = {
"parent": MediaClass.DIRECTORY, "parent": MediaClass.DIRECTORY,
"children": MediaClass.TRACK, "children": MediaClass.TRACK,
}, },
BrowsableMedia.NEW_RELEASES.value: {
"parent": MediaClass.DIRECTORY,
"children": MediaClass.ALBUM,
},
MediaType.PLAYLIST: { MediaType.PLAYLIST: {
"parent": MediaClass.PLAYLIST, "parent": MediaClass.PLAYLIST,
"children": MediaClass.TRACK, "children": MediaClass.TRACK,
@@ -356,14 +350,11 @@ async def build_item_response( # noqa: C901
elif media_content_type == BrowsableMedia.CURRENT_USER_TOP_TRACKS: elif media_content_type == BrowsableMedia.CURRENT_USER_TOP_TRACKS:
if top_tracks := await spotify.get_top_tracks(): if top_tracks := await spotify.get_top_tracks():
items = [_get_track_item_payload(track) for track in 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: elif media_content_type == MediaType.PLAYLIST:
if playlist := await spotify.get_playlist(media_content_id): if playlist := await spotify.get_playlist(media_content_id):
title = playlist.name title = playlist.name
image = playlist.images[0].url if playlist.images else None 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 playlist_item.track.type is ItemType.TRACK:
if TYPE_CHECKING: if TYPE_CHECKING:
assert isinstance(playlist_item.track, Track) assert isinstance(playlist_item.track, Track)
@@ -6,7 +6,7 @@ from collections.abc import Mapping
import logging import logging
from typing import Any from typing import Any
from spotifyaio import SpotifyClient from spotifyaio import SpotifyClient, SpotifyForbiddenError
from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlowResult from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlowResult
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_NAME, CONF_TOKEN from homeassistant.const import CONF_ACCESS_TOKEN, CONF_NAME, CONF_TOKEN
@@ -41,6 +41,9 @@ class SpotifyFlowHandler(
try: try:
current_user = await spotify.get_current_user() 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: except Exception:
self.logger.exception("Error while connecting to Spotify") self.logger.exception("Error while connecting to Spotify")
return self.async_abort(reason="connection_error") return self.async_abort(reason="connection_error")
@@ -11,12 +11,15 @@ from spotifyaio import (
Playlist, Playlist,
SpotifyClient, SpotifyClient,
SpotifyConnectionError, SpotifyConnectionError,
SpotifyForbiddenError,
SpotifyNotFoundError, SpotifyNotFoundError,
UserProfile, UserProfile,
) )
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant 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.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from homeassistant.util import dt as dt_util from homeassistant.util import dt as dt_util
@@ -33,6 +36,11 @@ type SpotifyConfigEntry = ConfigEntry[SpotifyData]
UPDATE_INTERVAL = timedelta(seconds=30) UPDATE_INTERVAL = timedelta(seconds=30)
FREE_API_BLOGPOST = (
"https://developer.spotify.com/blog/"
"2026-02-06-update-on-developer-access-and-platform-security"
)
@dataclass @dataclass
class SpotifyCoordinatorData: class SpotifyCoordinatorData:
@@ -78,6 +86,19 @@ class SpotifyCoordinator(DataUpdateCoordinator[SpotifyCoordinatorData]):
"""Set up the coordinator.""" """Set up the coordinator."""
try: try:
self.current_user = await self.client.get_current_user() 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: except SpotifyConnectionError as err:
raise UpdateFailed("Error communicating with Spotify API") from err raise UpdateFailed("Error communicating with Spotify API") from err
@@ -8,5 +8,5 @@
"integration_type": "service", "integration_type": "service",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["spotifyaio"], "loggers": ["spotifyaio"],
"requirements": ["spotifyaio==1.0.0"] "requirements": ["spotifyaio==2.0.2"]
} }
@@ -14,10 +14,10 @@ from spotifyaio import (
Item, Item,
ItemType, ItemType,
PlaybackState, PlaybackState,
ProductType,
RepeatMode as SpotifyRepeatMode, RepeatMode as SpotifyRepeatMode,
Track, Track,
) )
from spotifyaio.models import ProductType
from yarl import URL from yarl import URL
from homeassistant.components.media_player import ( from homeassistant.components.media_player import (
@@ -222,7 +222,7 @@ class SpotifyMediaPlayer(SpotifyEntity, MediaPlayerEntity):
if item.type == ItemType.EPISODE: if item.type == ItemType.EPISODE:
if TYPE_CHECKING: if TYPE_CHECKING:
assert isinstance(item, Episode) assert isinstance(item, Episode)
return item.show.publisher return item.show.name
if TYPE_CHECKING: if TYPE_CHECKING:
assert isinstance(item, Track) assert isinstance(item, Track)
@@ -230,12 +230,10 @@ class SpotifyMediaPlayer(SpotifyEntity, MediaPlayerEntity):
@property @property
@ensure_item @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.""" """Return the media album."""
if item.type == ItemType.EPISODE: if item.type == ItemType.EPISODE:
if TYPE_CHECKING: return None
assert isinstance(item, Episode)
return item.show.name
if TYPE_CHECKING: if TYPE_CHECKING:
assert isinstance(item, Track) assert isinstance(item, Track)
@@ -12,7 +12,8 @@
"oauth_timeout": "[%key:common::config_flow::abort::oauth2_timeout%]", "oauth_timeout": "[%key:common::config_flow::abort::oauth2_timeout%]",
"oauth_unauthorized": "[%key:common::config_flow::abort::oauth2_unauthorized%]", "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_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": { "create_entry": {
"default": "Successfully authenticated with Spotify." "default": "Successfully authenticated with Spotify."
@@ -41,6 +42,12 @@
"message": "[%key:common::exceptions::oauth2_implementation_unavailable::message%]" "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": { "system_health": {
"info": { "info": {
"api_endpoint_reachable": "Spotify API endpoint reachable" "api_endpoint_reachable": "Spotify API endpoint reachable"
@@ -8,6 +8,6 @@
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["aioswitcher"], "loggers": ["aioswitcher"],
"quality_scale": "silver", "quality_scale": "silver",
"requirements": ["aioswitcher==6.1.0"], "requirements": ["aioswitcher==6.1.1"],
"single_config_entry": true "single_config_entry": true
} }
@@ -15,5 +15,5 @@
"integration_type": "device", "integration_type": "device",
"iot_class": "local_polling", "iot_class": "local_polling",
"quality_scale": "silver", "quality_scale": "silver",
"requirements": ["teltasync==0.1.3"] "requirements": ["teltasync==0.2.0"]
} }
@@ -29,7 +29,7 @@ from homeassistant.core import (
) )
from homeassistant.exceptions import TemplateError from homeassistant.exceptions import TemplateError
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity, async_generate_entity_id
from homeassistant.helpers.event import ( from homeassistant.helpers.event import (
TrackTemplate, TrackTemplate,
TrackTemplateResult, TrackTemplateResult,
@@ -264,16 +264,30 @@ class TemplateEntity(AbstractTemplateEntity):
return None return None
return cast(str, self._blueprint_inputs[CONF_USE_BLUEPRINT][CONF_PATH]) return cast(str, self._blueprint_inputs[CONF_USE_BLUEPRINT][CONF_PATH])
def _get_this_variable(self) -> TemplateStateFromEntityId:
"""Create a this variable for the entity."""
entity_id = self.entity_id
if self._preview_callback:
# During config flow, the registry entry and entity_id will be None. In this scenario,
# a temporary entity_id is created.
# During option flow, the preview entity_id will be None, however the registry entry
# will contain the target entity_id.
if self.registry_entry:
entity_id = self.registry_entry.entity_id
else:
entity_id = async_generate_entity_id(
self._entity_id_format, self._attr_name or "preview", hass=self.hass
)
return TemplateStateFromEntityId(self.hass, entity_id)
def _render_script_variables(self) -> dict[str, Any]: def _render_script_variables(self) -> dict[str, Any]:
"""Render configured variables.""" """Render configured variables."""
if isinstance(self._run_variables, dict): if isinstance(self._run_variables, dict):
return self._run_variables return self._run_variables
return self._run_variables.async_render( return self._run_variables.async_render(
self.hass, self.hass, {"this": self._get_this_variable()}
{
"this": TemplateStateFromEntityId(self.hass, self.entity_id),
},
) )
def setup_state_template( def setup_state_template(
@@ -451,7 +465,7 @@ class TemplateEntity(AbstractTemplateEntity):
has_availability_template = False has_availability_template = False
variables = { variables = {
"this": TemplateStateFromEntityId(self.hass, self.entity_id), "this": self._get_this_variable(),
**self._render_script_variables(), **self._render_script_variables(),
} }
@@ -7,7 +7,7 @@
"documentation": "https://www.home-assistant.io/integrations/thread", "documentation": "https://www.home-assistant.io/integrations/thread",
"integration_type": "service", "integration_type": "service",
"iot_class": "local_polling", "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, "single_config_entry": true,
"zeroconf": ["_meshcop._udp.local."] "zeroconf": ["_meshcop._udp.local."]
} }
@@ -81,6 +81,7 @@ clean_area:
selector: selector:
area: area:
multiple: true multiple: true
reorder: true
send_command: send_command:
target: target:
@@ -398,7 +398,7 @@ SENSOR_DESCRIPTIONS = {
Keys.WARNING: VictronBLESensorEntityDescription( Keys.WARNING: VictronBLESensorEntityDescription(
key=Keys.WARNING, key=Keys.WARNING,
device_class=SensorDeviceClass.ENUM, device_class=SensorDeviceClass.ENUM,
translation_key="alarm", translation_key="warning",
options=ALARM_OPTIONS, options=ALARM_OPTIONS,
), ),
Keys.YIELD_TODAY: VictronBLESensorEntityDescription( Keys.YIELD_TODAY: VictronBLESensorEntityDescription(
@@ -248,7 +248,24 @@
"name": "[%key:component::victron_ble::common::starter_voltage%]" "name": "[%key:component::victron_ble::common::starter_voltage%]"
}, },
"warning": { "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": { "yield_today": {
"name": "Yield today" "name": "Yield today"
@@ -78,6 +78,7 @@ class VodafoneStationRouter(DataUpdateCoordinator[UpdateCoordinatorDataType]):
data, data,
session, session,
) )
self._session = session
# Last resort as no MAC or S/N can be retrieved via API # Last resort as no MAC or S/N can be retrieved via API
self._id = config_entry.unique_id 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) _LOGGER.debug("Polling Vodafone Station host: %s", self.api.base_url.host)
try: 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() raw_data_devices = await self.api.get_devices_data()
data_sensors = await self.api.get_sensor_data() data_sensors = await self.api.get_sensor_data()
data_wifi = await self.api.get_wifi_data() data_wifi = await self.api.get_wifi_data()
await self.api.logout()
except exceptions.CannotAuthenticate as err: except exceptions.CannotAuthenticate as err:
raise ConfigEntryAuthFailed( raise ConfigEntryAuthFailed(
translation_domain=DOMAIN, translation_domain=DOMAIN,
@@ -8,5 +8,5 @@
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["aiovodafone"], "loggers": ["aiovodafone"],
"quality_scale": "platinum", "quality_scale": "platinum",
"requirements": ["aiovodafone==3.1.2"] "requirements": ["aiovodafone==3.1.3"]
} }
@@ -104,6 +104,7 @@ class VodafoneSwitchEntity(CoordinatorEntity[VodafoneStationRouter], SwitchEntit
await self.coordinator.api.set_wifi_status( await self.coordinator.api.set_wifi_status(
status, self.entity_description.typology, self.entity_description.band status, self.entity_description.typology, self.entity_description.band
) )
await self.coordinator.async_request_refresh()
except CannotAuthenticate as err: except CannotAuthenticate as err:
self.coordinator.config_entry.async_start_reauth(self.hass) self.coordinator.config_entry.async_start_reauth(self.hass)
raise HomeAssistantError( raise HomeAssistantError(
+15 -3
View File
@@ -5,7 +5,7 @@ from __future__ import annotations
from pathlib import Path from pathlib import Path
from typing import cast from typing import cast
from aiohttp import ClientResponseError from aiohttp import ClientError
from yalexs.const import Brand from yalexs.const import Brand
from yalexs.exceptions import YaleApiError from yalexs.exceptions import YaleApiError
from yalexs.manager.const import CONF_BRAND 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.config_entries import ConfigEntry
from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from homeassistant.core import HomeAssistant 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 import device_registry as dr
from homeassistant.helpers.config_entry_oauth2_flow import ( from homeassistant.helpers.config_entry_oauth2_flow import (
ImplementationUnavailableError, 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) yale_gateway = YaleGateway(Path(hass.config.config_dir), session, oauth_session)
try: try:
await async_setup_yale(hass, entry, yale_gateway) await async_setup_yale(hass, entry, yale_gateway)
except OAuth2TokenRequestReauthError as err:
raise ConfigEntryAuthFailed from err
except (RequireValidation, InvalidAuth) as err: except (RequireValidation, InvalidAuth) as err:
raise ConfigEntryAuthFailed from err raise ConfigEntryAuthFailed from err
except TimeoutError as err: except TimeoutError as err:
raise ConfigEntryNotReady("Timed out connecting to yale api") from 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 raise ConfigEntryNotReady from err
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True return True
+1 -1
View File
@@ -14,5 +14,5 @@
"integration_type": "hub", "integration_type": "hub",
"iot_class": "cloud_push", "iot_class": "cloud_push",
"loggers": ["socketio", "engineio", "yalexs"], "loggers": ["socketio", "engineio", "yalexs"],
"requirements": ["yalexs==9.2.0", "yalexs-ble==3.2.7"] "requirements": ["yalexs==9.2.0", "yalexs-ble==3.2.8"]
} }
@@ -13,5 +13,5 @@
"documentation": "https://www.home-assistant.io/integrations/yalexs_ble", "documentation": "https://www.home-assistant.io/integrations/yalexs_ble",
"integration_type": "device", "integration_type": "device",
"iot_class": "local_push", "iot_class": "local_push",
"requirements": ["yalexs-ble==3.2.7"] "requirements": ["yalexs-ble==3.2.8"]
} }
+1 -1
View File
@@ -23,7 +23,7 @@
"universal_silabs_flasher", "universal_silabs_flasher",
"serialx" "serialx"
], ],
"requirements": ["zha==1.0.1", "serialx==0.6.2"], "requirements": ["zha==1.0.2", "serialx==0.6.2"],
"usb": [ "usb": [
{ {
"description": "*2652*", "description": "*2652*",
+18 -6
View File
@@ -87,6 +87,9 @@ class CoverPositionMixin(ZWaveBaseEntity, CoverEntity):
_current_position_value: ZwaveValue | None = None _current_position_value: ZwaveValue | None = None
_target_position_value: ZwaveValue | None = None _target_position_value: ZwaveValue | None = None
_stop_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( def _set_position_values(
self, self,
@@ -153,12 +156,19 @@ class CoverPositionMixin(ZWaveBaseEntity, CoverEntity):
if not self._attr_is_opening and not self._attr_is_closing: if not self._attr_is_opening and not self._attr_is_closing:
return return
if ( if (current := self._current_position_value) is None or current.value is None:
(current := self._current_position_value) is not None return
and (target := self._target_position_value) is not None
and current.value is not None # Prefer the Z-Wave targetValue property when the device reports it.
and current.value == target.value # 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_opening = False
self._attr_is_closing = False self._attr_is_closing = False
@@ -203,6 +213,8 @@ class CoverPositionMixin(ZWaveBaseEntity, CoverEntity):
else: else:
return return
self._commanded_target_position = target_position
self.async_write_ha_state() self.async_write_ha_state()
async def async_set_cover_position(self, **kwargs: Any) -> None: async def async_set_cover_position(self, **kwargs: Any) -> None:
+1 -1
View File
@@ -17,7 +17,7 @@ if TYPE_CHECKING:
APPLICATION_NAME: Final = "HomeAssistant" APPLICATION_NAME: Final = "HomeAssistant"
MAJOR_VERSION: Final = 2026 MAJOR_VERSION: Final = 2026
MINOR_VERSION: Final = 3 MINOR_VERSION: Final = 3
PATCH_VERSION: Final = "0b3" PATCH_VERSION: Final = "2"
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__: Final = f"{__short_version__}.{PATCH_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 14, 2) REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 14, 2)
+6
View File
@@ -87,6 +87,12 @@ async def _ssrf_redirect_middleware(
# Relative redirects stay on the same host - always safe # Relative redirects stay on the same host - always safe
return resp 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 host = redirect_url.host
if await _async_is_blocked_host(host, connector): if await _async_is_blocked_host(host, connector):
resp.close() resp.close()
+34 -11
View File
@@ -181,15 +181,24 @@ class RestoreStateData:
} }
# Start with the currently registered states # Start with the currently registered states
stored_states = [ stored_states: list[StoredState] = []
StoredState( for entity_id, entity in self.entities.items():
current_states_by_entity_id[entity_id], if entity_id not in current_states_by_entity_id:
entity.extra_restore_state_data, continue
now, 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 expiration_time = now - STATE_EXPIRATION
for entity_id, stored_state in self.last_states.items(): for entity_id, stored_state in self.last_states.items():
@@ -219,6 +228,8 @@ class RestoreStateData:
) )
except HomeAssistantError as exc: except HomeAssistantError as exc:
_LOGGER.error("Error saving current states", exc_info=exc) _LOGGER.error("Error saving current states", exc_info=exc)
except Exception:
_LOGGER.exception("Unexpected error saving current states")
@callback @callback
def async_setup_dump(self, *args: Any) -> None: def async_setup_dump(self, *args: Any) -> None:
@@ -258,13 +269,15 @@ class RestoreStateData:
@callback @callback
def async_restore_entity_removed( 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: ) -> None:
"""Unregister this entity from saving state.""" """Unregister this entity from saving state."""
# When an entity is being removed from hass, store its last state. This # 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 # allows us to support state restoration if the entity is removed, then
# re-added while hass is still running. # 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, # 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. # we're going to serialize it to JSON and then re-load it.
if state is not None: if state is not None:
@@ -287,8 +300,18 @@ class RestoreEntity(Entity):
async def async_internal_will_remove_from_hass(self) -> None: async def async_internal_will_remove_from_hass(self) -> None:
"""Run when entity will be removed from hass.""" """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( 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() await super().async_internal_will_remove_from_hass()
+2
View File
@@ -301,6 +301,7 @@ class AreaSelectorConfig(BaseSelectorConfig, total=False):
entity: EntityFilterSelectorConfig | list[EntityFilterSelectorConfig] entity: EntityFilterSelectorConfig | list[EntityFilterSelectorConfig]
device: DeviceFilterSelectorConfig | list[DeviceFilterSelectorConfig] device: DeviceFilterSelectorConfig | list[DeviceFilterSelectorConfig]
multiple: bool multiple: bool
reorder: bool
@SELECTORS.register("area") @SELECTORS.register("area")
@@ -320,6 +321,7 @@ class AreaSelector(Selector[AreaSelectorConfig]):
[DEVICE_FILTER_SELECTOR_CONFIG_SCHEMA], [DEVICE_FILTER_SELECTOR_CONFIG_SCHEMA],
), ),
vol.Optional("multiple", default=False): cv.boolean, vol.Optional("multiple", default=False): cv.boolean,
vol.Optional("reorder", default=False): cv.boolean,
} }
) )
+2 -2
View File
@@ -40,7 +40,7 @@ habluetooth==5.8.0
hass-nabucasa==1.15.0 hass-nabucasa==1.15.0
hassil==3.5.0 hassil==3.5.0
home-assistant-bluetooth==1.13.1 home-assistant-bluetooth==1.13.1
home-assistant-frontend==20260304.0 home-assistant-frontend==20260312.0
home-assistant-intents==2026.3.3 home-assistant-intents==2026.3.3
httpx==0.28.1 httpx==0.28.1
ifaddr==0.2.0 ifaddr==0.2.0
@@ -48,7 +48,7 @@ Jinja2==3.1.6
lru-dict==1.3.0 lru-dict==1.3.0
mutagen==1.47.0 mutagen==1.47.0
openai==2.21.0 openai==2.21.0
orjson==3.11.5 orjson==3.11.7
packaging>=23.1 packaging>=23.1
paho-mqtt==2.1.0 paho-mqtt==2.1.0
Pillow==12.1.1 Pillow==12.1.1
+2 -2
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "homeassistant" name = "homeassistant"
version = "2026.3.0b3" version = "2026.3.2"
license = "Apache-2.0" license = "Apache-2.0"
license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"] license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"]
description = "Open-source home automation platform running on Python 3." description = "Open-source home automation platform running on Python 3."
@@ -65,7 +65,7 @@ dependencies = [
"Pillow==12.1.1", "Pillow==12.1.1",
"propcache==0.4.1", "propcache==0.4.1",
"pyOpenSSL==25.3.0", "pyOpenSSL==25.3.0",
"orjson==3.11.5", "orjson==3.11.7",
"packaging>=23.1", "packaging>=23.1",
"psutil-home-assistant==0.0.1", "psutil-home-assistant==0.0.1",
"python-slugify==8.0.4", "python-slugify==8.0.4",
+1 -1
View File
@@ -34,7 +34,7 @@ ifaddr==0.2.0
Jinja2==3.1.6 Jinja2==3.1.6
lru-dict==1.3.0 lru-dict==1.3.0
mutagen==1.47.0 mutagen==1.47.0
orjson==3.11.5 orjson==3.11.7
packaging>=23.1 packaging>=23.1
Pillow==12.1.1 Pillow==12.1.1
propcache==0.4.1 propcache==0.4.1
+22 -22
View File
@@ -47,7 +47,7 @@ PlexAPI==4.15.16
ProgettiHWSW==0.1.3 ProgettiHWSW==0.1.3
# homeassistant.components.cast # homeassistant.components.cast
PyChromecast==14.0.9 PyChromecast==14.0.10
# homeassistant.components.flume # homeassistant.components.flume
PyFlume==0.6.5 PyFlume==0.6.5
@@ -190,7 +190,7 @@ aioairzone-cloud==0.7.2
aioairzone==1.0.5 aioairzone==1.0.5
# homeassistant.components.alexa_devices # homeassistant.components.alexa_devices
aioamazondevices==13.0.0 aioamazondevices==13.0.1
# homeassistant.components.ambient_network # homeassistant.components.ambient_network
# homeassistant.components.ambient_station # homeassistant.components.ambient_station
@@ -224,7 +224,7 @@ aiobafi6==0.9.0
aiobotocore==2.21.1 aiobotocore==2.21.1
# homeassistant.components.comelit # homeassistant.components.comelit
aiocomelit==2.0.0 aiocomelit==2.0.1
# homeassistant.components.dhcp # homeassistant.components.dhcp
aiodhcpwatcher==1.2.1 aiodhcpwatcher==1.2.1
@@ -413,7 +413,7 @@ aiosteamist==1.0.1
aiostreammagic==2.13.0 aiostreammagic==2.13.0
# homeassistant.components.switcher_kis # homeassistant.components.switcher_kis
aioswitcher==6.1.0 aioswitcher==6.1.1
# homeassistant.components.syncthing # homeassistant.components.syncthing
aiosyncthing==0.7.1 aiosyncthing==0.7.1
@@ -437,7 +437,7 @@ aiousbwatcher==1.1.1
aiovlc==0.5.1 aiovlc==0.5.1
# homeassistant.components.vodafone_station # homeassistant.components.vodafone_station
aiovodafone==3.1.2 aiovodafone==3.1.3
# homeassistant.components.waqi # homeassistant.components.waqi
aiowaqi==3.1.0 aiowaqi==3.1.0
@@ -556,7 +556,7 @@ async-upnp-client==0.46.2
asyncarve==0.1.1 asyncarve==0.1.1
# homeassistant.components.keyboard_remote # homeassistant.components.keyboard_remote
asyncinotify==4.2.0 asyncinotify==4.4.0
# homeassistant.components.supla # homeassistant.components.supla
asyncpysupla==0.0.5 asyncpysupla==0.0.5
@@ -939,7 +939,7 @@ eternalegypt==0.0.18
eufylife-ble-client==0.1.8 eufylife-ble-client==0.1.8
# homeassistant.components.keyboard_remote # homeassistant.components.keyboard_remote
# evdev==1.6.1 # evdev==1.9.3
# homeassistant.components.evohome # homeassistant.components.evohome
evohome-async==1.1.3 evohome-async==1.1.3
@@ -1122,7 +1122,7 @@ gotailwind==0.3.0
govee-ble==0.44.0 govee-ble==0.44.0
# homeassistant.components.govee_light_local # homeassistant.components.govee_light_local
govee-local-api==2.3.0 govee-local-api==2.4.0
# homeassistant.components.remote_rpi_gpio # homeassistant.components.remote_rpi_gpio
gpiozero==1.6.2 gpiozero==1.6.2
@@ -1226,7 +1226,7 @@ hole==0.9.0
holidays==0.84 holidays==0.84
# homeassistant.components.frontend # homeassistant.components.frontend
home-assistant-frontend==20260304.0 home-assistant-frontend==20260312.0
# homeassistant.components.conversation # homeassistant.components.conversation
home-assistant-intents==2026.3.3 home-assistant-intents==2026.3.3
@@ -1271,7 +1271,7 @@ ibeacon-ble==1.2.0
# homeassistant.components.local_calendar # homeassistant.components.local_calendar
# homeassistant.components.local_todo # homeassistant.components.local_todo
# homeassistant.components.remote_calendar # homeassistant.components.remote_calendar
ical==13.2.0 ical==13.2.2
# homeassistant.components.caldav # homeassistant.components.caldav
icalendar==6.3.1 icalendar==6.3.1
@@ -1663,7 +1663,7 @@ odp-amsterdam==6.1.2
oemthermostat==1.1.1 oemthermostat==1.1.1
# homeassistant.components.ohme # homeassistant.components.ohme
ohme==1.6.0 ohme==1.7.0
# homeassistant.components.ollama # homeassistant.components.ollama
ollama==0.5.1 ollama==0.5.1
@@ -1676,7 +1676,7 @@ ondilo==0.5.0
# homeassistant.components.onedrive # homeassistant.components.onedrive
# homeassistant.components.onedrive_for_business # homeassistant.components.onedrive_for_business
onedrive-personal-sdk==0.1.4 onedrive-personal-sdk==0.1.7
# homeassistant.components.onvif # homeassistant.components.onvif
onvif-zeep-async==4.0.4 onvif-zeep-async==4.0.4
@@ -1938,7 +1938,7 @@ pyairobotrest==0.3.0
pyairvisual==2023.08.1 pyairvisual==2023.08.1
# homeassistant.components.anglian_water # homeassistant.components.anglian_water
pyanglianwater==3.1.0 pyanglianwater==3.1.1
# homeassistant.components.aprilaire # homeassistant.components.aprilaire
pyaprilaire==0.9.1 pyaprilaire==0.9.1
@@ -2179,7 +2179,7 @@ pyitachip2ir==0.0.7
pyituran==0.1.5 pyituran==0.1.5
# homeassistant.components.jvc_projector # homeassistant.components.jvc_projector
pyjvcprojector==2.0.1 pyjvcprojector==2.0.3
# homeassistant.components.kaleidescape # homeassistant.components.kaleidescape
pykaleidescape==1.1.3 pykaleidescape==1.1.3
@@ -2370,7 +2370,7 @@ pyplaato==0.0.19
pypoint==3.0.0 pypoint==3.0.0
# homeassistant.components.portainer # homeassistant.components.portainer
pyportainer==1.0.31 pyportainer==1.0.33
# homeassistant.components.probe_plus # homeassistant.components.probe_plus
pyprobeplus==1.1.2 pyprobeplus==1.1.2
@@ -2473,7 +2473,7 @@ pysmappee==0.2.29
pysmarlaapi==1.0.1 pysmarlaapi==1.0.1
# homeassistant.components.smartthings # homeassistant.components.smartthings
pysmartthings==3.6.0 pysmartthings==3.7.0
# homeassistant.components.smarty # homeassistant.components.smarty
pysmarty2==0.10.3 pysmarty2==0.10.3
@@ -2530,7 +2530,7 @@ python-awair==0.2.5
python-blockchain-api==0.0.2 python-blockchain-api==0.0.2
# homeassistant.components.bsblan # homeassistant.components.bsblan
python-bsblan==5.1.0 python-bsblan==5.1.2
# homeassistant.components.citybikes # homeassistant.components.citybikes
python-citybikes==0.3.3 python-citybikes==0.3.3
@@ -2612,7 +2612,7 @@ python-opensky==1.0.1
# homeassistant.components.otbr # homeassistant.components.otbr
# homeassistant.components.thread # homeassistant.components.thread
python-otbr-api==2.8.0 python-otbr-api==2.9.0
# homeassistant.components.overseerr # homeassistant.components.overseerr
python-overseerr==0.9.0 python-overseerr==0.9.0
@@ -2969,7 +2969,7 @@ speak2mary==1.4.0
speedtest-cli==2.1.3 speedtest-cli==2.1.3
# homeassistant.components.spotify # homeassistant.components.spotify
spotifyaio==1.0.0 spotifyaio==2.0.2
# homeassistant.components.sql # homeassistant.components.sql
sqlparse==0.5.5 sqlparse==0.5.5
@@ -3038,7 +3038,7 @@ tellcore-py==1.1.2
tellduslive==0.10.12 tellduslive==0.10.12
# homeassistant.components.teltonika # homeassistant.components.teltonika
teltasync==0.1.3 teltasync==0.2.0
# homeassistant.components.lg_soundbar # homeassistant.components.lg_soundbar
temescal==0.5 temescal==0.5
@@ -3307,7 +3307,7 @@ yalesmartalarmclient==0.4.3
# homeassistant.components.august # homeassistant.components.august
# homeassistant.components.yale # homeassistant.components.yale
# homeassistant.components.yalexs_ble # homeassistant.components.yalexs_ble
yalexs-ble==3.2.7 yalexs-ble==3.2.8
# homeassistant.components.august # homeassistant.components.august
# homeassistant.components.yale # homeassistant.components.yale
@@ -3347,7 +3347,7 @@ zeroconf==0.148.0
zeversolar==0.3.2 zeversolar==0.3.2
# homeassistant.components.zha # homeassistant.components.zha
zha==1.0.1 zha==1.0.2
# homeassistant.components.zhong_hong # homeassistant.components.zhong_hong
zhong-hong-hvac==1.0.13 zhong-hong-hvac==1.0.13
+20 -20
View File
@@ -47,7 +47,7 @@ PlexAPI==4.15.16
ProgettiHWSW==0.1.3 ProgettiHWSW==0.1.3
# homeassistant.components.cast # homeassistant.components.cast
PyChromecast==14.0.9 PyChromecast==14.0.10
# homeassistant.components.flume # homeassistant.components.flume
PyFlume==0.6.5 PyFlume==0.6.5
@@ -181,7 +181,7 @@ aioairzone-cloud==0.7.2
aioairzone==1.0.5 aioairzone==1.0.5
# homeassistant.components.alexa_devices # homeassistant.components.alexa_devices
aioamazondevices==13.0.0 aioamazondevices==13.0.1
# homeassistant.components.ambient_network # homeassistant.components.ambient_network
# homeassistant.components.ambient_station # homeassistant.components.ambient_station
@@ -215,7 +215,7 @@ aiobafi6==0.9.0
aiobotocore==2.21.1 aiobotocore==2.21.1
# homeassistant.components.comelit # homeassistant.components.comelit
aiocomelit==2.0.0 aiocomelit==2.0.1
# homeassistant.components.dhcp # homeassistant.components.dhcp
aiodhcpwatcher==1.2.1 aiodhcpwatcher==1.2.1
@@ -398,7 +398,7 @@ aiosteamist==1.0.1
aiostreammagic==2.13.0 aiostreammagic==2.13.0
# homeassistant.components.switcher_kis # homeassistant.components.switcher_kis
aioswitcher==6.1.0 aioswitcher==6.1.1
# homeassistant.components.syncthing # homeassistant.components.syncthing
aiosyncthing==0.7.1 aiosyncthing==0.7.1
@@ -422,7 +422,7 @@ aiousbwatcher==1.1.1
aiovlc==0.5.1 aiovlc==0.5.1
# homeassistant.components.vodafone_station # homeassistant.components.vodafone_station
aiovodafone==3.1.2 aiovodafone==3.1.3
# homeassistant.components.waqi # homeassistant.components.waqi
aiowaqi==3.1.0 aiowaqi==3.1.0
@@ -998,7 +998,7 @@ gotailwind==0.3.0
govee-ble==0.44.0 govee-ble==0.44.0
# homeassistant.components.govee_light_local # homeassistant.components.govee_light_local
govee-local-api==2.3.0 govee-local-api==2.4.0
# homeassistant.components.gpsd # homeassistant.components.gpsd
gps3==0.33.3 gps3==0.33.3
@@ -1087,7 +1087,7 @@ hole==0.9.0
holidays==0.84 holidays==0.84
# homeassistant.components.frontend # homeassistant.components.frontend
home-assistant-frontend==20260304.0 home-assistant-frontend==20260312.0
# homeassistant.components.conversation # homeassistant.components.conversation
home-assistant-intents==2026.3.3 home-assistant-intents==2026.3.3
@@ -1126,7 +1126,7 @@ ibeacon-ble==1.2.0
# homeassistant.components.local_calendar # homeassistant.components.local_calendar
# homeassistant.components.local_todo # homeassistant.components.local_todo
# homeassistant.components.remote_calendar # homeassistant.components.remote_calendar
ical==13.2.0 ical==13.2.2
# homeassistant.components.caldav # homeassistant.components.caldav
icalendar==6.3.1 icalendar==6.3.1
@@ -1449,7 +1449,7 @@ objgraph==3.5.0
odp-amsterdam==6.1.2 odp-amsterdam==6.1.2
# homeassistant.components.ohme # homeassistant.components.ohme
ohme==1.6.0 ohme==1.7.0
# homeassistant.components.ollama # homeassistant.components.ollama
ollama==0.5.1 ollama==0.5.1
@@ -1462,7 +1462,7 @@ ondilo==0.5.0
# homeassistant.components.onedrive # homeassistant.components.onedrive
# homeassistant.components.onedrive_for_business # homeassistant.components.onedrive_for_business
onedrive-personal-sdk==0.1.4 onedrive-personal-sdk==0.1.7
# homeassistant.components.onvif # homeassistant.components.onvif
onvif-zeep-async==4.0.4 onvif-zeep-async==4.0.4
@@ -1669,7 +1669,7 @@ pyairobotrest==0.3.0
pyairvisual==2023.08.1 pyairvisual==2023.08.1
# homeassistant.components.anglian_water # homeassistant.components.anglian_water
pyanglianwater==3.1.0 pyanglianwater==3.1.1
# homeassistant.components.aprilaire # homeassistant.components.aprilaire
pyaprilaire==0.9.1 pyaprilaire==0.9.1
@@ -1856,7 +1856,7 @@ pyisy==3.4.1
pyituran==0.1.5 pyituran==0.1.5
# homeassistant.components.jvc_projector # homeassistant.components.jvc_projector
pyjvcprojector==2.0.1 pyjvcprojector==2.0.3
# homeassistant.components.kaleidescape # homeassistant.components.kaleidescape
pykaleidescape==1.1.3 pykaleidescape==1.1.3
@@ -2020,7 +2020,7 @@ pyplaato==0.0.19
pypoint==3.0.0 pypoint==3.0.0
# homeassistant.components.portainer # homeassistant.components.portainer
pyportainer==1.0.31 pyportainer==1.0.33
# homeassistant.components.probe_plus # homeassistant.components.probe_plus
pyprobeplus==1.1.2 pyprobeplus==1.1.2
@@ -2102,7 +2102,7 @@ pysmappee==0.2.29
pysmarlaapi==1.0.1 pysmarlaapi==1.0.1
# homeassistant.components.smartthings # homeassistant.components.smartthings
pysmartthings==3.6.0 pysmartthings==3.7.0
# homeassistant.components.smarty # homeassistant.components.smarty
pysmarty2==0.10.3 pysmarty2==0.10.3
@@ -2153,7 +2153,7 @@ python-MotionMount==2.3.0
python-awair==0.2.5 python-awair==0.2.5
# homeassistant.components.bsblan # homeassistant.components.bsblan
python-bsblan==5.1.0 python-bsblan==5.1.2
# homeassistant.components.ecobee # homeassistant.components.ecobee
python-ecobee-api==0.3.2 python-ecobee-api==0.3.2
@@ -2208,7 +2208,7 @@ python-opensky==1.0.1
# homeassistant.components.otbr # homeassistant.components.otbr
# homeassistant.components.thread # homeassistant.components.thread
python-otbr-api==2.8.0 python-otbr-api==2.9.0
# homeassistant.components.overseerr # homeassistant.components.overseerr
python-overseerr==0.9.0 python-overseerr==0.9.0
@@ -2502,7 +2502,7 @@ speak2mary==1.4.0
speedtest-cli==2.1.3 speedtest-cli==2.1.3
# homeassistant.components.spotify # homeassistant.components.spotify
spotifyaio==1.0.0 spotifyaio==2.0.2
# homeassistant.components.sql # homeassistant.components.sql
sqlparse==0.5.5 sqlparse==0.5.5
@@ -2550,7 +2550,7 @@ tailscale==0.6.2
tellduslive==0.10.12 tellduslive==0.10.12
# homeassistant.components.teltonika # homeassistant.components.teltonika
teltasync==0.1.3 teltasync==0.2.0
# homeassistant.components.lg_soundbar # homeassistant.components.lg_soundbar
temescal==0.5 temescal==0.5
@@ -2783,7 +2783,7 @@ yalesmartalarmclient==0.4.3
# homeassistant.components.august # homeassistant.components.august
# homeassistant.components.yale # homeassistant.components.yale
# homeassistant.components.yalexs_ble # homeassistant.components.yalexs_ble
yalexs-ble==3.2.7 yalexs-ble==3.2.8
# homeassistant.components.august # homeassistant.components.august
# homeassistant.components.yale # homeassistant.components.yale
@@ -2817,7 +2817,7 @@ zeroconf==0.148.0
zeversolar==0.3.2 zeversolar==0.3.2
# homeassistant.components.zha # homeassistant.components.zha
zha==1.0.1 zha==1.0.2
# homeassistant.components.zinvolt # homeassistant.components.zinvolt
zinvolt==0.3.0 zinvolt==0.3.0
-1
View File
@@ -181,7 +181,6 @@ EXCEPTIONS = {
"PySwitchmate", # https://github.com/Danielhiversen/pySwitchmate/pull/16 "PySwitchmate", # https://github.com/Danielhiversen/pySwitchmate/pull/16
"PyXiaomiGateway", # https://github.com/Danielhiversen/PyXiaomiGateway/pull/201 "PyXiaomiGateway", # https://github.com/Danielhiversen/PyXiaomiGateway/pull/201
"chacha20poly1305", # LGPL "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 "commentjson", # https://github.com/vaidik/commentjson/pull/55
"crownstone-cloud", # https://github.com/crownstone/crownstone-lib-python-cloud/pull/5 "crownstone-cloud", # https://github.com/crownstone/crownstone-lib-python-cloud/pull/5
"crownstone-core", # https://github.com/crownstone/crownstone-lib-python-core/pull/6 "crownstone-core", # https://github.com/crownstone/crownstone-lib-python-core/pull/6
+3 -6
View File
@@ -2,10 +2,7 @@
from unittest.mock import AsyncMock from unittest.mock import AsyncMock
from aioamazondevices.const.devices import ( from aioamazondevices.const.devices import SPEAKER_GROUP_FAMILY
SPEAKER_GROUP_DEVICE_TYPE,
SPEAKER_GROUP_FAMILY,
)
from aioamazondevices.exceptions import CannotConnect, CannotRetrieveData from aioamazondevices.exceptions import CannotConnect, CannotRetrieveData
import pytest import pytest
@@ -117,7 +114,7 @@ async def test_alexa_dnd_group_removal(
identifiers={(DOMAIN, mock_config_entry.entry_id)}, identifiers={(DOMAIN, mock_config_entry.entry_id)},
name=mock_config_entry.title, name=mock_config_entry.title,
manufacturer="Amazon", manufacturer="Amazon",
model=SPEAKER_GROUP_DEVICE_TYPE, model="Speaker Group",
entry_type=dr.DeviceEntryType.SERVICE, entry_type=dr.DeviceEntryType.SERVICE,
) )
@@ -156,7 +153,7 @@ async def test_alexa_unsupported_notification_sensor_removal(
identifiers={(DOMAIN, mock_config_entry.entry_id)}, identifiers={(DOMAIN, mock_config_entry.entry_id)},
name=mock_config_entry.title, name=mock_config_entry.title,
manufacturer="Amazon", manufacturer="Amazon",
model=SPEAKER_GROUP_DEVICE_TYPE, model="Speaker Group",
entry_type=dr.DeviceEntryType.SERVICE, entry_type=dr.DeviceEntryType.SERVICE,
) )
+62 -2
View File
@@ -2,7 +2,7 @@
from unittest.mock import Mock, patch from unittest.mock import Mock, patch
from aiohttp import ClientResponseError from aiohttp import ClientError, ClientResponseError
import pytest import pytest
from yalexs.const import Brand from yalexs.const import Brand
from yalexs.exceptions import AugustApiAIOHTTPError, InvalidAuth from yalexs.exceptions import AugustApiAIOHTTPError, InvalidAuth
@@ -18,7 +18,11 @@ from homeassistant.const import (
STATE_ON, STATE_ON,
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import (
HomeAssistantError,
OAuth2TokenRequestReauthError,
OAuth2TokenRequestTransientError,
)
from homeassistant.helpers import ( from homeassistant.helpers import (
device_registry as dr, device_registry as dr,
entity_registry as er, entity_registry as er,
@@ -304,3 +308,59 @@ async def test_oauth_implementation_not_available(hass: HomeAssistant) -> None:
await hass.async_block_till_done() await hass.async_block_till_done()
assert entry.state is ConfigEntryState.SETUP_RETRY 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
@@ -1,73 +0,0 @@
# serializer version: 1
# name: test_entry_diagnostics[large]
dict({
'backup': list([
dict({
'addons': list([
]),
'backup_id': '23e64aec',
'database_included': True,
'date': '2024-11-22T11:48:48.727189+01:00',
'extra_metadata': dict({
}),
'folders': list([
]),
'homeassistant_included': True,
'homeassistant_version': '2024.12.0.dev0',
'name': 'Core 2024.12.0.dev0',
'protected': False,
'size': 20971520,
}),
]),
'backup_agents': list([
dict({
'name': 'test',
}),
]),
'config': dict({
'access_key_id': '**REDACTED**',
'bucket': 'test',
'endpoint_url': 'https://s3.eu-south-1.amazonaws.com',
'secret_access_key': '**REDACTED**',
}),
'coordinator_data': dict({
'all_backups_size': 20971520,
}),
})
# ---
# name: test_entry_diagnostics[small]
dict({
'backup': list([
dict({
'addons': list([
]),
'backup_id': '23e64aec',
'database_included': True,
'date': '2024-11-22T11:48:48.727189+01:00',
'extra_metadata': dict({
}),
'folders': list([
]),
'homeassistant_included': True,
'homeassistant_version': '2024.12.0.dev0',
'name': 'Core 2024.12.0.dev0',
'protected': False,
'size': 1048576,
}),
]),
'backup_agents': list([
dict({
'name': 'test',
}),
]),
'config': dict({
'access_key_id': '**REDACTED**',
'bucket': 'test',
'endpoint_url': 'https://s3.eu-south-1.amazonaws.com',
'secret_access_key': '**REDACTED**',
}),
'coordinator_data': dict({
'all_backups_size': 1048576,
}),
})
# ---
@@ -1,29 +0,0 @@
"""Tests for AWS S3 diagnostics."""
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.backup import DOMAIN as BACKUP_DOMAIN
from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component
from tests.common import MockConfigEntry
from tests.components.diagnostics import get_diagnostics_for_config_entry
from tests.typing import ClientSessionGenerator
async def test_entry_diagnostics(
hass: HomeAssistant,
hass_client: ClientSessionGenerator,
mock_config_entry: MockConfigEntry,
snapshot: SnapshotAssertion,
) -> None:
"""Test config entry diagnostics."""
mock_config_entry.add_to_hass(hass)
assert await async_setup_component(hass, BACKUP_DOMAIN, {BACKUP_DOMAIN: {}})
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
assert (
await get_diagnostics_for_config_entry(hass, hass_client, mock_config_entry)
== snapshot
)
+62
View File
@@ -286,6 +286,68 @@ def mock_thermostat_with_operating_mode() -> Mock:
return climate return climate
@pytest.fixture
def mock_thermostat_quickapp_1() -> Mock:
"""Fixture for a thermostat."""
climate = Mock()
climate.fibaro_id = 6
climate.parent_fibaro_id = 0
climate.has_endpoint_id = False
climate.name = "Test climate"
climate.room_id = 1
climate.dead = False
climate.visible = True
climate.enabled = True
climate.type = "com.fibaro.hvacSystemHeat"
climate.base_type = "com.fibaro.hvacSystem"
climate.properties = {"manufacturer": ""}
climate.actions = {"setHeatingThermostatSetpoint": 1, "setThermostatMode": 1}
climate.supported_features = {}
climate.has_supported_operating_modes = False
climate.has_supported_thermostat_modes = True
climate.supported_thermostat_modes = ["Off", "Heat"]
climate.has_thermostat_mode = True
climate.thermostat_mode = "Heat"
climate.has_unit = False
climate.has_heating_thermostat_setpoint = False
climate.has_heating_thermostat_setpoint_future = False
value_mock = Mock()
value_mock.has_value = False
climate.value = value_mock
return climate
@pytest.fixture
def mock_thermostat_quickapp_2() -> Mock:
"""Fixture for a thermostat."""
climate = Mock()
climate.fibaro_id = 7
climate.parent_fibaro_id = 0
climate.has_endpoint_id = False
climate.name = "Test climate 2"
climate.room_id = 1
climate.dead = False
climate.visible = True
climate.enabled = True
climate.type = "com.fibaro.hvacSystemHeat"
climate.base_type = "com.fibaro.hvacSystem"
climate.properties = {"manufacturer": ""}
climate.actions = {"setHeatingThermostatSetpoint": 1, "setThermostatMode": 1}
climate.supported_features = {}
climate.has_supported_operating_modes = False
climate.has_supported_thermostat_modes = True
climate.supported_thermostat_modes = ["Off", "Heat"]
climate.has_thermostat_mode = True
climate.thermostat_mode = "Heat"
climate.has_unit = False
climate.has_heating_thermostat_setpoint = False
climate.has_heating_thermostat_setpoint_future = False
value_mock = Mock()
value_mock.has_value = False
climate.value = value_mock
return climate
@pytest.fixture @pytest.fixture
def mock_fan_device() -> Mock: def mock_fan_device() -> Mock:
"""Fixture for a fan endpoint of a thermostat device.""" """Fixture for a fan endpoint of a thermostat device."""
+28
View File
@@ -41,6 +41,34 @@ async def test_climate_setup(
) )
async def test_climate_setup_2_quickapps(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
mock_fibaro_client: Mock,
mock_config_entry: MockConfigEntry,
mock_thermostat_quickapp_1: Mock,
mock_thermostat_quickapp_2: Mock,
mock_room: Mock,
) -> None:
"""Test that the climate creates entities for more than one QuickApp."""
# Arrange
mock_fibaro_client.read_rooms.return_value = [mock_room]
mock_fibaro_client.read_devices.return_value = [
mock_thermostat_quickapp_1,
mock_thermostat_quickapp_2,
]
with patch("homeassistant.components.fibaro.PLATFORMS", [Platform.CLIMATE]):
# Act
await init_integration(hass, mock_config_entry)
# Assert
entry1 = entity_registry.async_get("climate.room_1_test_climate_6")
assert entry1
entry2 = entity_registry.async_get("climate.room_1_test_climate_2_7")
assert entry2
async def test_hvac_mode_preset( async def test_hvac_mode_preset(
hass: HomeAssistant, hass: HomeAssistant,
mock_fibaro_client: Mock, mock_fibaro_client: Mock,
+5
View File
@@ -396,6 +396,11 @@ async def test_switch_device_no_ip_address(
"async_set_deflection_enable", "async_set_deflection_enable",
STATE_ON, STATE_ON,
), ),
(
"switch.mock_title_wi_fi_mywifi",
"async_set_wlan_configuration",
STATE_ON,
),
], ],
) )
async def test_switch_turn_on_off( async def test_switch_turn_on_off(
+1 -1
View File
@@ -16,7 +16,7 @@ from tests.common import MockConfigEntry
API_URL = "https://test.ghost.io" API_URL = "https://test.ghost.io"
API_KEY = "650b7a9f8e8c1234567890ab:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef" API_KEY = "650b7a9f8e8c1234567890ab:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"
SITE_UUID = "a1b2c3d4-e5f6-7890-abcd-ef1234567890" 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} POSTS_DATA = {"published": 42, "drafts": 5, "scheduled": 2}
MEMBERS_DATA = {"total": 1000, "paid": 100, "free": 850, "comped": 50} MEMBERS_DATA = {"total": 1000, "paid": 100, "free": 850, "comped": 50}
LATEST_POST_DATA = { LATEST_POST_DATA = {
@@ -414,7 +414,7 @@
'object_id_base': 'Energy exported', 'object_id_base': 'Energy exported',
'options': dict({ 'options': dict({
'sensor': dict({ 'sensor': dict({
'suggested_display_precision': 0, 'suggested_display_precision': 2,
}), }),
}), }),
'original_device_class': <SensorDeviceClass.ENERGY: 'energy'>, 'original_device_class': <SensorDeviceClass.ENERGY: 'energy'>,
@@ -426,7 +426,7 @@
'supported_features': 0, 'supported_features': 0,
'translation_key': 'energy_exported', 'translation_key': 'energy_exported',
'unique_id': '40580137858664_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] # name: test_entities[sensor.homevolt_ems_energy_exported-state]
@@ -435,7 +435,7 @@
'device_class': 'energy', 'device_class': 'energy',
'friendly_name': 'Homevolt EMS Energy exported', 'friendly_name': 'Homevolt EMS Energy exported',
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>, 'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
'unit_of_measurement': <UnitOfEnergy.WATT_HOUR: 'Wh'>, 'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}), }),
'context': <ANY>, 'context': <ANY>,
'entity_id': 'sensor.homevolt_ems_energy_exported', 'entity_id': 'sensor.homevolt_ems_energy_exported',
@@ -471,7 +471,7 @@
'object_id_base': 'Energy imported', 'object_id_base': 'Energy imported',
'options': dict({ 'options': dict({
'sensor': dict({ 'sensor': dict({
'suggested_display_precision': 0, 'suggested_display_precision': 2,
}), }),
}), }),
'original_device_class': <SensorDeviceClass.ENERGY: 'energy'>, 'original_device_class': <SensorDeviceClass.ENERGY: 'energy'>,
@@ -483,7 +483,7 @@
'supported_features': 0, 'supported_features': 0,
'translation_key': 'energy_imported', 'translation_key': 'energy_imported',
'unique_id': '40580137858664_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] # name: test_entities[sensor.homevolt_ems_energy_imported-state]
@@ -492,7 +492,7 @@
'device_class': 'energy', 'device_class': 'energy',
'friendly_name': 'Homevolt EMS Energy imported', 'friendly_name': 'Homevolt EMS Energy imported',
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>, 'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
'unit_of_measurement': <UnitOfEnergy.WATT_HOUR: 'Wh'>, 'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}), }),
'context': <ANY>, 'context': <ANY>,
'entity_id': 'sensor.homevolt_ems_energy_imported', 'entity_id': 'sensor.homevolt_ems_energy_imported',
@@ -8007,8 +8007,11 @@
'name': None, 'name': None,
'object_id_base': 'Water usage', 'object_id_base': 'Water usage',
'options': dict({ '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_icon': None,
'original_name': 'Water usage', 'original_name': 'Water usage',
'platform': 'homewizard', 'platform': 'homewizard',
@@ -8023,6 +8026,7 @@
# name: test_sensors[HWE-P1-entity_ids0][sensor.device_water_usage:state] # name: test_sensors[HWE-P1-entity_ids0][sensor.device_water_usage:state]
StateSnapshot({ StateSnapshot({
'attributes': ReadOnlyDict({ 'attributes': ReadOnlyDict({
'device_class': 'volume_flow_rate',
'friendly_name': 'Device Water usage', 'friendly_name': 'Device Water usage',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>, 'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfVolumeFlowRate.LITERS_PER_MINUTE: 'L/min'>, 'unit_of_measurement': <UnitOfVolumeFlowRate.LITERS_PER_MINUTE: 'L/min'>,
@@ -11927,8 +11931,11 @@
'name': None, 'name': None,
'object_id_base': 'Water usage', 'object_id_base': 'Water usage',
'options': dict({ '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_icon': None,
'original_name': 'Water usage', 'original_name': 'Water usage',
'platform': 'homewizard', 'platform': 'homewizard',
@@ -11943,6 +11950,7 @@
# name: test_sensors[HWE-P1-invalid-EAN-entity_ids9][sensor.device_water_usage:state] # name: test_sensors[HWE-P1-invalid-EAN-entity_ids9][sensor.device_water_usage:state]
StateSnapshot({ StateSnapshot({
'attributes': ReadOnlyDict({ 'attributes': ReadOnlyDict({
'device_class': 'volume_flow_rate',
'friendly_name': 'Device Water usage', 'friendly_name': 'Device Water usage',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>, 'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfVolumeFlowRate.LITERS_PER_MINUTE: 'L/min'>, 'unit_of_measurement': <UnitOfVolumeFlowRate.LITERS_PER_MINUTE: 'L/min'>,
@@ -15408,8 +15416,11 @@
'name': None, 'name': None,
'object_id_base': 'Water usage', 'object_id_base': 'Water usage',
'options': dict({ '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_icon': None,
'original_name': 'Water usage', 'original_name': 'Water usage',
'platform': 'homewizard', 'platform': 'homewizard',
@@ -15424,6 +15435,7 @@
# name: test_sensors[HWE-P1-zero-values-entity_ids1][sensor.device_water_usage:state] # name: test_sensors[HWE-P1-zero-values-entity_ids1][sensor.device_water_usage:state]
StateSnapshot({ StateSnapshot({
'attributes': ReadOnlyDict({ 'attributes': ReadOnlyDict({
'device_class': 'volume_flow_rate',
'friendly_name': 'Device Water usage', 'friendly_name': 'Device Water usage',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>, 'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfVolumeFlowRate.LITERS_PER_MINUTE: 'L/min'>, 'unit_of_measurement': <UnitOfVolumeFlowRate.LITERS_PER_MINUTE: 'L/min'>,
@@ -17573,8 +17585,11 @@
'name': None, 'name': None,
'object_id_base': 'Water usage', 'object_id_base': 'Water usage',
'options': dict({ '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_icon': None,
'original_name': 'Water usage', 'original_name': 'Water usage',
'platform': 'homewizard', 'platform': 'homewizard',
@@ -17589,6 +17604,7 @@
# name: test_sensors[HWE-WTR-entity_ids4][sensor.device_water_usage:state] # name: test_sensors[HWE-WTR-entity_ids4][sensor.device_water_usage:state]
StateSnapshot({ StateSnapshot({
'attributes': ReadOnlyDict({ 'attributes': ReadOnlyDict({
'device_class': 'volume_flow_rate',
'friendly_name': 'Device Water usage', 'friendly_name': 'Device Water usage',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>, 'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfVolumeFlowRate.LITERS_PER_MINUTE: 'L/min'>, 'unit_of_measurement': <UnitOfVolumeFlowRate.LITERS_PER_MINUTE: 'L/min'>,
@@ -427,9 +427,7 @@
'aliases': set({ 'aliases': set({
}), }),
'area_id': None, 'area_id': None,
'capabilities': dict({ 'capabilities': None,
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>, 'config_entry_id': <ANY>,
'config_subentry_id': <ANY>, 'config_subentry_id': <ANY>,
'device_class': None, 'device_class': None,
@@ -466,7 +464,6 @@
'attribution': 'Data provided by unpublished Intellifire API', 'attribution': 'Data provided by unpublished Intellifire API',
'device_class': 'timestamp', 'device_class': 'timestamp',
'friendly_name': 'IntelliFire Timer end', 'friendly_name': 'IntelliFire Timer end',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}), }),
'context': <ANY>, 'context': <ANY>,
'entity_id': 'sensor.intellifire_timer_end', 'entity_id': 'sensor.intellifire_timer_end',
@@ -833,7 +833,7 @@
'max': 450, 'max': 450,
'min': 10, 'min': 10,
'mode': <NumberMode.BOX: 'box'>, 'mode': <NumberMode.BOX: 'box'>,
'step': 5, 'step': 1,
}), }),
'config_entry_id': <ANY>, 'config_entry_id': <ANY>,
'config_subentry_id': <ANY>, 'config_subentry_id': <ANY>,
@@ -872,7 +872,7 @@
'max': 450, 'max': 450,
'min': 10, 'min': 10,
'mode': <NumberMode.BOX: 'box'>, 'mode': <NumberMode.BOX: 'box'>,
'step': 5, 'step': 1,
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>, 'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
}), }),
'context': <ANY>, 'context': <ANY>,
+33
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
@@ -644,6 +644,31 @@ async def test_setting_device_tracker_location_via_abbr_reset_message(
assert state.attributes["source_type"] == "gps" assert state.attributes["source_type"] == "gps"
assert state.state == STATE_HOME 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( async def test_setting_blocked_attribute_via_mqtt_json_message(
hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator
@@ -2,7 +2,6 @@
# name: test_diagnostics # name: test_diagnostics
dict({ dict({
'cap_available': True, 'cap_available': True,
'ct_connected': True,
'device_info': dict({ 'device_info': dict({
'model': 'Home Pro', 'model': 'Home Pro',
'name': 'Ohme Home Pro', 'name': 'Ohme Home Pro',
+10 -2
View File
@@ -263,20 +263,28 @@ async def test_full_flow_reconfigure_unique_id(
) -> None: ) -> None:
"""Test the full flow of the config flow, this time with a known unique ID.""" """Test the full flow of the config flow, this time with a known unique ID."""
mock_config_entry.add_to_hass(hass) 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) result = await mock_config_entry.start_reconfigure_flow(hass)
assert result["type"] is FlowResultType.FORM assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "reconfigure" assert result["step_id"] == "reconfigure"
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
result["flow_id"], result["flow_id"],
user_input=MOCK_USER_SETUP, user_input=USER_INPUT_RECONFIGURE,
) )
assert result["type"] is FlowResultType.ABORT assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "already_configured" assert result["reason"] == "already_configured"
assert mock_config_entry.data[CONF_API_TOKEN] == "test_api_token" 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_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 assert len(mock_setup_entry.mock_calls) == 0
+2 -1
View File
@@ -5,6 +5,7 @@ from __future__ import annotations
from collections.abc import Generator from collections.abc import Generator
from http import HTTPStatus from http import HTTPStatus
import json import json
import re
from typing import Any from typing import Any
from unittest.mock import patch from unittest.mock import patch
@@ -278,4 +279,4 @@ def handle_responses(
async def handle(method, url, data) -> AiohttpClientMockResponse: async def handle(method, url, data) -> AiohttpClientMockResponse:
return responses.pop(0) return responses.pop(0)
aioclient_mock.post(URL, side_effect=handle) aioclient_mock.post(re.compile(r"^https?://[^/]+/stick$"), side_effect=handle)
@@ -70,7 +70,7 @@ async def test_no_unique_id(
"""Test rainsensor binary sensor with no unique id.""" """Test rainsensor binary sensor with no unique id."""
# Failure to migrate config entry to a 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) await hass.config_entries.async_setup(config_entry.entry_id)
assert config_entry.state is ConfigEntryState.LOADED assert config_entry.state is ConfigEntryState.LOADED
+1 -1
View File
@@ -292,7 +292,7 @@ async def test_no_unique_id(
"""Test calendar entity with no unique id.""" """Test calendar entity with no unique id."""
# Failure to migrate config entry to a 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) await hass.config_entries.async_setup(config_entry.entry_id)
assert config_entry.state is ConfigEntryState.LOADED assert config_entry.state is ConfigEntryState.LOADED
+29 -6
View File
@@ -19,6 +19,7 @@ from .conftest import (
CONFIG_ENTRY_DATA, CONFIG_ENTRY_DATA,
HOST, HOST,
MAC_ADDRESS_UNIQUE_ID, MAC_ADDRESS_UNIQUE_ID,
MODEL_AND_VERSION_RESPONSE,
PASSWORD, PASSWORD,
SERIAL_NUMBER, SERIAL_NUMBER,
SERIAL_RESPONSE, SERIAL_RESPONSE,
@@ -36,7 +37,11 @@ from tests.test_util.aiohttp import AiohttpClientMocker, AiohttpClientMockRespon
@pytest.fixture(name="responses") @pytest.fixture(name="responses")
def mock_responses() -> list[AiohttpClientMockResponse]: def mock_responses() -> list[AiohttpClientMockResponse]:
"""Set up fake serial number response when testing the connection.""" """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) @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_response(SERIAL_RESPONSE),
mock_json_response(WIFI_PARAMS_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_response(ZERO_SERIAL_RESPONSE),
mock_json_response(WIFI_PARAMS_RESPONSE), mock_json_response(WIFI_PARAMS_RESPONSE),
], ],
@@ -123,7 +130,11 @@ async def test_controller_flow(
( (
"other-serial-number", "other-serial-number",
{**CONFIG_ENTRY_DATA, "host": "other-host"}, {**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, CONFIG_ENTRY_DATA,
), ),
( (
@@ -133,6 +144,7 @@ async def test_controller_flow(
"host": "other-host", "host": "other-host",
}, },
[ [
mock_response(MODEL_AND_VERSION_RESPONSE),
mock_response(SERIAL_RESPONSE), mock_response(SERIAL_RESPONSE),
mock_json_response(WIFI_PARAMS_RESPONSE), mock_json_response(WIFI_PARAMS_RESPONSE),
], ],
@@ -142,6 +154,7 @@ async def test_controller_flow(
None, None,
{**CONFIG_ENTRY_DATA, "serial_number": 0, "host": "other-host"}, {**CONFIG_ENTRY_DATA, "serial_number": 0, "host": "other-host"},
[ [
mock_response(MODEL_AND_VERSION_RESPONSE),
mock_response(ZERO_SERIAL_RESPONSE), mock_response(ZERO_SERIAL_RESPONSE),
mock_json_response(WIFI_PARAMS_RESPONSE), mock_json_response(WIFI_PARAMS_RESPONSE),
], ],
@@ -185,6 +198,7 @@ async def test_multiple_config_entries(
MAC_ADDRESS_UNIQUE_ID, MAC_ADDRESS_UNIQUE_ID,
CONFIG_ENTRY_DATA, CONFIG_ENTRY_DATA,
[ [
mock_response(MODEL_AND_VERSION_RESPONSE),
mock_response(SERIAL_RESPONSE), mock_response(SERIAL_RESPONSE),
mock_json_response(WIFI_PARAMS_RESPONSE), mock_json_response(WIFI_PARAMS_RESPONSE),
], ],
@@ -194,7 +208,11 @@ async def test_multiple_config_entries(
( (
SERIAL_NUMBER, SERIAL_NUMBER,
CONFIG_ENTRY_DATA, 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, CONFIG_ENTRY_DATA,
), ),
# Old unique id with no serial, but same host # Old unique id with no serial, but same host
@@ -202,6 +220,7 @@ async def test_multiple_config_entries(
None, None,
{**CONFIG_ENTRY_DATA, "serial_number": 0}, {**CONFIG_ENTRY_DATA, "serial_number": 0},
[ [
mock_response(MODEL_AND_VERSION_RESPONSE),
mock_response(ZERO_SERIAL_RESPONSE), mock_response(ZERO_SERIAL_RESPONSE),
mock_json_response(WIFI_PARAMS_RESPONSE), mock_json_response(WIFI_PARAMS_RESPONSE),
], ],
@@ -214,7 +233,11 @@ async def test_multiple_config_entries(
**CONFIG_ENTRY_DATA, **CONFIG_ENTRY_DATA,
"host": f"other-{HOST}", "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 CONFIG_ENTRY_DATA, # Updated the host
), ),
], ],
@@ -281,8 +304,8 @@ async def test_controller_invalid_auth(
[ [
# Incorrect password response # Incorrect password response
AiohttpClientMockResponse("POST", URL, status=HTTPStatus.FORBIDDEN), AiohttpClientMockResponse("POST", URL, status=HTTPStatus.FORBIDDEN),
AiohttpClientMockResponse("POST", URL, status=HTTPStatus.FORBIDDEN),
# Second attempt with the correct password # Second attempt with the correct password
mock_response(MODEL_AND_VERSION_RESPONSE),
mock_response(SERIAL_RESPONSE), mock_response(SERIAL_RESPONSE),
mock_json_response(WIFI_PARAMS_RESPONSE), mock_json_response(WIFI_PARAMS_RESPONSE),
] ]
@@ -346,8 +369,8 @@ async def test_controller_timeout(
[ [
# First attempt simulate the wrong password # First attempt simulate the wrong password
AiohttpClientMockResponse("POST", URL, status=HTTPStatus.FORBIDDEN), AiohttpClientMockResponse("POST", URL, status=HTTPStatus.FORBIDDEN),
AiohttpClientMockResponse("POST", URL, status=HTTPStatus.FORBIDDEN),
# Second attempt simulate the correct password # Second attempt simulate the correct password
mock_response(MODEL_AND_VERSION_RESPONSE),
mock_response(SERIAL_RESPONSE), mock_response(SERIAL_RESPONSE),
mock_json_response(WIFI_PARAMS_RESPONSE), mock_json_response(WIFI_PARAMS_RESPONSE),
], ],
+24 -7
View File
@@ -62,6 +62,7 @@ async def test_init_success(
( (
CONFIG_ENTRY_DATA, CONFIG_ENTRY_DATA,
[ [
mock_response(MODEL_AND_VERSION_RESPONSE),
mock_response(MODEL_AND_VERSION_RESPONSE), mock_response(MODEL_AND_VERSION_RESPONSE),
mock_response_error(HTTPStatus.SERVICE_UNAVAILABLE), mock_response_error(HTTPStatus.SERVICE_UNAVAILABLE),
], ],
@@ -71,6 +72,7 @@ async def test_init_success(
( (
CONFIG_ENTRY_DATA, CONFIG_ENTRY_DATA,
[ [
mock_response(MODEL_AND_VERSION_RESPONSE),
mock_response(MODEL_AND_VERSION_RESPONSE), mock_response(MODEL_AND_VERSION_RESPONSE),
mock_response_error(HTTPStatus.INTERNAL_SERVER_ERROR), mock_response_error(HTTPStatus.INTERNAL_SERVER_ERROR),
], ],
@@ -123,7 +125,7 @@ async def test_fix_unique_id(
) -> None: ) -> None:
"""Test fix of a config entry with no unique id.""" """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) entries = hass.config_entries.async_entries(DOMAIN)
assert len(entries) == 1 assert len(entries) == 1
@@ -181,7 +183,7 @@ async def test_fix_unique_id_failure(
) -> None: ) -> None:
"""Test a failure during fix of a config entry with no unique id.""" """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) await hass.config_entries.async_setup(config_entry.entry_id)
# Config entry is loaded, but not updated # Config entry is loaded, but not updated
@@ -212,11 +214,16 @@ async def test_fix_unique_id_duplicate(
) )
other_entry.add_to_hass(hass) other_entry.add_to_hass(hass)
# Responses for the second config entry. This first fetches wifi params # Responses for the second config entry.
# to repair the unique id. #
responses_copy = [*responses] # `pyrainbird.async_client.create_controller` probes by calling
responses.append(mock_json_response(WIFI_PARAMS_RESPONSE)) # `get_model_and_version()`, then `_async_fix_unique_id` fetches wifi params.
responses.extend(responses_copy) responses.extend(
[
mock_response(MODEL_AND_VERSION_RESPONSE),
mock_json_response(WIFI_PARAMS_RESPONSE),
]
)
await hass.config_entries.async_setup(config_entry.entry_id) await hass.config_entries.async_setup(config_entry.entry_id)
assert config_entry.state is ConfigEntryState.LOADED 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 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( async def test_reload_migration_with_leading_zero_mac(
hass: HomeAssistant, hass: HomeAssistant,
device_registry: dr.DeviceRegistry, device_registry: dr.DeviceRegistry,
entity_registry: er.EntityRegistry, entity_registry: er.EntityRegistry,
responses: list[AiohttpClientMockResponse],
) -> None: ) -> None:
"""Test migration and reload of a device with a mac address with a leading zero.""" """Test migration and reload of a device with a mac address with a leading zero."""
mac_address = "01:02:03:04:05:06" 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) 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 # Create a device and entity with the old unique id format
device_entry = device_registry.async_get_or_create( device_entry = device_registry.async_get_or_create(
config_entry_id=config_entry.entry_id, config_entry_id=config_entry.entry_id,

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