mirror of
https://github.com/home-assistant/core.git
synced 2026-03-17 08:21:58 +01:00
Compare commits
61 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2c47e83342 | ||
|
|
e3c6a2184d | ||
|
|
0ba0829350 | ||
|
|
678048e681 | ||
|
|
743eeeae53 | ||
|
|
46555c6d9a | ||
|
|
dbaca0a723 | ||
|
|
9bb2959029 | ||
|
|
0304781fa9 | ||
|
|
e081d28aa4 | ||
|
|
34aa28c72f | ||
|
|
cfa2946db8 | ||
|
|
1b0779347c | ||
|
|
93a281e7af | ||
|
|
6b32e27fd3 | ||
|
|
79928a8c7c | ||
|
|
9146518e13 | ||
|
|
e9c5172f43 | ||
|
|
cce21ad4b9 | ||
|
|
10ec02ca3c | ||
|
|
bdf54491e5 | ||
|
|
0b05d34238 | ||
|
|
4c69a1c5f7 | ||
|
|
6f1f56dcaa | ||
|
|
d0b9991232 | ||
|
|
aacf39be8a | ||
|
|
bf055da82c | ||
|
|
0fb118bcd9 | ||
|
|
954ef7d1f5 | ||
|
|
b091299320 | ||
|
|
52483e18b2 | ||
|
|
57e8683ed7 | ||
|
|
67faace978 | ||
|
|
e4be64fcb1 | ||
|
|
f552b8221f | ||
|
|
55dc5392f9 | ||
|
|
5b93aeae38 | ||
|
|
33610bb1a1 | ||
|
|
6c3cebe413 | ||
|
|
5346895d9b | ||
|
|
05c3f08c6c | ||
|
|
1ce025733d | ||
|
|
1537ea86b8 | ||
|
|
ec137870fa | ||
|
|
816ee7f53e | ||
|
|
6e7eeec827 | ||
|
|
d100477a22 | ||
|
|
98ac6dd2c1 | ||
|
|
6b30969f60 | ||
|
|
e9a6b5d662 | ||
|
|
f95f3f9982 | ||
|
|
3f884a8cd1 | ||
|
|
10f284932e | ||
|
|
e1c4e6dc42 | ||
|
|
0976e7de4e | ||
|
|
ae1012b2f0 | ||
|
|
bb7c4faca5 | ||
|
|
0b1be61336 | ||
|
|
3ec44024a2 | ||
|
|
1200cc5779 | ||
|
|
d632931f74 |
@@ -1,6 +1,5 @@
|
||||
"""Defines a base Alexa Devices entity."""
|
||||
|
||||
from aioamazondevices.const.devices import SPEAKER_GROUP_DEVICE_TYPE
|
||||
from aioamazondevices.structures import AmazonDevice
|
||||
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
@@ -25,20 +24,15 @@ class AmazonEntity(CoordinatorEntity[AmazonDevicesCoordinator]):
|
||||
"""Initialize the entity."""
|
||||
super().__init__(coordinator)
|
||||
self._serial_num = serial_num
|
||||
model = self.device.model
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, serial_num)},
|
||||
name=self.device.account_name,
|
||||
model=model,
|
||||
model=self.device.model,
|
||||
model_id=self.device.device_type,
|
||||
manufacturer=self.device.manufacturer or "Amazon",
|
||||
hw_version=self.device.hardware_version,
|
||||
sw_version=(
|
||||
self.device.software_version
|
||||
if model != SPEAKER_GROUP_DEVICE_TYPE
|
||||
else None
|
||||
),
|
||||
serial_number=serial_num if model != SPEAKER_GROUP_DEVICE_TYPE else None,
|
||||
sw_version=self.device.software_version,
|
||||
serial_number=serial_num,
|
||||
)
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{serial_num}-{description.key}"
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aioamazondevices"],
|
||||
"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
|
||||
|
||||
await method(self.device, state)
|
||||
await self.coordinator.async_request_refresh()
|
||||
self.coordinator.data[self.device.serial_number].sensors[
|
||||
self.entity_description.key
|
||||
].value = state
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the switch on."""
|
||||
|
||||
@@ -9,5 +9,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["pyanglianwater"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["pyanglianwater==3.1.0"]
|
||||
"requirements": ["pyanglianwater==3.1.1"]
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ from __future__ import annotations
|
||||
from pathlib import Path
|
||||
from typing import cast
|
||||
|
||||
from aiohttp import ClientResponseError
|
||||
from aiohttp import ClientError
|
||||
from yalexs.exceptions import AugustApiAIOHTTPError
|
||||
from yalexs.manager.exceptions import CannotConnect, InvalidAuth, RequireValidation
|
||||
from yalexs.manager.gateway import Config as YaleXSConfig
|
||||
@@ -13,7 +13,12 @@ from yalexs.manager.gateway import Config as YaleXSConfig
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.exceptions import (
|
||||
ConfigEntryAuthFailed,
|
||||
ConfigEntryNotReady,
|
||||
OAuth2TokenRequestError,
|
||||
OAuth2TokenRequestReauthError,
|
||||
)
|
||||
from homeassistant.helpers import device_registry as dr, issue_registry as ir
|
||||
from homeassistant.helpers.config_entry_oauth2_flow import (
|
||||
ImplementationUnavailableError,
|
||||
@@ -45,11 +50,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: AugustConfigEntry) -> bo
|
||||
august_gateway = AugustGateway(Path(hass.config.config_dir), session, oauth_session)
|
||||
try:
|
||||
await async_setup_august(hass, entry, august_gateway)
|
||||
except OAuth2TokenRequestReauthError as err:
|
||||
raise ConfigEntryAuthFailed from err
|
||||
except (RequireValidation, InvalidAuth) as err:
|
||||
raise ConfigEntryAuthFailed from err
|
||||
except TimeoutError as err:
|
||||
raise ConfigEntryNotReady("Timed out connecting to august api") from err
|
||||
except (AugustApiAIOHTTPError, ClientResponseError, CannotConnect) as err:
|
||||
except (
|
||||
AugustApiAIOHTTPError,
|
||||
OAuth2TokenRequestError,
|
||||
ClientError,
|
||||
CannotConnect,
|
||||
) as err:
|
||||
raise ConfigEntryNotReady from err
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
return True
|
||||
|
||||
@@ -30,5 +30,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["pubnub", "yalexs"],
|
||||
"requirements": ["yalexs==9.2.0", "yalexs-ble==3.2.7"]
|
||||
"requirements": ["yalexs==9.2.0", "yalexs-ble==3.2.8"]
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["bsblan"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["python-bsblan==5.1.0"],
|
||||
"requirements": ["python-bsblan==5.1.2"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"name": "bsb-lan*",
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["casttube", "pychromecast"],
|
||||
"requirements": ["PyChromecast==14.0.9"],
|
||||
"requirements": ["PyChromecast==14.0.10"],
|
||||
"single_config_entry": true,
|
||||
"zeroconf": ["_googlecast._tcp.local."]
|
||||
}
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["aiocomelit"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["aiocomelit==2.0.0"]
|
||||
"requirements": ["aiocomelit==2.0.1"]
|
||||
}
|
||||
|
||||
@@ -133,26 +133,20 @@ async def _async_wifi_entities_list(
|
||||
]
|
||||
)
|
||||
_LOGGER.debug("WiFi networks count: %s", wifi_count)
|
||||
networks: dict = {}
|
||||
networks: dict[int, dict[str, Any]] = {}
|
||||
for i in range(1, wifi_count + 1):
|
||||
network_info = await avm_wrapper.async_get_wlan_configuration(i)
|
||||
# Devices with 4 WLAN services, use the 2nd for internal communications
|
||||
if not (wifi_count == 4 and i == 2):
|
||||
networks[i] = {
|
||||
"ssid": network_info["NewSSID"],
|
||||
"bssid": network_info["NewBSSID"],
|
||||
"standard": network_info["NewStandard"],
|
||||
"enabled": network_info["NewEnable"],
|
||||
"status": network_info["NewStatus"],
|
||||
}
|
||||
networks[i] = network_info
|
||||
for i, network in networks.copy().items():
|
||||
networks[i]["switch_name"] = network["ssid"]
|
||||
networks[i]["switch_name"] = network["NewSSID"]
|
||||
if (
|
||||
len(
|
||||
[
|
||||
j
|
||||
for j, n in networks.items()
|
||||
if slugify(n["ssid"]) == slugify(network["ssid"])
|
||||
if slugify(n["NewSSID"]) == slugify(network["NewSSID"])
|
||||
]
|
||||
)
|
||||
> 1
|
||||
@@ -434,13 +428,11 @@ class FritzBoxPortSwitch(FritzBoxBaseSwitch):
|
||||
for key, attr in attributes_dict.items():
|
||||
self._attributes[attr] = self.port_mapping[key]
|
||||
|
||||
async def _async_switch_on_off_executor(self, turn_on: bool) -> bool:
|
||||
async def _async_switch_on_off_executor(self, turn_on: bool) -> None:
|
||||
self.port_mapping["NewEnabled"] = "1" if turn_on else "0"
|
||||
|
||||
resp = await self._avm_wrapper.async_add_port_mapping(
|
||||
await self._avm_wrapper.async_add_port_mapping(
|
||||
self.connection_type, self.port_mapping
|
||||
)
|
||||
return bool(resp is not None)
|
||||
|
||||
|
||||
class FritzBoxDeflectionSwitch(FritzBoxBaseCoordinatorSwitch):
|
||||
@@ -525,12 +517,11 @@ class FritzBoxProfileSwitch(FritzDeviceBase, SwitchEntity):
|
||||
"""Turn off switch."""
|
||||
await self._async_handle_turn_on_off(turn_on=False)
|
||||
|
||||
async def _async_handle_turn_on_off(self, turn_on: bool) -> bool:
|
||||
async def _async_handle_turn_on_off(self, turn_on: bool) -> None:
|
||||
"""Handle switch state change request."""
|
||||
await self._avm_wrapper.async_set_allow_wan_access(self.ip_address, turn_on)
|
||||
self._avm_wrapper.devices[self._mac].wan_access = turn_on
|
||||
self.async_write_ha_state()
|
||||
return True
|
||||
|
||||
|
||||
class FritzBoxWifiSwitch(FritzBoxBaseSwitch):
|
||||
@@ -541,10 +532,11 @@ class FritzBoxWifiSwitch(FritzBoxBaseSwitch):
|
||||
avm_wrapper: AvmWrapper,
|
||||
device_friendly_name: str,
|
||||
network_num: int,
|
||||
network_data: dict,
|
||||
network_data: dict[str, Any],
|
||||
) -> None:
|
||||
"""Init Fritz Wifi switch."""
|
||||
self._avm_wrapper = avm_wrapper
|
||||
self._wifi_info = network_data
|
||||
|
||||
self._attributes = {}
|
||||
self._attr_entity_category = EntityCategory.CONFIG
|
||||
@@ -560,7 +552,7 @@ class FritzBoxWifiSwitch(FritzBoxBaseSwitch):
|
||||
type=SWITCH_TYPE_WIFINETWORK,
|
||||
callback_update=self._async_fetch_update,
|
||||
callback_switch=self._async_switch_on_off_executor,
|
||||
init_state=network_data["enabled"],
|
||||
init_state=network_data["NewEnable"],
|
||||
)
|
||||
super().__init__(self._avm_wrapper, device_friendly_name, switch_info)
|
||||
|
||||
@@ -587,7 +579,9 @@ class FritzBoxWifiSwitch(FritzBoxBaseSwitch):
|
||||
self._attributes["mac_address_control"] = wifi_info[
|
||||
"NewMACAddressControlEnabled"
|
||||
]
|
||||
self._wifi_info = wifi_info
|
||||
|
||||
async def _async_switch_on_off_executor(self, turn_on: bool) -> None:
|
||||
"""Handle wifi switch."""
|
||||
self._wifi_info["NewEnable"] = turn_on
|
||||
await self._avm_wrapper.async_set_wlan_configuration(self._network_num, turn_on)
|
||||
|
||||
@@ -21,5 +21,5 @@
|
||||
"integration_type": "system",
|
||||
"preview_features": { "winter_mode": {} },
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["home-assistant-frontend==20260304.0"]
|
||||
"requirements": ["home-assistant-frontend==20260312.0"]
|
||||
}
|
||||
|
||||
@@ -89,7 +89,7 @@ class GhostConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
site_title = site["title"]
|
||||
|
||||
await self.async_set_unique_id(site["uuid"])
|
||||
await self.async_set_unique_id(site["site_uuid"])
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
return self.async_create_entry(
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["googleapiclient"],
|
||||
"requirements": ["gcal-sync==8.0.0", "oauth2client==4.1.3", "ical==13.2.0"]
|
||||
"requirements": ["gcal-sync==8.0.0", "oauth2client==4.1.3", "ical==13.2.2"]
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"dependencies": ["network"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/govee_light_local",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["govee-local-api==2.3.0"]
|
||||
"requirements": ["govee-local-api==2.4.0"]
|
||||
}
|
||||
|
||||
@@ -91,14 +91,14 @@ SENSORS: tuple[SensorEntityDescription, ...] = (
|
||||
translation_key="energy_exported",
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="energy_imported",
|
||||
translation_key="energy_imported",
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="frequency",
|
||||
|
||||
@@ -610,6 +610,7 @@ SENSORS: Final[tuple[HomeWizardSensorEntityDescription, ...]] = (
|
||||
key="active_liter_lpm",
|
||||
translation_key="active_liter_lpm",
|
||||
native_unit_of_measurement=UnitOfVolumeFlowRate.LITERS_PER_MINUTE,
|
||||
device_class=SensorDeviceClass.VOLUME_FLOW_RATE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
has_fn=lambda data: data.measurement.active_liter_lpm is not None,
|
||||
value_fn=lambda data: data.measurement.active_liter_lpm,
|
||||
|
||||
@@ -901,7 +901,9 @@ class PowerViewShadeDualOverlappedRear(PowerViewShadeDualOverlappedBase):
|
||||
)
|
||||
|
||||
|
||||
class PowerViewShadeDualOverlappedCombinedTilt(PowerViewShadeDualOverlappedCombined):
|
||||
class PowerViewShadeDualOverlappedCombinedTilt(
|
||||
PowerViewShadeDualOverlappedCombined, PowerViewShadeWithTiltBase
|
||||
):
|
||||
"""Represent a shade that has a front sheer and rear opaque panel.
|
||||
|
||||
This equates to two shades being controlled by one motor.
|
||||
@@ -915,26 +917,6 @@ class PowerViewShadeDualOverlappedCombinedTilt(PowerViewShadeDualOverlappedCombi
|
||||
Type 10 - Duolite with 180° Tilt
|
||||
"""
|
||||
|
||||
# type
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: PowerviewShadeUpdateCoordinator,
|
||||
device_info: PowerviewDeviceInfo,
|
||||
room_name: str,
|
||||
shade: BaseShade,
|
||||
name: str,
|
||||
) -> None:
|
||||
"""Initialize the shade."""
|
||||
super().__init__(coordinator, device_info, room_name, shade, name)
|
||||
self._attr_supported_features |= (
|
||||
CoverEntityFeature.OPEN_TILT
|
||||
| CoverEntityFeature.CLOSE_TILT
|
||||
| CoverEntityFeature.SET_TILT_POSITION
|
||||
)
|
||||
if self._shade.is_supported(MOTION_STOP):
|
||||
self._attr_supported_features |= CoverEntityFeature.STOP_TILT
|
||||
self._max_tilt = self._shade.shade_limits.tilt_max
|
||||
|
||||
@property
|
||||
def transition_steps(self) -> int:
|
||||
"""Return the steps to make a move."""
|
||||
@@ -949,26 +931,6 @@ class PowerViewShadeDualOverlappedCombinedTilt(PowerViewShadeDualOverlappedCombi
|
||||
tilt = self.positions.tilt
|
||||
return ceil(primary + secondary + tilt)
|
||||
|
||||
@callback
|
||||
def _get_shade_tilt(self, target_hass_tilt_position: int) -> ShadePosition:
|
||||
"""Return a ShadePosition."""
|
||||
return ShadePosition(
|
||||
tilt=target_hass_tilt_position,
|
||||
velocity=self.positions.velocity,
|
||||
)
|
||||
|
||||
@property
|
||||
def open_tilt_position(self) -> ShadePosition:
|
||||
"""Return the open tilt position and required additional positions."""
|
||||
return replace(self._shade.open_position_tilt, velocity=self.positions.velocity)
|
||||
|
||||
@property
|
||||
def close_tilt_position(self) -> ShadePosition:
|
||||
"""Return the open tilt position and required additional positions."""
|
||||
return replace(
|
||||
self._shade.close_position_tilt, velocity=self.positions.velocity
|
||||
)
|
||||
|
||||
|
||||
TYPE_TO_CLASSES = {
|
||||
0: (PowerViewShade,),
|
||||
|
||||
@@ -97,7 +97,6 @@ INTELLIFIRE_SENSORS: tuple[IntellifireSensorEntityDescription, ...] = (
|
||||
IntellifireSensorEntityDescription(
|
||||
key="timer_end_timestamp",
|
||||
translation_key="timer_end_timestamp",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
device_class=SensorDeviceClass.TIMESTAMP,
|
||||
value_fn=_time_remaining_to_timestamp,
|
||||
),
|
||||
|
||||
@@ -221,13 +221,13 @@ class IntesisAC(ClimateEntity):
|
||||
def extra_state_attributes(self) -> dict[str, Any]:
|
||||
"""Return the device specific state attributes."""
|
||||
attrs = {}
|
||||
if self._outdoor_temp:
|
||||
if self._outdoor_temp is not None:
|
||||
attrs["outdoor_temp"] = self._outdoor_temp
|
||||
if self._power_consumption_heat:
|
||||
if self._power_consumption_heat is not None:
|
||||
attrs["power_consumption_heat_kw"] = round(
|
||||
self._power_consumption_heat / 1000, 1
|
||||
)
|
||||
if self._power_consumption_cool:
|
||||
if self._power_consumption_cool is not None:
|
||||
attrs["power_consumption_cool_kw"] = round(
|
||||
self._power_consumption_cool / 1000, 1
|
||||
)
|
||||
@@ -244,7 +244,7 @@ class IntesisAC(ClimateEntity):
|
||||
if hvac_mode := kwargs.get(ATTR_HVAC_MODE):
|
||||
await self.async_set_hvac_mode(hvac_mode)
|
||||
|
||||
if temperature := kwargs.get(ATTR_TEMPERATURE):
|
||||
if (temperature := kwargs.get(ATTR_TEMPERATURE)) is not None:
|
||||
_LOGGER.debug("Setting %s to %s degrees", self._device_type, temperature)
|
||||
await self._controller.set_temperature(self._device_id, temperature)
|
||||
self._attr_target_temperature = temperature
|
||||
@@ -271,7 +271,7 @@ class IntesisAC(ClimateEntity):
|
||||
await self._controller.set_mode(self._device_id, MAP_HVAC_MODE_TO_IH[hvac_mode])
|
||||
|
||||
# Send the temperature again in case changing modes has changed it
|
||||
if self._attr_target_temperature:
|
||||
if self._attr_target_temperature is not None:
|
||||
await self._controller.set_temperature(
|
||||
self._device_id, self._attr_target_temperature
|
||||
)
|
||||
|
||||
@@ -358,7 +358,7 @@ PINECIL_SETPOINT_NUMBER_DESCRIPTION = IronOSNumberEntityDescription(
|
||||
native_max_value=MAX_TEMP,
|
||||
native_min_value_f=MIN_TEMP_F,
|
||||
native_max_value_f=MAX_TEMP_F,
|
||||
native_step=5,
|
||||
native_step=1,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["jvcprojector"],
|
||||
"requirements": ["pyjvcprojector==2.0.1"]
|
||||
"requirements": ["pyjvcprojector==2.0.3"]
|
||||
}
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["aionotify", "evdev"],
|
||||
"quality_scale": "legacy",
|
||||
"requirements": ["evdev==1.6.1", "asyncinotify==4.2.0"]
|
||||
"requirements": ["evdev==1.9.3", "asyncinotify==4.4.0"]
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ from xknx.dpt import DPTBase, DPTComplex, DPTEnum, DPTNumeric
|
||||
from xknx.dpt.dpt_16 import DPTString
|
||||
|
||||
from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass
|
||||
from homeassistant.const import UnitOfReactiveEnergy
|
||||
|
||||
HaDptClass = Literal["numeric", "enum", "complex", "string"]
|
||||
|
||||
@@ -36,7 +37,7 @@ def get_supported_dpts() -> Mapping[str, DPTInfo]:
|
||||
main=dpt_class.dpt_main_number, # type: ignore[typeddict-item] # checked in xknx unit tests
|
||||
sub=dpt_class.dpt_sub_number,
|
||||
name=dpt_class.value_type,
|
||||
unit=dpt_class.unit,
|
||||
unit=_sensor_unit_overrides.get(dpt_number_str, dpt_class.unit),
|
||||
sensor_device_class=_sensor_device_classes.get(dpt_number_str),
|
||||
sensor_state_class=_get_sensor_state_class(ha_dpt_class, dpt_number_str),
|
||||
)
|
||||
@@ -77,13 +78,13 @@ _sensor_device_classes: Mapping[str, SensorDeviceClass] = {
|
||||
"12.1200": SensorDeviceClass.VOLUME,
|
||||
"12.1201": SensorDeviceClass.VOLUME,
|
||||
"13.002": SensorDeviceClass.VOLUME_FLOW_RATE,
|
||||
"13.010": SensorDeviceClass.ENERGY,
|
||||
"13.012": SensorDeviceClass.REACTIVE_ENERGY,
|
||||
"13.013": SensorDeviceClass.ENERGY,
|
||||
"13.015": SensorDeviceClass.REACTIVE_ENERGY,
|
||||
"13.016": SensorDeviceClass.ENERGY,
|
||||
"13.1200": SensorDeviceClass.VOLUME,
|
||||
"13.1201": SensorDeviceClass.VOLUME,
|
||||
"13.010": SensorDeviceClass.ENERGY, # DPTActiveEnergy
|
||||
"13.012": SensorDeviceClass.REACTIVE_ENERGY, # DPTReactiveEnergy
|
||||
"13.013": SensorDeviceClass.ENERGY, # DPTActiveEnergykWh
|
||||
"13.015": SensorDeviceClass.REACTIVE_ENERGY, # DPTReactiveEnergykVARh
|
||||
"13.016": SensorDeviceClass.ENERGY, # DPTActiveEnergyMWh
|
||||
"13.1200": SensorDeviceClass.VOLUME, # DPTDeltaVolumeLiquidLitre
|
||||
"13.1201": SensorDeviceClass.VOLUME, # DPTDeltaVolumeM3
|
||||
"14.010": SensorDeviceClass.AREA,
|
||||
"14.019": SensorDeviceClass.CURRENT,
|
||||
"14.027": SensorDeviceClass.VOLTAGE,
|
||||
@@ -91,7 +92,7 @@ _sensor_device_classes: Mapping[str, SensorDeviceClass] = {
|
||||
"14.030": SensorDeviceClass.VOLTAGE,
|
||||
"14.031": SensorDeviceClass.ENERGY,
|
||||
"14.033": SensorDeviceClass.FREQUENCY,
|
||||
"14.037": SensorDeviceClass.ENERGY_STORAGE,
|
||||
"14.037": SensorDeviceClass.ENERGY_STORAGE, # DPTHeatQuantity
|
||||
"14.039": SensorDeviceClass.DISTANCE,
|
||||
"14.051": SensorDeviceClass.WEIGHT,
|
||||
"14.056": SensorDeviceClass.POWER,
|
||||
@@ -101,7 +102,7 @@ _sensor_device_classes: Mapping[str, SensorDeviceClass] = {
|
||||
"14.068": SensorDeviceClass.TEMPERATURE,
|
||||
"14.069": SensorDeviceClass.TEMPERATURE,
|
||||
"14.070": SensorDeviceClass.TEMPERATURE_DELTA,
|
||||
"14.076": SensorDeviceClass.VOLUME,
|
||||
"14.076": SensorDeviceClass.VOLUME, # DPTVolume
|
||||
"14.077": SensorDeviceClass.VOLUME_FLOW_RATE,
|
||||
"14.080": SensorDeviceClass.APPARENT_POWER,
|
||||
"14.1200": SensorDeviceClass.VOLUME_FLOW_RATE,
|
||||
@@ -121,17 +122,28 @@ _sensor_state_class_overrides: Mapping[str, SensorStateClass | None] = {
|
||||
"13.010": SensorStateClass.TOTAL, # DPTActiveEnergy
|
||||
"13.011": SensorStateClass.TOTAL, # DPTApparantEnergy
|
||||
"13.012": SensorStateClass.TOTAL, # DPTReactiveEnergy
|
||||
"13.013": SensorStateClass.TOTAL, # DPTActiveEnergykWh
|
||||
"13.015": SensorStateClass.TOTAL, # DPTReactiveEnergykVARh
|
||||
"13.016": SensorStateClass.TOTAL, # DPTActiveEnergyMWh
|
||||
"13.1200": SensorStateClass.TOTAL, # DPTDeltaVolumeLiquidLitre
|
||||
"13.1201": SensorStateClass.TOTAL, # DPTDeltaVolumeM3
|
||||
"14.007": SensorStateClass.MEASUREMENT_ANGLE, # DPTAngleDeg
|
||||
"14.037": SensorStateClass.TOTAL, # DPTHeatQuantity
|
||||
"14.051": SensorStateClass.TOTAL, # DPTMass
|
||||
"14.055": SensorStateClass.MEASUREMENT_ANGLE, # DPTPhaseAngleDeg
|
||||
"14.031": SensorStateClass.TOTAL_INCREASING, # DPTEnergy
|
||||
"14.076": SensorStateClass.TOTAL, # DPTVolume
|
||||
"17.001": None, # DPTSceneNumber
|
||||
"29.010": SensorStateClass.TOTAL, # DPTActiveEnergy8Byte
|
||||
"29.011": SensorStateClass.TOTAL, # DPTApparantEnergy8Byte
|
||||
"29.012": SensorStateClass.TOTAL, # DPTReactiveEnergy8Byte
|
||||
}
|
||||
|
||||
_sensor_unit_overrides: Mapping[str, str] = {
|
||||
"13.012": UnitOfReactiveEnergy.VOLT_AMPERE_REACTIVE_HOUR, # DPTReactiveEnergy (VARh in KNX)
|
||||
"13.015": UnitOfReactiveEnergy.KILO_VOLT_AMPERE_REACTIVE_HOUR, # DPTReactiveEnergykVARh (kVARh in KNX)
|
||||
"29.012": UnitOfReactiveEnergy.VOLT_AMPERE_REACTIVE_HOUR, # DPTReactiveEnergy8Byte (VARh in KNX)
|
||||
}
|
||||
|
||||
|
||||
def _get_sensor_state_class(
|
||||
ha_dpt_class: HaDptClass, dpt_number_str: str
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/local_calendar",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["ical"],
|
||||
"requirements": ["ical==13.2.0"]
|
||||
"requirements": ["ical==13.2.2"]
|
||||
}
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/local_todo",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["ical==13.2.0"]
|
||||
"requirements": ["ical==13.2.2"]
|
||||
}
|
||||
|
||||
@@ -188,6 +188,7 @@ class ProgramPhaseTumbleDryer(MieleEnum, missing_to_none=True):
|
||||
finished = 522, 11012
|
||||
extra_dry = 523
|
||||
hand_iron = 524
|
||||
hygiene_drying = 525
|
||||
moisten = 526
|
||||
thermo_spin = 527
|
||||
timed_drying = 528
|
||||
@@ -617,11 +618,11 @@ class OvenProgramId(MieleEnum, missing_to_none=True):
|
||||
evaporate_water = 327
|
||||
shabbat_program = 335
|
||||
yom_tov = 336
|
||||
drying = 357
|
||||
drying = 357, 2028
|
||||
heat_crockery = 358
|
||||
prove_dough = 359
|
||||
prove_dough = 359, 2023
|
||||
low_temperature_cooking = 360
|
||||
steam_cooking = 361
|
||||
steam_cooking = 8, 361
|
||||
keeping_warm = 362
|
||||
apple_sponge = 364
|
||||
apple_pie = 365
|
||||
@@ -668,9 +669,9 @@ class OvenProgramId(MieleEnum, missing_to_none=True):
|
||||
saddle_of_roebuck = 456
|
||||
salmon_fillet = 461
|
||||
potato_cheese_gratin = 464
|
||||
trout = 486
|
||||
carp = 491
|
||||
salmon_trout = 492
|
||||
trout = 486, 2224
|
||||
carp = 491, 2233
|
||||
salmon_trout = 492, 2241
|
||||
springform_tin_15cm = 496
|
||||
springform_tin_20cm = 497
|
||||
springform_tin_25cm = 498
|
||||
@@ -736,137 +737,15 @@ class OvenProgramId(MieleEnum, missing_to_none=True):
|
||||
pork_belly = 701
|
||||
pikeperch_fillet_with_vegetables = 702
|
||||
steam_bake = 99001
|
||||
|
||||
|
||||
class DishWarmerProgramId(MieleEnum, missing_to_none=True):
|
||||
"""Program Id codes for dish warmers."""
|
||||
|
||||
no_program = 0, -1
|
||||
warm_cups_glasses = 1
|
||||
warm_dishes_plates = 2
|
||||
keep_warm = 3
|
||||
slow_roasting = 4
|
||||
|
||||
|
||||
class RobotVacuumCleanerProgramId(MieleEnum, missing_to_none=True):
|
||||
"""Program Id codes for robot vacuum cleaners."""
|
||||
|
||||
no_program = 0, -1
|
||||
auto = 1
|
||||
spot = 2
|
||||
turbo = 3
|
||||
silent = 4
|
||||
|
||||
|
||||
class CoffeeSystemProgramId(MieleEnum, missing_to_none=True):
|
||||
"""Program Id codes for coffee systems."""
|
||||
|
||||
no_program = 0, -1
|
||||
|
||||
check_appliance = 17004
|
||||
|
||||
# profile 1
|
||||
ristretto = 24000, 24032, 24064, 24096, 24128
|
||||
espresso = 24001, 24033, 24065, 24097, 24129
|
||||
coffee = 24002, 24034, 24066, 24098, 24130
|
||||
long_coffee = 24003, 24035, 24067, 24099, 24131
|
||||
cappuccino = 24004, 24036, 24068, 24100, 24132
|
||||
cappuccino_italiano = 24005, 24037, 24069, 24101, 24133
|
||||
latte_macchiato = 24006, 24038, 24070, 24102, 24134
|
||||
espresso_macchiato = 24007, 24039, 24071, 24135
|
||||
cafe_au_lait = 24008, 24040, 24072, 24104, 24136
|
||||
caffe_latte = 24009, 24041, 24073, 24105, 24137
|
||||
flat_white = 24012, 24044, 24076, 24108, 24140
|
||||
very_hot_water = 24013, 24045, 24077, 24109, 24141
|
||||
hot_water = 24014, 24046, 24078, 24110, 24142
|
||||
hot_milk = 24015, 24047, 24079, 24111, 24143
|
||||
milk_foam = 24016, 24048, 24080, 24112, 24144
|
||||
black_tea = 24017, 24049, 24081, 24113, 24145
|
||||
herbal_tea = 24018, 24050, 24082, 24114, 24146
|
||||
fruit_tea = 24019, 24051, 24083, 24115, 24147
|
||||
green_tea = 24020, 24052, 24084, 24116, 24148
|
||||
white_tea = 24021, 24053, 24085, 24117, 24149
|
||||
japanese_tea = 24022, 29054, 24086, 24118, 24150
|
||||
# special programs
|
||||
coffee_pot = 24400
|
||||
barista_assistant = 24407
|
||||
# machine settings menu
|
||||
appliance_settings = (
|
||||
16016, # display brightness
|
||||
16018, # volume
|
||||
16019, # buttons volume
|
||||
16020, # child lock
|
||||
16021, # water hardness
|
||||
16027, # welcome sound
|
||||
16033, # connection status
|
||||
16035, # remote control
|
||||
16037, # remote update
|
||||
24500, # total dispensed
|
||||
24502, # lights appliance on
|
||||
24503, # lights appliance off
|
||||
24504, # turn off lights after
|
||||
24506, # altitude
|
||||
24513, # performance mode
|
||||
24516, # turn off after
|
||||
24537, # advanced mode
|
||||
24542, # tea timer
|
||||
24549, # total coffee dispensed
|
||||
24550, # total tea dispensed
|
||||
24551, # total ristretto
|
||||
24552, # total cappuccino
|
||||
24553, # total espresso
|
||||
24554, # total coffee
|
||||
24555, # total long coffee
|
||||
24556, # total italian cappuccino
|
||||
24557, # total latte macchiato
|
||||
24558, # total caffe latte
|
||||
24560, # total espresso macchiato
|
||||
24562, # total flat white
|
||||
24563, # total coffee with milk
|
||||
24564, # total black tea
|
||||
24565, # total herbal tea
|
||||
24566, # total fruit tea
|
||||
24567, # total green tea
|
||||
24568, # total white tea
|
||||
24569, # total japanese tea
|
||||
24571, # total milk foam
|
||||
24572, # total hot milk
|
||||
24573, # total hot water
|
||||
24574, # total very hot water
|
||||
24575, # counter to descaling
|
||||
24576, # counter to brewing unit degreasing
|
||||
24800, # maintenance
|
||||
24801, # profiles settings menu
|
||||
24813, # add profile
|
||||
)
|
||||
appliance_rinse = 24750, 24759, 24773, 24787, 24788
|
||||
intermediate_rinsing = 24758
|
||||
automatic_maintenance = 24778
|
||||
descaling = 24751
|
||||
brewing_unit_degrease = 24753
|
||||
milk_pipework_rinse = 24754
|
||||
milk_pipework_clean = 24789
|
||||
|
||||
|
||||
class SteamOvenMicroProgramId(MieleEnum, missing_to_none=True):
|
||||
"""Program Id codes for steam oven micro combo."""
|
||||
|
||||
no_program = 0, -1
|
||||
steam_cooking = 8
|
||||
microwave = 19
|
||||
popcorn = 53
|
||||
quick_mw = 54
|
||||
sous_vide = 72
|
||||
eco_steam_cooking = 75
|
||||
rapid_steam_cooking = 77
|
||||
descale = 326
|
||||
menu_cooking = 330
|
||||
reheating_with_steam = 2018
|
||||
defrosting_with_steam = 2019
|
||||
blanching = 2020
|
||||
bottling = 2021
|
||||
sterilize_crockery = 2022
|
||||
prove_dough = 2023
|
||||
soak = 2027
|
||||
reheating_with_microwave = 2029
|
||||
defrosting_with_microwave = 2030
|
||||
@@ -1020,18 +899,15 @@ class SteamOvenMicroProgramId(MieleEnum, missing_to_none=True):
|
||||
gilt_head_bream_fillet = 2220
|
||||
codfish_piece = 2221, 2232
|
||||
codfish_fillet = 2222, 2231
|
||||
trout = 2224
|
||||
pike_fillet = 2225
|
||||
pike_piece = 2226
|
||||
halibut_fillet_2_cm = 2227
|
||||
halibut_fillet_3_cm = 2230
|
||||
carp = 2233
|
||||
salmon_fillet_2_cm = 2234
|
||||
salmon_fillet_3_cm = 2235
|
||||
salmon_steak_2_cm = 2238
|
||||
salmon_steak_3_cm = 2239
|
||||
salmon_piece = 2240
|
||||
salmon_trout = 2241
|
||||
iridescent_shark_fillet = 2244
|
||||
red_snapper_fillet_2_cm = 2245
|
||||
red_snapper_fillet_3_cm = 2248
|
||||
@@ -1268,6 +1144,116 @@ class SteamOvenMicroProgramId(MieleEnum, missing_to_none=True):
|
||||
round_grain_rice_general_rapid_steam_cooking = 3411
|
||||
|
||||
|
||||
class DishWarmerProgramId(MieleEnum, missing_to_none=True):
|
||||
"""Program Id codes for dish warmers."""
|
||||
|
||||
no_program = 0, -1
|
||||
warm_cups_glasses = 1
|
||||
warm_dishes_plates = 2
|
||||
keep_warm = 3
|
||||
slow_roasting = 4
|
||||
|
||||
|
||||
class RobotVacuumCleanerProgramId(MieleEnum, missing_to_none=True):
|
||||
"""Program Id codes for robot vacuum cleaners."""
|
||||
|
||||
no_program = 0, -1
|
||||
auto = 1
|
||||
spot = 2
|
||||
turbo = 3
|
||||
silent = 4
|
||||
|
||||
|
||||
class CoffeeSystemProgramId(MieleEnum, missing_to_none=True):
|
||||
"""Program Id codes for coffee systems."""
|
||||
|
||||
no_program = 0, -1
|
||||
|
||||
check_appliance = 17004
|
||||
|
||||
# profile 1
|
||||
ristretto = 24000, 24032, 24064, 24096, 24128
|
||||
espresso = 24001, 24033, 24065, 24097, 24129
|
||||
coffee = 24002, 24034, 24066, 24098, 24130
|
||||
long_coffee = 24003, 24035, 24067, 24099, 24131
|
||||
cappuccino = 24004, 24036, 24068, 24100, 24132
|
||||
cappuccino_italiano = 24005, 24037, 24069, 24101, 24133
|
||||
latte_macchiato = 24006, 24038, 24070, 24102, 24134
|
||||
espresso_macchiato = 24007, 24039, 24071, 24135
|
||||
cafe_au_lait = 24008, 24040, 24072, 24104, 24136
|
||||
caffe_latte = 24009, 24041, 24073, 24105, 24137
|
||||
flat_white = 24012, 24044, 24076, 24108, 24140
|
||||
very_hot_water = 24013, 24045, 24077, 24109, 24141
|
||||
hot_water = 24014, 24046, 24078, 24110, 24142
|
||||
hot_milk = 24015, 24047, 24079, 24111, 24143
|
||||
milk_foam = 24016, 24048, 24080, 24112, 24144
|
||||
black_tea = 24017, 24049, 24081, 24113, 24145
|
||||
herbal_tea = 24018, 24050, 24082, 24114, 24146
|
||||
fruit_tea = 24019, 24051, 24083, 24115, 24147
|
||||
green_tea = 24020, 24052, 24084, 24116, 24148
|
||||
white_tea = 24021, 24053, 24085, 24117, 24149
|
||||
japanese_tea = 24022, 29054, 24086, 24118, 24150
|
||||
# special programs
|
||||
coffee_pot = 24400
|
||||
barista_assistant = 24407
|
||||
# machine settings menu
|
||||
appliance_settings = (
|
||||
16016, # display brightness
|
||||
16018, # volume
|
||||
16019, # buttons volume
|
||||
16020, # child lock
|
||||
16021, # water hardness
|
||||
16027, # welcome sound
|
||||
16033, # connection status
|
||||
16035, # remote control
|
||||
16037, # remote update
|
||||
24500, # total dispensed
|
||||
24502, # lights appliance on
|
||||
24503, # lights appliance off
|
||||
24504, # turn off lights after
|
||||
24506, # altitude
|
||||
24513, # performance mode
|
||||
24516, # turn off after
|
||||
24537, # advanced mode
|
||||
24542, # tea timer
|
||||
24549, # total coffee dispensed
|
||||
24550, # total tea dispensed
|
||||
24551, # total ristretto
|
||||
24552, # total cappuccino
|
||||
24553, # total espresso
|
||||
24554, # total coffee
|
||||
24555, # total long coffee
|
||||
24556, # total italian cappuccino
|
||||
24557, # total latte macchiato
|
||||
24558, # total caffe latte
|
||||
24560, # total espresso macchiato
|
||||
24562, # total flat white
|
||||
24563, # total coffee with milk
|
||||
24564, # total black tea
|
||||
24565, # total herbal tea
|
||||
24566, # total fruit tea
|
||||
24567, # total green tea
|
||||
24568, # total white tea
|
||||
24569, # total japanese tea
|
||||
24571, # total milk foam
|
||||
24572, # total hot milk
|
||||
24573, # total hot water
|
||||
24574, # total very hot water
|
||||
24575, # counter to descaling
|
||||
24576, # counter to brewing unit degreasing
|
||||
24800, # maintenance
|
||||
24801, # profiles settings menu
|
||||
24813, # add profile
|
||||
)
|
||||
appliance_rinse = 24750, 24759, 24773, 24787, 24788
|
||||
intermediate_rinsing = 24758
|
||||
automatic_maintenance = 24778
|
||||
descaling = 24751
|
||||
brewing_unit_degrease = 24753
|
||||
milk_pipework_rinse = 24754
|
||||
milk_pipework_clean = 24789
|
||||
|
||||
|
||||
PROGRAM_IDS: dict[int, type[MieleEnum]] = {
|
||||
MieleAppliance.WASHING_MACHINE: WashingMachineProgramId,
|
||||
MieleAppliance.TUMBLE_DRYER: TumbleDryerProgramId,
|
||||
@@ -1278,7 +1264,7 @@ PROGRAM_IDS: dict[int, type[MieleEnum]] = {
|
||||
MieleAppliance.STEAM_OVEN_MK2: OvenProgramId,
|
||||
MieleAppliance.STEAM_OVEN: OvenProgramId,
|
||||
MieleAppliance.STEAM_OVEN_COMBI: OvenProgramId,
|
||||
MieleAppliance.STEAM_OVEN_MICRO: SteamOvenMicroProgramId,
|
||||
MieleAppliance.STEAM_OVEN_MICRO: OvenProgramId,
|
||||
MieleAppliance.WASHER_DRYER: WashingMachineProgramId,
|
||||
MieleAppliance.ROBOT_VACUUM_CLEANER: RobotVacuumCleanerProgramId,
|
||||
MieleAppliance.COFFEE_SYSTEM: CoffeeSystemProgramId,
|
||||
|
||||
@@ -474,6 +474,7 @@
|
||||
"drain_spin": "Drain/spin",
|
||||
"drop_cookies_1_tray": "Drop cookies (1 tray)",
|
||||
"drop_cookies_2_trays": "Drop cookies (2 trays)",
|
||||
"drying": "Drying",
|
||||
"duck": "Duck",
|
||||
"dutch_hash": "Dutch hash",
|
||||
"easy_care": "Easy care",
|
||||
@@ -1005,6 +1006,7 @@
|
||||
"heating_up_phase": "Heating up phase",
|
||||
"hot_milk": "Hot milk",
|
||||
"hygiene": "Hygiene",
|
||||
"hygiene_drying": "Hygiene drying",
|
||||
"interim_rinse": "Interim rinse",
|
||||
"keep_warm": "Keep warm",
|
||||
"keeping_warm": "Keeping warm",
|
||||
|
||||
@@ -163,8 +163,6 @@ class MqttDeviceTracker(MqttEntity, TrackerEntity):
|
||||
latitude: float | None
|
||||
longitude: float | None
|
||||
gps_accuracy: float
|
||||
# Reset manually set location to allow automatic zone detection
|
||||
self._attr_location_name = None
|
||||
if isinstance(
|
||||
latitude := extra_state_attributes.get(ATTR_LATITUDE), (int, float)
|
||||
) and isinstance(
|
||||
|
||||
@@ -24,7 +24,7 @@ SUBENTRY_TYPE_ZONE = "zone"
|
||||
|
||||
# Defaults
|
||||
DEFAULT_PORT = 4999
|
||||
DEFAULT_SCAN_INTERVAL = timedelta(minutes=1)
|
||||
DEFAULT_SCAN_INTERVAL = timedelta(seconds=5)
|
||||
DEFAULT_INFER_ARMING_STATE = False
|
||||
DEFAULT_ZONE_TYPE = BinarySensorDeviceClass.MOTION
|
||||
|
||||
|
||||
@@ -19,6 +19,5 @@ async def async_get_config_entry_diagnostics(
|
||||
return {
|
||||
"device_info": client.device_info,
|
||||
"vehicles": client.vehicles,
|
||||
"ct_connected": client.ct_connected,
|
||||
"cap_available": client.cap_available,
|
||||
}
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "cloud_polling",
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["ohme==1.6.0"]
|
||||
"requirements": ["ohme==1.7.0"]
|
||||
}
|
||||
|
||||
@@ -10,5 +10,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["onedrive_personal_sdk"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["onedrive-personal-sdk==0.1.4"]
|
||||
"requirements": ["onedrive-personal-sdk==0.1.7"]
|
||||
}
|
||||
|
||||
@@ -10,5 +10,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["onedrive_personal_sdk"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["onedrive-personal-sdk==0.1.4"]
|
||||
"requirements": ["onedrive-personal-sdk==0.1.7"]
|
||||
}
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/otbr",
|
||||
"integration_type": "service",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["python-otbr-api==2.8.0"]
|
||||
"requirements": ["python-otbr-api==2.9.0"]
|
||||
}
|
||||
|
||||
@@ -159,8 +159,12 @@ class PortainerConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
await self.async_set_unique_id(user_input[CONF_API_TOKEN])
|
||||
self._abort_if_unique_id_configured()
|
||||
# Logic that can be reverted back once the new unique ID is in
|
||||
existing_entry = await self.async_set_unique_id(
|
||||
user_input[CONF_API_TOKEN]
|
||||
)
|
||||
if existing_entry and existing_entry.entry_id != reconf_entry.entry_id:
|
||||
return self.async_abort(reason="already_configured")
|
||||
return self.async_update_reload_and_abort(
|
||||
reconf_entry,
|
||||
data_updates={
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["pyportainer==1.0.31"]
|
||||
"requirements": ["pyportainer==1.0.33"]
|
||||
}
|
||||
|
||||
@@ -2,11 +2,12 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import aiohttp
|
||||
from pyrainbird.async_client import AsyncRainbirdController, CreateController
|
||||
from pyrainbird.async_client import AsyncRainbirdController, create_controller
|
||||
from pyrainbird.exceptions import RainbirdApiException, RainbirdAuthException
|
||||
|
||||
from homeassistant.const import (
|
||||
@@ -26,7 +27,7 @@ from homeassistant.helpers import (
|
||||
from homeassistant.helpers.device_registry import format_mac
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import CONF_SERIAL_NUMBER, DOMAIN
|
||||
from .const import CONF_SERIAL_NUMBER, DOMAIN, TIMEOUT_SECONDS
|
||||
from .coordinator import (
|
||||
RainbirdScheduleUpdateCoordinator,
|
||||
RainbirdUpdateCoordinator,
|
||||
@@ -77,11 +78,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: RainbirdConfigEntry) ->
|
||||
clientsession = async_create_clientsession()
|
||||
_async_register_clientsession_shutdown(hass, entry, clientsession)
|
||||
|
||||
controller = CreateController(
|
||||
clientsession,
|
||||
entry.data[CONF_HOST],
|
||||
entry.data[CONF_PASSWORD],
|
||||
)
|
||||
try:
|
||||
async with asyncio.timeout(TIMEOUT_SECONDS):
|
||||
controller = await create_controller(
|
||||
clientsession,
|
||||
entry.data[CONF_HOST],
|
||||
entry.data[CONF_PASSWORD],
|
||||
)
|
||||
except TimeoutError as err:
|
||||
raise ConfigEntryNotReady from err
|
||||
except RainbirdAuthException as err:
|
||||
raise ConfigEntryAuthFailed from err
|
||||
except RainbirdApiException as err:
|
||||
raise ConfigEntryNotReady from err
|
||||
|
||||
if not (await _async_fix_unique_id(hass, controller, entry)):
|
||||
return False
|
||||
|
||||
@@ -7,7 +7,7 @@ from collections.abc import Mapping
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from pyrainbird.async_client import CreateController
|
||||
from pyrainbird.async_client import create_controller
|
||||
from pyrainbird.data import WifiParams
|
||||
from pyrainbird.exceptions import RainbirdApiException, RainbirdAuthException
|
||||
import voluptuous as vol
|
||||
@@ -137,9 +137,9 @@ class RainbirdConfigFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
Raises a ConfigFlowError on failure.
|
||||
"""
|
||||
clientsession = async_create_clientsession()
|
||||
controller = CreateController(clientsession, host, password)
|
||||
try:
|
||||
async with asyncio.timeout(TIMEOUT_SECONDS):
|
||||
controller = await create_controller(clientsession, host, password)
|
||||
return await asyncio.gather(
|
||||
controller.get_serial_number(),
|
||||
controller.get_wifi_params(),
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["ical"],
|
||||
"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]
|
||||
assert self._username
|
||||
self._client = RoborockApiClient(
|
||||
self._username, session=async_get_clientsession(self.hass)
|
||||
self._username,
|
||||
base_url=entry_data[CONF_BASE_URL],
|
||||
session=async_get_clientsession(self.hass),
|
||||
)
|
||||
return await self.async_step_reauth_confirm()
|
||||
|
||||
|
||||
@@ -34,5 +34,5 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["pysmartthings"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["pysmartthings==3.6.0"]
|
||||
"requirements": ["pysmartthings==3.7.0"]
|
||||
}
|
||||
|
||||
@@ -74,7 +74,7 @@ async def async_setup_entry(
|
||||
radios = coordinator.data.info.radios
|
||||
|
||||
async_add_entities(SmButton(coordinator, button) for button in BUTTONS)
|
||||
entity_created = [False, False]
|
||||
entity_created = [False] * len(radios)
|
||||
|
||||
@callback
|
||||
def _check_router(startup: bool = False) -> None:
|
||||
|
||||
@@ -131,7 +131,7 @@ class SnapcastClientDevice(SnapcastCoordinatorEntity, MediaPlayerEntity):
|
||||
return f"{CLIENT_PREFIX}{host}_{id}"
|
||||
|
||||
@property
|
||||
def _current_group(self) -> Snapgroup:
|
||||
def _current_group(self) -> Snapgroup | None:
|
||||
"""Return the group the client is associated with."""
|
||||
return self._device.group
|
||||
|
||||
@@ -158,12 +158,17 @@ class SnapcastClientDevice(SnapcastCoordinatorEntity, MediaPlayerEntity):
|
||||
def state(self) -> MediaPlayerState | None:
|
||||
"""Return the state of the player."""
|
||||
if self._device.connected:
|
||||
if self.is_volume_muted or self._current_group.muted:
|
||||
if (
|
||||
self.is_volume_muted
|
||||
or self._current_group is None
|
||||
or self._current_group.muted
|
||||
):
|
||||
return MediaPlayerState.IDLE
|
||||
try:
|
||||
return STREAM_STATUS.get(self._current_group.stream_status)
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
return MediaPlayerState.OFF
|
||||
|
||||
@property
|
||||
@@ -182,15 +187,31 @@ class SnapcastClientDevice(SnapcastCoordinatorEntity, MediaPlayerEntity):
|
||||
@property
|
||||
def source(self) -> str | None:
|
||||
"""Return the current input source."""
|
||||
if self._current_group is None:
|
||||
return None
|
||||
|
||||
return self._current_group.stream
|
||||
|
||||
@property
|
||||
def source_list(self) -> list[str]:
|
||||
"""List of available input sources."""
|
||||
if self._current_group is None:
|
||||
return []
|
||||
|
||||
return list(self._current_group.streams_by_name().keys())
|
||||
|
||||
async def async_select_source(self, source: str) -> None:
|
||||
"""Set input source."""
|
||||
if self._current_group is None:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="select_source_no_group",
|
||||
translation_placeholders={
|
||||
"entity_id": self.entity_id,
|
||||
"source": source,
|
||||
},
|
||||
)
|
||||
|
||||
streams = self._current_group.streams_by_name()
|
||||
if source in streams:
|
||||
await self._current_group.set_stream(streams[source].identifier)
|
||||
@@ -233,6 +254,9 @@ class SnapcastClientDevice(SnapcastCoordinatorEntity, MediaPlayerEntity):
|
||||
@property
|
||||
def group_members(self) -> list[str] | None:
|
||||
"""List of player entities which are currently grouped together for synchronous playback."""
|
||||
if self._current_group is None:
|
||||
return None
|
||||
|
||||
entity_registry = er.async_get(self.hass)
|
||||
return [
|
||||
entity_id
|
||||
@@ -248,6 +272,15 @@ class SnapcastClientDevice(SnapcastCoordinatorEntity, MediaPlayerEntity):
|
||||
|
||||
async def async_join_players(self, group_members: list[str]) -> None:
|
||||
"""Add `group_members` to this client's current group."""
|
||||
if self._current_group is None:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="join_players_no_group",
|
||||
translation_placeholders={
|
||||
"entity_id": self.entity_id,
|
||||
},
|
||||
)
|
||||
|
||||
# Get the client entity for each group member excluding self
|
||||
entity_registry = er.async_get(self.hass)
|
||||
clients = [
|
||||
@@ -271,13 +304,25 @@ class SnapcastClientDevice(SnapcastCoordinatorEntity, MediaPlayerEntity):
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_unjoin_player(self) -> None:
|
||||
"""Remove this client from it's current group."""
|
||||
"""Remove this client from its current group."""
|
||||
if self._current_group is None:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="unjoin_no_group",
|
||||
translation_placeholders={
|
||||
"entity_id": self.entity_id,
|
||||
},
|
||||
)
|
||||
|
||||
await self._current_group.remove_client(self._device.identifier)
|
||||
self.async_write_ha_state()
|
||||
|
||||
@property
|
||||
def metadata(self) -> Mapping[str, Any]:
|
||||
"""Get metadata from the current stream."""
|
||||
if self._current_group is None:
|
||||
return {}
|
||||
|
||||
try:
|
||||
if metadata := self.coordinator.server.stream(
|
||||
self._current_group.stream
|
||||
@@ -341,6 +386,9 @@ class SnapcastClientDevice(SnapcastCoordinatorEntity, MediaPlayerEntity):
|
||||
@property
|
||||
def media_position(self) -> int | None:
|
||||
"""Position of current playing media in seconds."""
|
||||
if self._current_group is None:
|
||||
return None
|
||||
|
||||
try:
|
||||
# Position is part of properties object, not metadata object
|
||||
if properties := self.coordinator.server.stream(
|
||||
|
||||
@@ -21,6 +21,17 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"join_players_no_group": {
|
||||
"message": "Client {entity_id} has no group. Unable to join players."
|
||||
},
|
||||
"select_source_no_group": {
|
||||
"message": "Client {entity_id} has no group. Unable to select source {source}."
|
||||
},
|
||||
"unjoin_no_group": {
|
||||
"message": "Client {entity_id} has no group. Unable to unjoin player."
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"restore": {
|
||||
"description": "Restores a previously taken snapshot of a media player.",
|
||||
|
||||
@@ -118,7 +118,6 @@ class BrowsableMedia(StrEnum):
|
||||
CURRENT_USER_RECENTLY_PLAYED = "current_user_recently_played"
|
||||
CURRENT_USER_TOP_ARTISTS = "current_user_top_artists"
|
||||
CURRENT_USER_TOP_TRACKS = "current_user_top_tracks"
|
||||
NEW_RELEASES = "new_releases"
|
||||
|
||||
|
||||
LIBRARY_MAP = {
|
||||
@@ -130,7 +129,6 @@ LIBRARY_MAP = {
|
||||
BrowsableMedia.CURRENT_USER_RECENTLY_PLAYED.value: "Recently played",
|
||||
BrowsableMedia.CURRENT_USER_TOP_ARTISTS.value: "Top Artists",
|
||||
BrowsableMedia.CURRENT_USER_TOP_TRACKS.value: "Top Tracks",
|
||||
BrowsableMedia.NEW_RELEASES.value: "New Releases",
|
||||
}
|
||||
|
||||
CONTENT_TYPE_MEDIA_CLASS: dict[str, Any] = {
|
||||
@@ -166,10 +164,6 @@ CONTENT_TYPE_MEDIA_CLASS: dict[str, Any] = {
|
||||
"parent": MediaClass.DIRECTORY,
|
||||
"children": MediaClass.TRACK,
|
||||
},
|
||||
BrowsableMedia.NEW_RELEASES.value: {
|
||||
"parent": MediaClass.DIRECTORY,
|
||||
"children": MediaClass.ALBUM,
|
||||
},
|
||||
MediaType.PLAYLIST: {
|
||||
"parent": MediaClass.PLAYLIST,
|
||||
"children": MediaClass.TRACK,
|
||||
@@ -356,14 +350,11 @@ async def build_item_response( # noqa: C901
|
||||
elif media_content_type == BrowsableMedia.CURRENT_USER_TOP_TRACKS:
|
||||
if top_tracks := await spotify.get_top_tracks():
|
||||
items = [_get_track_item_payload(track) for track in top_tracks]
|
||||
elif media_content_type == BrowsableMedia.NEW_RELEASES:
|
||||
if new_releases := await spotify.get_new_releases():
|
||||
items = [_get_album_item_payload(album) for album in new_releases]
|
||||
elif media_content_type == MediaType.PLAYLIST:
|
||||
if playlist := await spotify.get_playlist(media_content_id):
|
||||
title = playlist.name
|
||||
image = playlist.images[0].url if playlist.images else None
|
||||
for playlist_item in playlist.tracks.items:
|
||||
for playlist_item in playlist.items.items:
|
||||
if playlist_item.track.type is ItemType.TRACK:
|
||||
if TYPE_CHECKING:
|
||||
assert isinstance(playlist_item.track, Track)
|
||||
|
||||
@@ -6,7 +6,7 @@ from collections.abc import Mapping
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from spotifyaio import SpotifyClient
|
||||
from spotifyaio import SpotifyClient, SpotifyForbiddenError
|
||||
|
||||
from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlowResult
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_NAME, CONF_TOKEN
|
||||
@@ -41,6 +41,9 @@ class SpotifyFlowHandler(
|
||||
|
||||
try:
|
||||
current_user = await spotify.get_current_user()
|
||||
except SpotifyForbiddenError:
|
||||
self.logger.exception("User is not subscribed to Spotify")
|
||||
return self.async_abort(reason="user_not_premium")
|
||||
except Exception:
|
||||
self.logger.exception("Error while connecting to Spotify")
|
||||
return self.async_abort(reason="connection_error")
|
||||
|
||||
@@ -11,12 +11,15 @@ from spotifyaio import (
|
||||
Playlist,
|
||||
SpotifyClient,
|
||||
SpotifyConnectionError,
|
||||
SpotifyForbiddenError,
|
||||
SpotifyNotFoundError,
|
||||
UserProfile,
|
||||
)
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryError
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
@@ -33,6 +36,11 @@ type SpotifyConfigEntry = ConfigEntry[SpotifyData]
|
||||
|
||||
UPDATE_INTERVAL = timedelta(seconds=30)
|
||||
|
||||
FREE_API_BLOGPOST = (
|
||||
"https://developer.spotify.com/blog/"
|
||||
"2026-02-06-update-on-developer-access-and-platform-security"
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class SpotifyCoordinatorData:
|
||||
@@ -78,6 +86,19 @@ class SpotifyCoordinator(DataUpdateCoordinator[SpotifyCoordinatorData]):
|
||||
"""Set up the coordinator."""
|
||||
try:
|
||||
self.current_user = await self.client.get_current_user()
|
||||
except SpotifyForbiddenError as err:
|
||||
async_create_issue(
|
||||
self.hass,
|
||||
DOMAIN,
|
||||
f"user_not_premium_{self.config_entry.unique_id}",
|
||||
is_fixable=False,
|
||||
issue_domain=DOMAIN,
|
||||
severity=IssueSeverity.ERROR,
|
||||
translation_key="user_not_premium",
|
||||
translation_placeholders={"entry_title": self.config_entry.title},
|
||||
learn_more_url=FREE_API_BLOGPOST,
|
||||
)
|
||||
raise ConfigEntryError("User is not subscribed to Spotify") from err
|
||||
except SpotifyConnectionError as err:
|
||||
raise UpdateFailed("Error communicating with Spotify API") from err
|
||||
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["spotifyaio"],
|
||||
"requirements": ["spotifyaio==1.0.0"]
|
||||
"requirements": ["spotifyaio==2.0.2"]
|
||||
}
|
||||
|
||||
@@ -14,10 +14,10 @@ from spotifyaio import (
|
||||
Item,
|
||||
ItemType,
|
||||
PlaybackState,
|
||||
ProductType,
|
||||
RepeatMode as SpotifyRepeatMode,
|
||||
Track,
|
||||
)
|
||||
from spotifyaio.models import ProductType
|
||||
from yarl import URL
|
||||
|
||||
from homeassistant.components.media_player import (
|
||||
@@ -222,7 +222,7 @@ class SpotifyMediaPlayer(SpotifyEntity, MediaPlayerEntity):
|
||||
if item.type == ItemType.EPISODE:
|
||||
if TYPE_CHECKING:
|
||||
assert isinstance(item, Episode)
|
||||
return item.show.publisher
|
||||
return item.show.name
|
||||
|
||||
if TYPE_CHECKING:
|
||||
assert isinstance(item, Track)
|
||||
@@ -230,12 +230,10 @@ class SpotifyMediaPlayer(SpotifyEntity, MediaPlayerEntity):
|
||||
|
||||
@property
|
||||
@ensure_item
|
||||
def media_album_name(self, item: Item) -> str: # noqa: PLR0206
|
||||
def media_album_name(self, item: Item) -> str | None: # noqa: PLR0206
|
||||
"""Return the media album."""
|
||||
if item.type == ItemType.EPISODE:
|
||||
if TYPE_CHECKING:
|
||||
assert isinstance(item, Episode)
|
||||
return item.show.name
|
||||
return None
|
||||
|
||||
if TYPE_CHECKING:
|
||||
assert isinstance(item, Track)
|
||||
|
||||
@@ -12,7 +12,8 @@
|
||||
"oauth_timeout": "[%key:common::config_flow::abort::oauth2_timeout%]",
|
||||
"oauth_unauthorized": "[%key:common::config_flow::abort::oauth2_unauthorized%]",
|
||||
"reauth_account_mismatch": "The Spotify account authenticated with does not match the account that needed re-authentication.",
|
||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
|
||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
|
||||
"user_not_premium": "The Spotify API has been changed and Developer applications created with a free account can no longer access the API. To continue using the Spotify integration, you should use an Spotify Developer application created with a Spotify Premium account, or upgrade to Spotify Premium."
|
||||
},
|
||||
"create_entry": {
|
||||
"default": "Successfully authenticated with Spotify."
|
||||
@@ -41,6 +42,12 @@
|
||||
"message": "[%key:common::exceptions::oauth2_implementation_unavailable::message%]"
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"user_not_premium": {
|
||||
"description": "[%key:component::spotify::config::abort::user_not_premium%]",
|
||||
"title": "Spotify integration requires a Spotify Premium account"
|
||||
}
|
||||
},
|
||||
"system_health": {
|
||||
"info": {
|
||||
"api_endpoint_reachable": "Spotify API endpoint reachable"
|
||||
|
||||
@@ -8,6 +8,6 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["aioswitcher"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["aioswitcher==6.1.0"],
|
||||
"requirements": ["aioswitcher==6.1.1"],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
||||
@@ -15,5 +15,5 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["teltasync==0.1.3"]
|
||||
"requirements": ["teltasync==0.2.0"]
|
||||
}
|
||||
|
||||
@@ -266,13 +266,20 @@ class TemplateEntity(AbstractTemplateEntity):
|
||||
|
||||
def _get_this_variable(self) -> TemplateStateFromEntityId:
|
||||
"""Create a this variable for the entity."""
|
||||
entity_id = self.entity_id
|
||||
if self._preview_callback:
|
||||
preview_entity_id = async_generate_entity_id(
|
||||
self._entity_id_format, self._attr_name or "preview", hass=self.hass
|
||||
)
|
||||
return TemplateStateFromEntityId(self.hass, preview_entity_id)
|
||||
# During config flow, the registry entry and entity_id will be None. In this scenario,
|
||||
# a temporary entity_id is created.
|
||||
# During option flow, the preview entity_id will be None, however the registry entry
|
||||
# will contain the target entity_id.
|
||||
if self.registry_entry:
|
||||
entity_id = self.registry_entry.entity_id
|
||||
else:
|
||||
entity_id = async_generate_entity_id(
|
||||
self._entity_id_format, self._attr_name or "preview", hass=self.hass
|
||||
)
|
||||
|
||||
return TemplateStateFromEntityId(self.hass, self.entity_id)
|
||||
return TemplateStateFromEntityId(self.hass, entity_id)
|
||||
|
||||
def _render_script_variables(self) -> dict[str, Any]:
|
||||
"""Render configured variables."""
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/thread",
|
||||
"integration_type": "service",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["python-otbr-api==2.8.0", "pyroute2==0.7.5"],
|
||||
"requirements": ["python-otbr-api==2.9.0", "pyroute2==0.7.5"],
|
||||
"single_config_entry": true,
|
||||
"zeroconf": ["_meshcop._udp.local."]
|
||||
}
|
||||
|
||||
@@ -81,6 +81,7 @@ clean_area:
|
||||
selector:
|
||||
area:
|
||||
multiple: true
|
||||
reorder: true
|
||||
|
||||
send_command:
|
||||
target:
|
||||
|
||||
@@ -398,7 +398,7 @@ SENSOR_DESCRIPTIONS = {
|
||||
Keys.WARNING: VictronBLESensorEntityDescription(
|
||||
key=Keys.WARNING,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
translation_key="alarm",
|
||||
translation_key="warning",
|
||||
options=ALARM_OPTIONS,
|
||||
),
|
||||
Keys.YIELD_TODAY: VictronBLESensorEntityDescription(
|
||||
|
||||
@@ -248,7 +248,24 @@
|
||||
"name": "[%key:component::victron_ble::common::starter_voltage%]"
|
||||
},
|
||||
"warning": {
|
||||
"name": "Warning"
|
||||
"name": "Warning",
|
||||
"state": {
|
||||
"bms_lockout": "[%key:component::victron_ble::entity::sensor::alarm::state::bms_lockout%]",
|
||||
"dc_ripple": "[%key:component::victron_ble::entity::sensor::alarm::state::dc_ripple%]",
|
||||
"high_starter_voltage": "[%key:component::victron_ble::entity::sensor::alarm::state::high_starter_voltage%]",
|
||||
"high_temperature": "[%key:component::victron_ble::entity::sensor::alarm::state::high_temperature%]",
|
||||
"high_v_ac_out": "[%key:component::victron_ble::entity::sensor::alarm::state::high_v_ac_out%]",
|
||||
"high_voltage": "[%key:component::victron_ble::entity::sensor::alarm::state::high_voltage%]",
|
||||
"low_soc": "[%key:component::victron_ble::entity::sensor::alarm::state::low_soc%]",
|
||||
"low_starter_voltage": "[%key:component::victron_ble::entity::sensor::alarm::state::low_starter_voltage%]",
|
||||
"low_temperature": "[%key:component::victron_ble::entity::sensor::alarm::state::low_temperature%]",
|
||||
"low_v_ac_out": "[%key:component::victron_ble::entity::sensor::alarm::state::low_v_ac_out%]",
|
||||
"low_voltage": "[%key:component::victron_ble::entity::sensor::alarm::state::low_voltage%]",
|
||||
"mid_voltage": "[%key:component::victron_ble::common::midpoint_voltage%]",
|
||||
"no_alarm": "[%key:component::victron_ble::entity::sensor::alarm::state::no_alarm%]",
|
||||
"overload": "[%key:component::victron_ble::entity::sensor::alarm::state::overload%]",
|
||||
"short_circuit": "[%key:component::victron_ble::entity::sensor::alarm::state::short_circuit%]"
|
||||
}
|
||||
},
|
||||
"yield_today": {
|
||||
"name": "Yield today"
|
||||
|
||||
@@ -78,6 +78,7 @@ class VodafoneStationRouter(DataUpdateCoordinator[UpdateCoordinatorDataType]):
|
||||
data,
|
||||
session,
|
||||
)
|
||||
self._session = session
|
||||
|
||||
# Last resort as no MAC or S/N can be retrieved via API
|
||||
self._id = config_entry.unique_id
|
||||
@@ -135,11 +136,15 @@ class VodafoneStationRouter(DataUpdateCoordinator[UpdateCoordinatorDataType]):
|
||||
_LOGGER.debug("Polling Vodafone Station host: %s", self.api.base_url.host)
|
||||
|
||||
try:
|
||||
await self.api.login()
|
||||
if not self._session.cookie_jar.filter_cookies(self.api.base_url):
|
||||
_LOGGER.debug(
|
||||
"Session cookies missing for host %s, re-login",
|
||||
self.api.base_url.host,
|
||||
)
|
||||
await self.api.login()
|
||||
raw_data_devices = await self.api.get_devices_data()
|
||||
data_sensors = await self.api.get_sensor_data()
|
||||
data_wifi = await self.api.get_wifi_data()
|
||||
await self.api.logout()
|
||||
except exceptions.CannotAuthenticate as err:
|
||||
raise ConfigEntryAuthFailed(
|
||||
translation_domain=DOMAIN,
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["aiovodafone"],
|
||||
"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(
|
||||
status, self.entity_description.typology, self.entity_description.band
|
||||
)
|
||||
await self.coordinator.async_request_refresh()
|
||||
except CannotAuthenticate as err:
|
||||
self.coordinator.config_entry.async_start_reauth(self.hass)
|
||||
raise HomeAssistantError(
|
||||
|
||||
@@ -5,7 +5,7 @@ from __future__ import annotations
|
||||
from pathlib import Path
|
||||
from typing import cast
|
||||
|
||||
from aiohttp import ClientResponseError
|
||||
from aiohttp import ClientError
|
||||
from yalexs.const import Brand
|
||||
from yalexs.exceptions import YaleApiError
|
||||
from yalexs.manager.const import CONF_BRAND
|
||||
@@ -15,7 +15,12 @@ from yalexs.manager.gateway import Config as YaleXSConfig
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.exceptions import (
|
||||
ConfigEntryAuthFailed,
|
||||
ConfigEntryNotReady,
|
||||
OAuth2TokenRequestError,
|
||||
OAuth2TokenRequestReauthError,
|
||||
)
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.config_entry_oauth2_flow import (
|
||||
ImplementationUnavailableError,
|
||||
@@ -42,11 +47,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: YaleConfigEntry) -> bool
|
||||
yale_gateway = YaleGateway(Path(hass.config.config_dir), session, oauth_session)
|
||||
try:
|
||||
await async_setup_yale(hass, entry, yale_gateway)
|
||||
except OAuth2TokenRequestReauthError as err:
|
||||
raise ConfigEntryAuthFailed from err
|
||||
except (RequireValidation, InvalidAuth) as err:
|
||||
raise ConfigEntryAuthFailed from err
|
||||
except TimeoutError as err:
|
||||
raise ConfigEntryNotReady("Timed out connecting to yale api") from err
|
||||
except (YaleApiError, ClientResponseError, CannotConnect) as err:
|
||||
except (
|
||||
YaleApiError,
|
||||
OAuth2TokenRequestError,
|
||||
ClientError,
|
||||
CannotConnect,
|
||||
) as err:
|
||||
raise ConfigEntryNotReady from err
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
return True
|
||||
|
||||
@@ -14,5 +14,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["socketio", "engineio", "yalexs"],
|
||||
"requirements": ["yalexs==9.2.0", "yalexs-ble==3.2.7"]
|
||||
"requirements": ["yalexs==9.2.0", "yalexs-ble==3.2.8"]
|
||||
}
|
||||
|
||||
@@ -13,5 +13,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/yalexs_ble",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["yalexs-ble==3.2.7"]
|
||||
"requirements": ["yalexs-ble==3.2.8"]
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
"universal_silabs_flasher",
|
||||
"serialx"
|
||||
],
|
||||
"requirements": ["zha==1.0.1", "serialx==0.6.2"],
|
||||
"requirements": ["zha==1.0.2", "serialx==0.6.2"],
|
||||
"usb": [
|
||||
{
|
||||
"description": "*2652*",
|
||||
|
||||
@@ -87,6 +87,9 @@ class CoverPositionMixin(ZWaveBaseEntity, CoverEntity):
|
||||
_current_position_value: ZwaveValue | None = None
|
||||
_target_position_value: ZwaveValue | None = None
|
||||
_stop_position_value: ZwaveValue | None = None
|
||||
# Keep track of the target position for legacy devices
|
||||
# that don't include the targetValue in their reports.
|
||||
_commanded_target_position: int | None = None
|
||||
|
||||
def _set_position_values(
|
||||
self,
|
||||
@@ -153,12 +156,19 @@ class CoverPositionMixin(ZWaveBaseEntity, CoverEntity):
|
||||
if not self._attr_is_opening and not self._attr_is_closing:
|
||||
return
|
||||
|
||||
if (
|
||||
(current := self._current_position_value) is not None
|
||||
and (target := self._target_position_value) is not None
|
||||
and current.value is not None
|
||||
and current.value == target.value
|
||||
):
|
||||
if (current := self._current_position_value) is None or current.value is None:
|
||||
return
|
||||
|
||||
# Prefer the Z-Wave targetValue property when the device reports it.
|
||||
# Legacy multilevel switches only report currentValue, so fall back to
|
||||
# the target position we commanded when targetValue is not available.
|
||||
target_val = (
|
||||
t.value
|
||||
if (t := self._target_position_value) is not None and t.value is not None
|
||||
else self._commanded_target_position
|
||||
)
|
||||
|
||||
if target_val is not None and current.value == target_val:
|
||||
self._attr_is_opening = False
|
||||
self._attr_is_closing = False
|
||||
|
||||
@@ -203,6 +213,8 @@ class CoverPositionMixin(ZWaveBaseEntity, CoverEntity):
|
||||
else:
|
||||
return
|
||||
|
||||
self._commanded_target_position = target_position
|
||||
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_set_cover_position(self, **kwargs: Any) -> None:
|
||||
|
||||
@@ -17,7 +17,7 @@ if TYPE_CHECKING:
|
||||
APPLICATION_NAME: Final = "HomeAssistant"
|
||||
MAJOR_VERSION: Final = 2026
|
||||
MINOR_VERSION: Final = 3
|
||||
PATCH_VERSION: Final = "0"
|
||||
PATCH_VERSION: Final = "2"
|
||||
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
||||
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
|
||||
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 14, 2)
|
||||
|
||||
@@ -87,6 +87,12 @@ async def _ssrf_redirect_middleware(
|
||||
# Relative redirects stay on the same host - always safe
|
||||
return resp
|
||||
|
||||
# Only schemes that aiohttp can open a network connection for need
|
||||
# SSRF protection. Custom app URI schemes (e.g. weconnect://) are inert
|
||||
# from a networking perspective and must not be blocked.
|
||||
if connector and redirect_url.scheme not in connector.allowed_protocol_schema_set:
|
||||
return resp
|
||||
|
||||
host = redirect_url.host
|
||||
if await _async_is_blocked_host(host, connector):
|
||||
resp.close()
|
||||
|
||||
@@ -181,15 +181,24 @@ class RestoreStateData:
|
||||
}
|
||||
|
||||
# Start with the currently registered states
|
||||
stored_states = [
|
||||
StoredState(
|
||||
current_states_by_entity_id[entity_id],
|
||||
entity.extra_restore_state_data,
|
||||
now,
|
||||
stored_states: list[StoredState] = []
|
||||
for entity_id, entity in self.entities.items():
|
||||
if entity_id not in current_states_by_entity_id:
|
||||
continue
|
||||
try:
|
||||
extra_data = entity.extra_restore_state_data
|
||||
except Exception:
|
||||
_LOGGER.exception(
|
||||
"Error getting extra restore state data for %s", entity_id
|
||||
)
|
||||
continue
|
||||
stored_states.append(
|
||||
StoredState(
|
||||
current_states_by_entity_id[entity_id],
|
||||
extra_data,
|
||||
now,
|
||||
)
|
||||
)
|
||||
for entity_id, entity in self.entities.items()
|
||||
if entity_id in current_states_by_entity_id
|
||||
]
|
||||
expiration_time = now - STATE_EXPIRATION
|
||||
|
||||
for entity_id, stored_state in self.last_states.items():
|
||||
@@ -219,6 +228,8 @@ class RestoreStateData:
|
||||
)
|
||||
except HomeAssistantError as exc:
|
||||
_LOGGER.error("Error saving current states", exc_info=exc)
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected error saving current states")
|
||||
|
||||
@callback
|
||||
def async_setup_dump(self, *args: Any) -> None:
|
||||
@@ -258,13 +269,15 @@ class RestoreStateData:
|
||||
|
||||
@callback
|
||||
def async_restore_entity_removed(
|
||||
self, entity_id: str, extra_data: ExtraStoredData | None
|
||||
self,
|
||||
entity_id: str,
|
||||
state: State | None,
|
||||
extra_data: ExtraStoredData | None,
|
||||
) -> None:
|
||||
"""Unregister this entity from saving state."""
|
||||
# When an entity is being removed from hass, store its last state. This
|
||||
# allows us to support state restoration if the entity is removed, then
|
||||
# re-added while hass is still running.
|
||||
state = self.hass.states.get(entity_id)
|
||||
# To fully mimic all the attribute data types when loaded from storage,
|
||||
# we're going to serialize it to JSON and then re-load it.
|
||||
if state is not None:
|
||||
@@ -287,8 +300,18 @@ class RestoreEntity(Entity):
|
||||
|
||||
async def async_internal_will_remove_from_hass(self) -> None:
|
||||
"""Run when entity will be removed from hass."""
|
||||
try:
|
||||
extra_data = self.extra_restore_state_data
|
||||
except Exception:
|
||||
_LOGGER.exception(
|
||||
"Error getting extra restore state data for %s", self.entity_id
|
||||
)
|
||||
state = None
|
||||
extra_data = None
|
||||
else:
|
||||
state = self.hass.states.get(self.entity_id)
|
||||
async_get(self.hass).async_restore_entity_removed(
|
||||
self.entity_id, self.extra_restore_state_data
|
||||
self.entity_id, state, extra_data
|
||||
)
|
||||
await super().async_internal_will_remove_from_hass()
|
||||
|
||||
|
||||
@@ -301,6 +301,7 @@ class AreaSelectorConfig(BaseSelectorConfig, total=False):
|
||||
entity: EntityFilterSelectorConfig | list[EntityFilterSelectorConfig]
|
||||
device: DeviceFilterSelectorConfig | list[DeviceFilterSelectorConfig]
|
||||
multiple: bool
|
||||
reorder: bool
|
||||
|
||||
|
||||
@SELECTORS.register("area")
|
||||
@@ -320,6 +321,7 @@ class AreaSelector(Selector[AreaSelectorConfig]):
|
||||
[DEVICE_FILTER_SELECTOR_CONFIG_SCHEMA],
|
||||
),
|
||||
vol.Optional("multiple", default=False): cv.boolean,
|
||||
vol.Optional("reorder", default=False): cv.boolean,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ habluetooth==5.8.0
|
||||
hass-nabucasa==1.15.0
|
||||
hassil==3.5.0
|
||||
home-assistant-bluetooth==1.13.1
|
||||
home-assistant-frontend==20260304.0
|
||||
home-assistant-frontend==20260312.0
|
||||
home-assistant-intents==2026.3.3
|
||||
httpx==0.28.1
|
||||
ifaddr==0.2.0
|
||||
@@ -48,7 +48,7 @@ Jinja2==3.1.6
|
||||
lru-dict==1.3.0
|
||||
mutagen==1.47.0
|
||||
openai==2.21.0
|
||||
orjson==3.11.5
|
||||
orjson==3.11.7
|
||||
packaging>=23.1
|
||||
paho-mqtt==2.1.0
|
||||
Pillow==12.1.1
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "homeassistant"
|
||||
version = "2026.3.0"
|
||||
version = "2026.3.2"
|
||||
license = "Apache-2.0"
|
||||
license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"]
|
||||
description = "Open-source home automation platform running on Python 3."
|
||||
@@ -65,7 +65,7 @@ dependencies = [
|
||||
"Pillow==12.1.1",
|
||||
"propcache==0.4.1",
|
||||
"pyOpenSSL==25.3.0",
|
||||
"orjson==3.11.5",
|
||||
"orjson==3.11.7",
|
||||
"packaging>=23.1",
|
||||
"psutil-home-assistant==0.0.1",
|
||||
"python-slugify==8.0.4",
|
||||
|
||||
2
requirements.txt
generated
2
requirements.txt
generated
@@ -34,7 +34,7 @@ ifaddr==0.2.0
|
||||
Jinja2==3.1.6
|
||||
lru-dict==1.3.0
|
||||
mutagen==1.47.0
|
||||
orjson==3.11.5
|
||||
orjson==3.11.7
|
||||
packaging>=23.1
|
||||
Pillow==12.1.1
|
||||
propcache==0.4.1
|
||||
|
||||
44
requirements_all.txt
generated
44
requirements_all.txt
generated
@@ -47,7 +47,7 @@ PlexAPI==4.15.16
|
||||
ProgettiHWSW==0.1.3
|
||||
|
||||
# homeassistant.components.cast
|
||||
PyChromecast==14.0.9
|
||||
PyChromecast==14.0.10
|
||||
|
||||
# homeassistant.components.flume
|
||||
PyFlume==0.6.5
|
||||
@@ -190,7 +190,7 @@ aioairzone-cloud==0.7.2
|
||||
aioairzone==1.0.5
|
||||
|
||||
# homeassistant.components.alexa_devices
|
||||
aioamazondevices==13.0.0
|
||||
aioamazondevices==13.0.1
|
||||
|
||||
# homeassistant.components.ambient_network
|
||||
# homeassistant.components.ambient_station
|
||||
@@ -224,7 +224,7 @@ aiobafi6==0.9.0
|
||||
aiobotocore==2.21.1
|
||||
|
||||
# homeassistant.components.comelit
|
||||
aiocomelit==2.0.0
|
||||
aiocomelit==2.0.1
|
||||
|
||||
# homeassistant.components.dhcp
|
||||
aiodhcpwatcher==1.2.1
|
||||
@@ -413,7 +413,7 @@ aiosteamist==1.0.1
|
||||
aiostreammagic==2.13.0
|
||||
|
||||
# homeassistant.components.switcher_kis
|
||||
aioswitcher==6.1.0
|
||||
aioswitcher==6.1.1
|
||||
|
||||
# homeassistant.components.syncthing
|
||||
aiosyncthing==0.7.1
|
||||
@@ -437,7 +437,7 @@ aiousbwatcher==1.1.1
|
||||
aiovlc==0.5.1
|
||||
|
||||
# homeassistant.components.vodafone_station
|
||||
aiovodafone==3.1.2
|
||||
aiovodafone==3.1.3
|
||||
|
||||
# homeassistant.components.waqi
|
||||
aiowaqi==3.1.0
|
||||
@@ -556,7 +556,7 @@ async-upnp-client==0.46.2
|
||||
asyncarve==0.1.1
|
||||
|
||||
# homeassistant.components.keyboard_remote
|
||||
asyncinotify==4.2.0
|
||||
asyncinotify==4.4.0
|
||||
|
||||
# homeassistant.components.supla
|
||||
asyncpysupla==0.0.5
|
||||
@@ -939,7 +939,7 @@ eternalegypt==0.0.18
|
||||
eufylife-ble-client==0.1.8
|
||||
|
||||
# homeassistant.components.keyboard_remote
|
||||
# evdev==1.6.1
|
||||
# evdev==1.9.3
|
||||
|
||||
# homeassistant.components.evohome
|
||||
evohome-async==1.1.3
|
||||
@@ -1122,7 +1122,7 @@ gotailwind==0.3.0
|
||||
govee-ble==0.44.0
|
||||
|
||||
# homeassistant.components.govee_light_local
|
||||
govee-local-api==2.3.0
|
||||
govee-local-api==2.4.0
|
||||
|
||||
# homeassistant.components.remote_rpi_gpio
|
||||
gpiozero==1.6.2
|
||||
@@ -1226,7 +1226,7 @@ hole==0.9.0
|
||||
holidays==0.84
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20260304.0
|
||||
home-assistant-frontend==20260312.0
|
||||
|
||||
# homeassistant.components.conversation
|
||||
home-assistant-intents==2026.3.3
|
||||
@@ -1271,7 +1271,7 @@ ibeacon-ble==1.2.0
|
||||
# homeassistant.components.local_calendar
|
||||
# homeassistant.components.local_todo
|
||||
# homeassistant.components.remote_calendar
|
||||
ical==13.2.0
|
||||
ical==13.2.2
|
||||
|
||||
# homeassistant.components.caldav
|
||||
icalendar==6.3.1
|
||||
@@ -1663,7 +1663,7 @@ odp-amsterdam==6.1.2
|
||||
oemthermostat==1.1.1
|
||||
|
||||
# homeassistant.components.ohme
|
||||
ohme==1.6.0
|
||||
ohme==1.7.0
|
||||
|
||||
# homeassistant.components.ollama
|
||||
ollama==0.5.1
|
||||
@@ -1676,7 +1676,7 @@ ondilo==0.5.0
|
||||
|
||||
# homeassistant.components.onedrive
|
||||
# homeassistant.components.onedrive_for_business
|
||||
onedrive-personal-sdk==0.1.4
|
||||
onedrive-personal-sdk==0.1.7
|
||||
|
||||
# homeassistant.components.onvif
|
||||
onvif-zeep-async==4.0.4
|
||||
@@ -1938,7 +1938,7 @@ pyairobotrest==0.3.0
|
||||
pyairvisual==2023.08.1
|
||||
|
||||
# homeassistant.components.anglian_water
|
||||
pyanglianwater==3.1.0
|
||||
pyanglianwater==3.1.1
|
||||
|
||||
# homeassistant.components.aprilaire
|
||||
pyaprilaire==0.9.1
|
||||
@@ -2179,7 +2179,7 @@ pyitachip2ir==0.0.7
|
||||
pyituran==0.1.5
|
||||
|
||||
# homeassistant.components.jvc_projector
|
||||
pyjvcprojector==2.0.1
|
||||
pyjvcprojector==2.0.3
|
||||
|
||||
# homeassistant.components.kaleidescape
|
||||
pykaleidescape==1.1.3
|
||||
@@ -2370,7 +2370,7 @@ pyplaato==0.0.19
|
||||
pypoint==3.0.0
|
||||
|
||||
# homeassistant.components.portainer
|
||||
pyportainer==1.0.31
|
||||
pyportainer==1.0.33
|
||||
|
||||
# homeassistant.components.probe_plus
|
||||
pyprobeplus==1.1.2
|
||||
@@ -2473,7 +2473,7 @@ pysmappee==0.2.29
|
||||
pysmarlaapi==1.0.1
|
||||
|
||||
# homeassistant.components.smartthings
|
||||
pysmartthings==3.6.0
|
||||
pysmartthings==3.7.0
|
||||
|
||||
# homeassistant.components.smarty
|
||||
pysmarty2==0.10.3
|
||||
@@ -2530,7 +2530,7 @@ python-awair==0.2.5
|
||||
python-blockchain-api==0.0.2
|
||||
|
||||
# homeassistant.components.bsblan
|
||||
python-bsblan==5.1.0
|
||||
python-bsblan==5.1.2
|
||||
|
||||
# homeassistant.components.citybikes
|
||||
python-citybikes==0.3.3
|
||||
@@ -2612,7 +2612,7 @@ python-opensky==1.0.1
|
||||
|
||||
# homeassistant.components.otbr
|
||||
# homeassistant.components.thread
|
||||
python-otbr-api==2.8.0
|
||||
python-otbr-api==2.9.0
|
||||
|
||||
# homeassistant.components.overseerr
|
||||
python-overseerr==0.9.0
|
||||
@@ -2969,7 +2969,7 @@ speak2mary==1.4.0
|
||||
speedtest-cli==2.1.3
|
||||
|
||||
# homeassistant.components.spotify
|
||||
spotifyaio==1.0.0
|
||||
spotifyaio==2.0.2
|
||||
|
||||
# homeassistant.components.sql
|
||||
sqlparse==0.5.5
|
||||
@@ -3038,7 +3038,7 @@ tellcore-py==1.1.2
|
||||
tellduslive==0.10.12
|
||||
|
||||
# homeassistant.components.teltonika
|
||||
teltasync==0.1.3
|
||||
teltasync==0.2.0
|
||||
|
||||
# homeassistant.components.lg_soundbar
|
||||
temescal==0.5
|
||||
@@ -3307,7 +3307,7 @@ yalesmartalarmclient==0.4.3
|
||||
# homeassistant.components.august
|
||||
# homeassistant.components.yale
|
||||
# homeassistant.components.yalexs_ble
|
||||
yalexs-ble==3.2.7
|
||||
yalexs-ble==3.2.8
|
||||
|
||||
# homeassistant.components.august
|
||||
# homeassistant.components.yale
|
||||
@@ -3347,7 +3347,7 @@ zeroconf==0.148.0
|
||||
zeversolar==0.3.2
|
||||
|
||||
# homeassistant.components.zha
|
||||
zha==1.0.1
|
||||
zha==1.0.2
|
||||
|
||||
# homeassistant.components.zhong_hong
|
||||
zhong-hong-hvac==1.0.13
|
||||
|
||||
40
requirements_test_all.txt
generated
40
requirements_test_all.txt
generated
@@ -47,7 +47,7 @@ PlexAPI==4.15.16
|
||||
ProgettiHWSW==0.1.3
|
||||
|
||||
# homeassistant.components.cast
|
||||
PyChromecast==14.0.9
|
||||
PyChromecast==14.0.10
|
||||
|
||||
# homeassistant.components.flume
|
||||
PyFlume==0.6.5
|
||||
@@ -181,7 +181,7 @@ aioairzone-cloud==0.7.2
|
||||
aioairzone==1.0.5
|
||||
|
||||
# homeassistant.components.alexa_devices
|
||||
aioamazondevices==13.0.0
|
||||
aioamazondevices==13.0.1
|
||||
|
||||
# homeassistant.components.ambient_network
|
||||
# homeassistant.components.ambient_station
|
||||
@@ -215,7 +215,7 @@ aiobafi6==0.9.0
|
||||
aiobotocore==2.21.1
|
||||
|
||||
# homeassistant.components.comelit
|
||||
aiocomelit==2.0.0
|
||||
aiocomelit==2.0.1
|
||||
|
||||
# homeassistant.components.dhcp
|
||||
aiodhcpwatcher==1.2.1
|
||||
@@ -398,7 +398,7 @@ aiosteamist==1.0.1
|
||||
aiostreammagic==2.13.0
|
||||
|
||||
# homeassistant.components.switcher_kis
|
||||
aioswitcher==6.1.0
|
||||
aioswitcher==6.1.1
|
||||
|
||||
# homeassistant.components.syncthing
|
||||
aiosyncthing==0.7.1
|
||||
@@ -422,7 +422,7 @@ aiousbwatcher==1.1.1
|
||||
aiovlc==0.5.1
|
||||
|
||||
# homeassistant.components.vodafone_station
|
||||
aiovodafone==3.1.2
|
||||
aiovodafone==3.1.3
|
||||
|
||||
# homeassistant.components.waqi
|
||||
aiowaqi==3.1.0
|
||||
@@ -998,7 +998,7 @@ gotailwind==0.3.0
|
||||
govee-ble==0.44.0
|
||||
|
||||
# homeassistant.components.govee_light_local
|
||||
govee-local-api==2.3.0
|
||||
govee-local-api==2.4.0
|
||||
|
||||
# homeassistant.components.gpsd
|
||||
gps3==0.33.3
|
||||
@@ -1087,7 +1087,7 @@ hole==0.9.0
|
||||
holidays==0.84
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20260304.0
|
||||
home-assistant-frontend==20260312.0
|
||||
|
||||
# homeassistant.components.conversation
|
||||
home-assistant-intents==2026.3.3
|
||||
@@ -1126,7 +1126,7 @@ ibeacon-ble==1.2.0
|
||||
# homeassistant.components.local_calendar
|
||||
# homeassistant.components.local_todo
|
||||
# homeassistant.components.remote_calendar
|
||||
ical==13.2.0
|
||||
ical==13.2.2
|
||||
|
||||
# homeassistant.components.caldav
|
||||
icalendar==6.3.1
|
||||
@@ -1449,7 +1449,7 @@ objgraph==3.5.0
|
||||
odp-amsterdam==6.1.2
|
||||
|
||||
# homeassistant.components.ohme
|
||||
ohme==1.6.0
|
||||
ohme==1.7.0
|
||||
|
||||
# homeassistant.components.ollama
|
||||
ollama==0.5.1
|
||||
@@ -1462,7 +1462,7 @@ ondilo==0.5.0
|
||||
|
||||
# homeassistant.components.onedrive
|
||||
# homeassistant.components.onedrive_for_business
|
||||
onedrive-personal-sdk==0.1.4
|
||||
onedrive-personal-sdk==0.1.7
|
||||
|
||||
# homeassistant.components.onvif
|
||||
onvif-zeep-async==4.0.4
|
||||
@@ -1669,7 +1669,7 @@ pyairobotrest==0.3.0
|
||||
pyairvisual==2023.08.1
|
||||
|
||||
# homeassistant.components.anglian_water
|
||||
pyanglianwater==3.1.0
|
||||
pyanglianwater==3.1.1
|
||||
|
||||
# homeassistant.components.aprilaire
|
||||
pyaprilaire==0.9.1
|
||||
@@ -1856,7 +1856,7 @@ pyisy==3.4.1
|
||||
pyituran==0.1.5
|
||||
|
||||
# homeassistant.components.jvc_projector
|
||||
pyjvcprojector==2.0.1
|
||||
pyjvcprojector==2.0.3
|
||||
|
||||
# homeassistant.components.kaleidescape
|
||||
pykaleidescape==1.1.3
|
||||
@@ -2020,7 +2020,7 @@ pyplaato==0.0.19
|
||||
pypoint==3.0.0
|
||||
|
||||
# homeassistant.components.portainer
|
||||
pyportainer==1.0.31
|
||||
pyportainer==1.0.33
|
||||
|
||||
# homeassistant.components.probe_plus
|
||||
pyprobeplus==1.1.2
|
||||
@@ -2102,7 +2102,7 @@ pysmappee==0.2.29
|
||||
pysmarlaapi==1.0.1
|
||||
|
||||
# homeassistant.components.smartthings
|
||||
pysmartthings==3.6.0
|
||||
pysmartthings==3.7.0
|
||||
|
||||
# homeassistant.components.smarty
|
||||
pysmarty2==0.10.3
|
||||
@@ -2153,7 +2153,7 @@ python-MotionMount==2.3.0
|
||||
python-awair==0.2.5
|
||||
|
||||
# homeassistant.components.bsblan
|
||||
python-bsblan==5.1.0
|
||||
python-bsblan==5.1.2
|
||||
|
||||
# homeassistant.components.ecobee
|
||||
python-ecobee-api==0.3.2
|
||||
@@ -2208,7 +2208,7 @@ python-opensky==1.0.1
|
||||
|
||||
# homeassistant.components.otbr
|
||||
# homeassistant.components.thread
|
||||
python-otbr-api==2.8.0
|
||||
python-otbr-api==2.9.0
|
||||
|
||||
# homeassistant.components.overseerr
|
||||
python-overseerr==0.9.0
|
||||
@@ -2502,7 +2502,7 @@ speak2mary==1.4.0
|
||||
speedtest-cli==2.1.3
|
||||
|
||||
# homeassistant.components.spotify
|
||||
spotifyaio==1.0.0
|
||||
spotifyaio==2.0.2
|
||||
|
||||
# homeassistant.components.sql
|
||||
sqlparse==0.5.5
|
||||
@@ -2550,7 +2550,7 @@ tailscale==0.6.2
|
||||
tellduslive==0.10.12
|
||||
|
||||
# homeassistant.components.teltonika
|
||||
teltasync==0.1.3
|
||||
teltasync==0.2.0
|
||||
|
||||
# homeassistant.components.lg_soundbar
|
||||
temescal==0.5
|
||||
@@ -2783,7 +2783,7 @@ yalesmartalarmclient==0.4.3
|
||||
# homeassistant.components.august
|
||||
# homeassistant.components.yale
|
||||
# homeassistant.components.yalexs_ble
|
||||
yalexs-ble==3.2.7
|
||||
yalexs-ble==3.2.8
|
||||
|
||||
# homeassistant.components.august
|
||||
# homeassistant.components.yale
|
||||
@@ -2817,7 +2817,7 @@ zeroconf==0.148.0
|
||||
zeversolar==0.3.2
|
||||
|
||||
# homeassistant.components.zha
|
||||
zha==1.0.1
|
||||
zha==1.0.2
|
||||
|
||||
# homeassistant.components.zinvolt
|
||||
zinvolt==0.3.0
|
||||
|
||||
@@ -181,7 +181,6 @@ EXCEPTIONS = {
|
||||
"PySwitchmate", # https://github.com/Danielhiversen/pySwitchmate/pull/16
|
||||
"PyXiaomiGateway", # https://github.com/Danielhiversen/PyXiaomiGateway/pull/201
|
||||
"chacha20poly1305", # LGPL
|
||||
"caio", # Apache 2 https://github.com/mosquito/caio/?tab=Apache-2.0-1-ov-file#readme
|
||||
"commentjson", # https://github.com/vaidik/commentjson/pull/55
|
||||
"crownstone-cloud", # https://github.com/crownstone/crownstone-lib-python-cloud/pull/5
|
||||
"crownstone-core", # https://github.com/crownstone/crownstone-lib-python-core/pull/6
|
||||
|
||||
@@ -2,10 +2,7 @@
|
||||
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from aioamazondevices.const.devices import (
|
||||
SPEAKER_GROUP_DEVICE_TYPE,
|
||||
SPEAKER_GROUP_FAMILY,
|
||||
)
|
||||
from aioamazondevices.const.devices import SPEAKER_GROUP_FAMILY
|
||||
from aioamazondevices.exceptions import CannotConnect, CannotRetrieveData
|
||||
import pytest
|
||||
|
||||
@@ -117,7 +114,7 @@ async def test_alexa_dnd_group_removal(
|
||||
identifiers={(DOMAIN, mock_config_entry.entry_id)},
|
||||
name=mock_config_entry.title,
|
||||
manufacturer="Amazon",
|
||||
model=SPEAKER_GROUP_DEVICE_TYPE,
|
||||
model="Speaker Group",
|
||||
entry_type=dr.DeviceEntryType.SERVICE,
|
||||
)
|
||||
|
||||
@@ -156,7 +153,7 @@ async def test_alexa_unsupported_notification_sensor_removal(
|
||||
identifiers={(DOMAIN, mock_config_entry.entry_id)},
|
||||
name=mock_config_entry.title,
|
||||
manufacturer="Amazon",
|
||||
model=SPEAKER_GROUP_DEVICE_TYPE,
|
||||
model="Speaker Group",
|
||||
entry_type=dr.DeviceEntryType.SERVICE,
|
||||
)
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
from aiohttp import ClientResponseError
|
||||
from aiohttp import ClientError, ClientResponseError
|
||||
import pytest
|
||||
from yalexs.const import Brand
|
||||
from yalexs.exceptions import AugustApiAIOHTTPError, InvalidAuth
|
||||
@@ -18,7 +18,11 @@ from homeassistant.const import (
|
||||
STATE_ON,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.exceptions import (
|
||||
HomeAssistantError,
|
||||
OAuth2TokenRequestReauthError,
|
||||
OAuth2TokenRequestTransientError,
|
||||
)
|
||||
from homeassistant.helpers import (
|
||||
device_registry as dr,
|
||||
entity_registry as er,
|
||||
@@ -304,3 +308,59 @@ async def test_oauth_implementation_not_available(hass: HomeAssistant) -> None:
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert entry.state is ConfigEntryState.SETUP_RETRY
|
||||
|
||||
|
||||
async def test_oauth_token_request_reauth_error(hass: HomeAssistant) -> None:
|
||||
"""Test OAuth token request reauth error starts a reauth flow."""
|
||||
entry = await mock_august_config_entry(hass)
|
||||
|
||||
with patch(
|
||||
"homeassistant.helpers.config_entry_oauth2_flow.OAuth2Session.async_ensure_token_valid",
|
||||
side_effect=OAuth2TokenRequestReauthError(
|
||||
request_info=Mock(real_url="https://auth.august.com/access_token"),
|
||||
status=401,
|
||||
domain=DOMAIN,
|
||||
),
|
||||
):
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert entry.state is ConfigEntryState.SETUP_ERROR
|
||||
flows = hass.config_entries.flow.async_progress()
|
||||
assert len(flows) == 1
|
||||
assert flows[0]["step_id"] == "pick_implementation"
|
||||
assert flows[0]["context"]["source"] == "reauth"
|
||||
|
||||
|
||||
async def test_oauth_token_request_transient_error_is_retryable(
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
"""Test OAuth token transient request error marks entry for setup retry."""
|
||||
entry = await mock_august_config_entry(hass)
|
||||
|
||||
with patch(
|
||||
"homeassistant.helpers.config_entry_oauth2_flow.OAuth2Session.async_ensure_token_valid",
|
||||
side_effect=OAuth2TokenRequestTransientError(
|
||||
request_info=Mock(real_url="https://auth.august.com/access_token"),
|
||||
status=500,
|
||||
domain=DOMAIN,
|
||||
),
|
||||
):
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert entry.state is ConfigEntryState.SETUP_RETRY
|
||||
|
||||
|
||||
async def test_oauth_client_error_is_retryable(hass: HomeAssistant) -> None:
|
||||
"""Test OAuth transport client errors mark entry for setup retry."""
|
||||
entry = await mock_august_config_entry(hass)
|
||||
|
||||
with patch(
|
||||
"homeassistant.helpers.config_entry_oauth2_flow.OAuth2Session.async_ensure_token_valid",
|
||||
side_effect=ClientError("connection error"),
|
||||
):
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert entry.state is ConfigEntryState.SETUP_RETRY
|
||||
|
||||
@@ -396,6 +396,11 @@ async def test_switch_device_no_ip_address(
|
||||
"async_set_deflection_enable",
|
||||
STATE_ON,
|
||||
),
|
||||
(
|
||||
"switch.mock_title_wi_fi_mywifi",
|
||||
"async_set_wlan_configuration",
|
||||
STATE_ON,
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_switch_turn_on_off(
|
||||
|
||||
@@ -16,7 +16,7 @@ from tests.common import MockConfigEntry
|
||||
API_URL = "https://test.ghost.io"
|
||||
API_KEY = "650b7a9f8e8c1234567890ab:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"
|
||||
SITE_UUID = "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
|
||||
SITE_DATA = {"title": "Test Ghost", "url": API_URL, "uuid": SITE_UUID}
|
||||
SITE_DATA = {"title": "Test Ghost", "url": API_URL, "site_uuid": SITE_UUID}
|
||||
POSTS_DATA = {"published": 42, "drafts": 5, "scheduled": 2}
|
||||
MEMBERS_DATA = {"total": 1000, "paid": 100, "free": 850, "comped": 50}
|
||||
LATEST_POST_DATA = {
|
||||
|
||||
@@ -414,7 +414,7 @@
|
||||
'object_id_base': 'Energy exported',
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 0,
|
||||
'suggested_display_precision': 2,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.ENERGY: 'energy'>,
|
||||
@@ -426,7 +426,7 @@
|
||||
'supported_features': 0,
|
||||
'translation_key': 'energy_exported',
|
||||
'unique_id': '40580137858664_energy_exported',
|
||||
'unit_of_measurement': <UnitOfEnergy.WATT_HOUR: 'Wh'>,
|
||||
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_entities[sensor.homevolt_ems_energy_exported-state]
|
||||
@@ -435,7 +435,7 @@
|
||||
'device_class': 'energy',
|
||||
'friendly_name': 'Homevolt EMS Energy exported',
|
||||
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
|
||||
'unit_of_measurement': <UnitOfEnergy.WATT_HOUR: 'Wh'>,
|
||||
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.homevolt_ems_energy_exported',
|
||||
@@ -471,7 +471,7 @@
|
||||
'object_id_base': 'Energy imported',
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 0,
|
||||
'suggested_display_precision': 2,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.ENERGY: 'energy'>,
|
||||
@@ -483,7 +483,7 @@
|
||||
'supported_features': 0,
|
||||
'translation_key': 'energy_imported',
|
||||
'unique_id': '40580137858664_energy_imported',
|
||||
'unit_of_measurement': <UnitOfEnergy.WATT_HOUR: 'Wh'>,
|
||||
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_entities[sensor.homevolt_ems_energy_imported-state]
|
||||
@@ -492,7 +492,7 @@
|
||||
'device_class': 'energy',
|
||||
'friendly_name': 'Homevolt EMS Energy imported',
|
||||
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
|
||||
'unit_of_measurement': <UnitOfEnergy.WATT_HOUR: 'Wh'>,
|
||||
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.homevolt_ems_energy_imported',
|
||||
|
||||
@@ -8007,8 +8007,11 @@
|
||||
'name': None,
|
||||
'object_id_base': 'Water usage',
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 0,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_device_class': <SensorDeviceClass.VOLUME_FLOW_RATE: 'volume_flow_rate'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Water usage',
|
||||
'platform': 'homewizard',
|
||||
@@ -8023,6 +8026,7 @@
|
||||
# name: test_sensors[HWE-P1-entity_ids0][sensor.device_water_usage:state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'volume_flow_rate',
|
||||
'friendly_name': 'Device Water usage',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': <UnitOfVolumeFlowRate.LITERS_PER_MINUTE: 'L/min'>,
|
||||
@@ -11927,8 +11931,11 @@
|
||||
'name': None,
|
||||
'object_id_base': 'Water usage',
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 0,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_device_class': <SensorDeviceClass.VOLUME_FLOW_RATE: 'volume_flow_rate'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Water usage',
|
||||
'platform': 'homewizard',
|
||||
@@ -11943,6 +11950,7 @@
|
||||
# name: test_sensors[HWE-P1-invalid-EAN-entity_ids9][sensor.device_water_usage:state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'volume_flow_rate',
|
||||
'friendly_name': 'Device Water usage',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': <UnitOfVolumeFlowRate.LITERS_PER_MINUTE: 'L/min'>,
|
||||
@@ -15408,8 +15416,11 @@
|
||||
'name': None,
|
||||
'object_id_base': 'Water usage',
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 0,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_device_class': <SensorDeviceClass.VOLUME_FLOW_RATE: 'volume_flow_rate'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Water usage',
|
||||
'platform': 'homewizard',
|
||||
@@ -15424,6 +15435,7 @@
|
||||
# name: test_sensors[HWE-P1-zero-values-entity_ids1][sensor.device_water_usage:state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'volume_flow_rate',
|
||||
'friendly_name': 'Device Water usage',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': <UnitOfVolumeFlowRate.LITERS_PER_MINUTE: 'L/min'>,
|
||||
@@ -17573,8 +17585,11 @@
|
||||
'name': None,
|
||||
'object_id_base': 'Water usage',
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 0,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_device_class': <SensorDeviceClass.VOLUME_FLOW_RATE: 'volume_flow_rate'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Water usage',
|
||||
'platform': 'homewizard',
|
||||
@@ -17589,6 +17604,7 @@
|
||||
# name: test_sensors[HWE-WTR-entity_ids4][sensor.device_water_usage:state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'volume_flow_rate',
|
||||
'friendly_name': 'Device Water usage',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': <UnitOfVolumeFlowRate.LITERS_PER_MINUTE: 'L/min'>,
|
||||
|
||||
@@ -427,9 +427,7 @@
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
@@ -466,7 +464,6 @@
|
||||
'attribution': 'Data provided by unpublished Intellifire API',
|
||||
'device_class': 'timestamp',
|
||||
'friendly_name': 'IntelliFire Timer end',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.intellifire_timer_end',
|
||||
|
||||
@@ -833,7 +833,7 @@
|
||||
'max': 450,
|
||||
'min': 10,
|
||||
'mode': <NumberMode.BOX: 'box'>,
|
||||
'step': 5,
|
||||
'step': 1,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
@@ -872,7 +872,7 @@
|
||||
'max': 450,
|
||||
'min': 10,
|
||||
'mode': <NumberMode.BOX: 'box'>,
|
||||
'step': 5,
|
||||
'step': 1,
|
||||
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
|
||||
33
tests/components/knx/test_dpt.py
Normal file
33
tests/components/knx/test_dpt.py
Normal 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.state == STATE_HOME
|
||||
|
||||
# Override the GPS state via a direct state update
|
||||
async_fire_mqtt_message(hass, "test-topic", "office")
|
||||
state = hass.states.get("device_tracker.test")
|
||||
assert state.state == "office"
|
||||
|
||||
# Test a GPS attributes update without a reset
|
||||
async_fire_mqtt_message(
|
||||
hass,
|
||||
"attributes-topic",
|
||||
'{"latitude":32.87336,"longitude": -117.22743, "gps_accuracy":1.5}',
|
||||
)
|
||||
|
||||
state = hass.states.get("device_tracker.test")
|
||||
assert state.state == "office"
|
||||
|
||||
# Reset the manual set location
|
||||
# This should calculate the location from GPS attributes
|
||||
async_fire_mqtt_message(hass, "test-topic", "reset")
|
||||
state = hass.states.get("device_tracker.test")
|
||||
assert state.attributes["latitude"] == 32.87336
|
||||
assert state.attributes["longitude"] == -117.22743
|
||||
assert state.attributes["gps_accuracy"] == 1.5
|
||||
assert state.attributes["source_type"] == "gps"
|
||||
assert state.state == STATE_HOME
|
||||
|
||||
|
||||
async def test_setting_blocked_attribute_via_mqtt_json_message(
|
||||
hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
# name: test_diagnostics
|
||||
dict({
|
||||
'cap_available': True,
|
||||
'ct_connected': True,
|
||||
'device_info': dict({
|
||||
'model': 'Home Pro',
|
||||
'name': 'Ohme Home Pro',
|
||||
|
||||
@@ -263,20 +263,28 @@ async def test_full_flow_reconfigure_unique_id(
|
||||
) -> None:
|
||||
"""Test the full flow of the config flow, this time with a known unique ID."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
other_entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="Portainer other",
|
||||
data=USER_INPUT_RECONFIGURE,
|
||||
unique_id=USER_INPUT_RECONFIGURE[CONF_API_TOKEN],
|
||||
)
|
||||
other_entry.add_to_hass(hass)
|
||||
|
||||
result = await mock_config_entry.start_reconfigure_flow(hass)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "reconfigure"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input=MOCK_USER_SETUP,
|
||||
user_input=USER_INPUT_RECONFIGURE,
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
assert mock_config_entry.data[CONF_API_TOKEN] == "test_api_token"
|
||||
assert mock_config_entry.data[CONF_URL] == "https://127.0.0.1:9000/"
|
||||
assert mock_config_entry.data[CONF_VERIFY_SSL] is True
|
||||
assert len(mock_setup_entry.mock_calls) == 0
|
||||
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ from __future__ import annotations
|
||||
from collections.abc import Generator
|
||||
from http import HTTPStatus
|
||||
import json
|
||||
import re
|
||||
from typing import Any
|
||||
from unittest.mock import patch
|
||||
|
||||
@@ -278,4 +279,4 @@ def handle_responses(
|
||||
async def handle(method, url, data) -> AiohttpClientMockResponse:
|
||||
return responses.pop(0)
|
||||
|
||||
aioclient_mock.post(URL, side_effect=handle)
|
||||
aioclient_mock.post(re.compile(r"^https?://[^/]+/stick$"), side_effect=handle)
|
||||
|
||||
@@ -70,7 +70,7 @@ async def test_no_unique_id(
|
||||
"""Test rainsensor binary sensor with no unique id."""
|
||||
|
||||
# Failure to migrate config entry to a unique id
|
||||
responses.insert(0, mock_response_error(HTTPStatus.SERVICE_UNAVAILABLE))
|
||||
responses.insert(1, mock_response_error(HTTPStatus.SERVICE_UNAVAILABLE))
|
||||
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
@@ -292,7 +292,7 @@ async def test_no_unique_id(
|
||||
"""Test calendar entity with no unique id."""
|
||||
|
||||
# Failure to migrate config entry to a unique id
|
||||
responses.insert(0, mock_response_error(HTTPStatus.SERVICE_UNAVAILABLE))
|
||||
responses.insert(1, mock_response_error(HTTPStatus.SERVICE_UNAVAILABLE))
|
||||
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
@@ -19,6 +19,7 @@ from .conftest import (
|
||||
CONFIG_ENTRY_DATA,
|
||||
HOST,
|
||||
MAC_ADDRESS_UNIQUE_ID,
|
||||
MODEL_AND_VERSION_RESPONSE,
|
||||
PASSWORD,
|
||||
SERIAL_NUMBER,
|
||||
SERIAL_RESPONSE,
|
||||
@@ -36,7 +37,11 @@ from tests.test_util.aiohttp import AiohttpClientMocker, AiohttpClientMockRespon
|
||||
@pytest.fixture(name="responses")
|
||||
def mock_responses() -> list[AiohttpClientMockResponse]:
|
||||
"""Set up fake serial number response when testing the connection."""
|
||||
return [mock_response(SERIAL_RESPONSE), mock_json_response(WIFI_PARAMS_RESPONSE)]
|
||||
return [
|
||||
mock_response(MODEL_AND_VERSION_RESPONSE),
|
||||
mock_response(SERIAL_RESPONSE),
|
||||
mock_json_response(WIFI_PARAMS_RESPONSE),
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
@@ -77,6 +82,7 @@ async def complete_flow(hass: HomeAssistant, password: str = PASSWORD) -> FlowRe
|
||||
[
|
||||
(
|
||||
[
|
||||
mock_response(MODEL_AND_VERSION_RESPONSE),
|
||||
mock_response(SERIAL_RESPONSE),
|
||||
mock_json_response(WIFI_PARAMS_RESPONSE),
|
||||
],
|
||||
@@ -85,6 +91,7 @@ async def complete_flow(hass: HomeAssistant, password: str = PASSWORD) -> FlowRe
|
||||
),
|
||||
(
|
||||
[
|
||||
mock_response(MODEL_AND_VERSION_RESPONSE),
|
||||
mock_response(ZERO_SERIAL_RESPONSE),
|
||||
mock_json_response(WIFI_PARAMS_RESPONSE),
|
||||
],
|
||||
@@ -123,7 +130,11 @@ async def test_controller_flow(
|
||||
(
|
||||
"other-serial-number",
|
||||
{**CONFIG_ENTRY_DATA, "host": "other-host"},
|
||||
[mock_response(SERIAL_RESPONSE), mock_json_response(WIFI_PARAMS_RESPONSE)],
|
||||
[
|
||||
mock_response(MODEL_AND_VERSION_RESPONSE),
|
||||
mock_response(SERIAL_RESPONSE),
|
||||
mock_json_response(WIFI_PARAMS_RESPONSE),
|
||||
],
|
||||
CONFIG_ENTRY_DATA,
|
||||
),
|
||||
(
|
||||
@@ -133,6 +144,7 @@ async def test_controller_flow(
|
||||
"host": "other-host",
|
||||
},
|
||||
[
|
||||
mock_response(MODEL_AND_VERSION_RESPONSE),
|
||||
mock_response(SERIAL_RESPONSE),
|
||||
mock_json_response(WIFI_PARAMS_RESPONSE),
|
||||
],
|
||||
@@ -142,6 +154,7 @@ async def test_controller_flow(
|
||||
None,
|
||||
{**CONFIG_ENTRY_DATA, "serial_number": 0, "host": "other-host"},
|
||||
[
|
||||
mock_response(MODEL_AND_VERSION_RESPONSE),
|
||||
mock_response(ZERO_SERIAL_RESPONSE),
|
||||
mock_json_response(WIFI_PARAMS_RESPONSE),
|
||||
],
|
||||
@@ -185,6 +198,7 @@ async def test_multiple_config_entries(
|
||||
MAC_ADDRESS_UNIQUE_ID,
|
||||
CONFIG_ENTRY_DATA,
|
||||
[
|
||||
mock_response(MODEL_AND_VERSION_RESPONSE),
|
||||
mock_response(SERIAL_RESPONSE),
|
||||
mock_json_response(WIFI_PARAMS_RESPONSE),
|
||||
],
|
||||
@@ -194,7 +208,11 @@ async def test_multiple_config_entries(
|
||||
(
|
||||
SERIAL_NUMBER,
|
||||
CONFIG_ENTRY_DATA,
|
||||
[mock_response(SERIAL_RESPONSE), mock_json_response(WIFI_PARAMS_RESPONSE)],
|
||||
[
|
||||
mock_response(MODEL_AND_VERSION_RESPONSE),
|
||||
mock_response(SERIAL_RESPONSE),
|
||||
mock_json_response(WIFI_PARAMS_RESPONSE),
|
||||
],
|
||||
CONFIG_ENTRY_DATA,
|
||||
),
|
||||
# Old unique id with no serial, but same host
|
||||
@@ -202,6 +220,7 @@ async def test_multiple_config_entries(
|
||||
None,
|
||||
{**CONFIG_ENTRY_DATA, "serial_number": 0},
|
||||
[
|
||||
mock_response(MODEL_AND_VERSION_RESPONSE),
|
||||
mock_response(ZERO_SERIAL_RESPONSE),
|
||||
mock_json_response(WIFI_PARAMS_RESPONSE),
|
||||
],
|
||||
@@ -214,7 +233,11 @@ async def test_multiple_config_entries(
|
||||
**CONFIG_ENTRY_DATA,
|
||||
"host": f"other-{HOST}",
|
||||
},
|
||||
[mock_response(SERIAL_RESPONSE), mock_json_response(WIFI_PARAMS_RESPONSE)],
|
||||
[
|
||||
mock_response(MODEL_AND_VERSION_RESPONSE),
|
||||
mock_response(SERIAL_RESPONSE),
|
||||
mock_json_response(WIFI_PARAMS_RESPONSE),
|
||||
],
|
||||
CONFIG_ENTRY_DATA, # Updated the host
|
||||
),
|
||||
],
|
||||
@@ -281,8 +304,8 @@ async def test_controller_invalid_auth(
|
||||
[
|
||||
# Incorrect password response
|
||||
AiohttpClientMockResponse("POST", URL, status=HTTPStatus.FORBIDDEN),
|
||||
AiohttpClientMockResponse("POST", URL, status=HTTPStatus.FORBIDDEN),
|
||||
# Second attempt with the correct password
|
||||
mock_response(MODEL_AND_VERSION_RESPONSE),
|
||||
mock_response(SERIAL_RESPONSE),
|
||||
mock_json_response(WIFI_PARAMS_RESPONSE),
|
||||
]
|
||||
@@ -346,8 +369,8 @@ async def test_controller_timeout(
|
||||
[
|
||||
# First attempt simulate the wrong password
|
||||
AiohttpClientMockResponse("POST", URL, status=HTTPStatus.FORBIDDEN),
|
||||
AiohttpClientMockResponse("POST", URL, status=HTTPStatus.FORBIDDEN),
|
||||
# Second attempt simulate the correct password
|
||||
mock_response(MODEL_AND_VERSION_RESPONSE),
|
||||
mock_response(SERIAL_RESPONSE),
|
||||
mock_json_response(WIFI_PARAMS_RESPONSE),
|
||||
],
|
||||
|
||||
@@ -62,6 +62,7 @@ async def test_init_success(
|
||||
(
|
||||
CONFIG_ENTRY_DATA,
|
||||
[
|
||||
mock_response(MODEL_AND_VERSION_RESPONSE),
|
||||
mock_response(MODEL_AND_VERSION_RESPONSE),
|
||||
mock_response_error(HTTPStatus.SERVICE_UNAVAILABLE),
|
||||
],
|
||||
@@ -71,6 +72,7 @@ async def test_init_success(
|
||||
(
|
||||
CONFIG_ENTRY_DATA,
|
||||
[
|
||||
mock_response(MODEL_AND_VERSION_RESPONSE),
|
||||
mock_response(MODEL_AND_VERSION_RESPONSE),
|
||||
mock_response_error(HTTPStatus.INTERNAL_SERVER_ERROR),
|
||||
],
|
||||
@@ -123,7 +125,7 @@ async def test_fix_unique_id(
|
||||
) -> None:
|
||||
"""Test fix of a config entry with no unique id."""
|
||||
|
||||
responses.insert(0, mock_json_response(WIFI_PARAMS_RESPONSE))
|
||||
responses.insert(1, mock_json_response(WIFI_PARAMS_RESPONSE))
|
||||
|
||||
entries = hass.config_entries.async_entries(DOMAIN)
|
||||
assert len(entries) == 1
|
||||
@@ -181,7 +183,7 @@ async def test_fix_unique_id_failure(
|
||||
) -> None:
|
||||
"""Test a failure during fix of a config entry with no unique id."""
|
||||
|
||||
responses.insert(0, initial_response)
|
||||
responses.insert(1, initial_response)
|
||||
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
# Config entry is loaded, but not updated
|
||||
@@ -212,11 +214,16 @@ async def test_fix_unique_id_duplicate(
|
||||
)
|
||||
other_entry.add_to_hass(hass)
|
||||
|
||||
# Responses for the second config entry. This first fetches wifi params
|
||||
# to repair the unique id.
|
||||
responses_copy = [*responses]
|
||||
responses.append(mock_json_response(WIFI_PARAMS_RESPONSE))
|
||||
responses.extend(responses_copy)
|
||||
# Responses for the second config entry.
|
||||
#
|
||||
# `pyrainbird.async_client.create_controller` probes by calling
|
||||
# `get_model_and_version()`, then `_async_fix_unique_id` fetches wifi params.
|
||||
responses.extend(
|
||||
[
|
||||
mock_response(MODEL_AND_VERSION_RESPONSE),
|
||||
mock_json_response(WIFI_PARAMS_RESPONSE),
|
||||
]
|
||||
)
|
||||
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
@@ -451,10 +458,16 @@ async def test_fix_duplicate_device_ids(
|
||||
assert device_entry.disabled_by == expected_disabled_by
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("config_entry_data", "config_entry_unique_id"),
|
||||
[(None, None)],
|
||||
ids=["no_default_entry"],
|
||||
)
|
||||
async def test_reload_migration_with_leading_zero_mac(
|
||||
hass: HomeAssistant,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
responses: list[AiohttpClientMockResponse],
|
||||
) -> None:
|
||||
"""Test migration and reload of a device with a mac address with a leading zero."""
|
||||
mac_address = "01:02:03:04:05:06"
|
||||
@@ -474,6 +487,10 @@ async def test_reload_migration_with_leading_zero_mac(
|
||||
)
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
# This test sets up and then reloads the config entry, so we need a second
|
||||
# copy of the default response sequence.
|
||||
responses.extend([*responses])
|
||||
|
||||
# Create a device and entity with the old unique id format
|
||||
device_entry = device_registry.async_get_or_create(
|
||||
config_entry_id=config_entry.entry_id,
|
||||
|
||||
@@ -152,7 +152,7 @@ async def test_no_unique_id(
|
||||
"""Test number platform with no unique id."""
|
||||
|
||||
# Failure to migrate config entry to a unique id
|
||||
responses.insert(0, mock_response_error(HTTPStatus.SERVICE_UNAVAILABLE))
|
||||
responses.insert(1, mock_response_error(HTTPStatus.SERVICE_UNAVAILABLE))
|
||||
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
@@ -82,7 +82,7 @@ async def test_sensor_no_unique_id(
|
||||
"""Test sensor platform with no unique id."""
|
||||
|
||||
# Failure to migrate config entry to a unique id
|
||||
responses.insert(0, mock_response_error(HTTPStatus.SERVICE_UNAVAILABLE))
|
||||
responses.insert(1, mock_response_error(HTTPStatus.SERVICE_UNAVAILABLE))
|
||||
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
@@ -293,7 +293,7 @@ async def test_no_unique_id(
|
||||
"""Test an irrigation switch with no unique id due to migration failure."""
|
||||
|
||||
# Failure to migrate config entry to a unique id
|
||||
responses.insert(0, mock_response_error(HTTPStatus.SERVICE_UNAVAILABLE))
|
||||
responses.insert(1, mock_response_error(HTTPStatus.SERVICE_UNAVAILABLE))
|
||||
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
@@ -145,3 +145,31 @@ async def test_remove_router_reconnect(
|
||||
|
||||
entity = entity_registry.async_get("button.mock_title_reconnect_zigbee_router")
|
||||
assert entity is None
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||
async def test_router_button_with_3_radios(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_smlight_client: MagicMock,
|
||||
) -> None:
|
||||
"""Test creation of router buttons for device with 3 radios."""
|
||||
mock_smlight_client.get_info.side_effect = None
|
||||
mock_smlight_client.get_info.return_value = Info(
|
||||
MAC="AA:BB:CC:DD:EE:FF",
|
||||
radios=[
|
||||
Radio(zb_type=0, chip_index=0),
|
||||
Radio(zb_type=1, chip_index=1),
|
||||
Radio(zb_type=0, chip_index=2),
|
||||
],
|
||||
)
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
entities = er.async_entries_for_config_entry(
|
||||
entity_registry, mock_config_entry.entry_id
|
||||
)
|
||||
assert len(entities) == 4
|
||||
|
||||
entity = entity_registry.async_get("button.mock_title_reconnect_zigbee_router")
|
||||
assert entity is not None
|
||||
|
||||
@@ -7,9 +7,12 @@ from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.media_player import (
|
||||
ATTR_GROUP_MEMBERS,
|
||||
ATTR_INPUT_SOURCE,
|
||||
DOMAIN as MEDIA_PLAYER_DOMAIN,
|
||||
SERVICE_JOIN,
|
||||
SERVICE_SELECT_SOURCE,
|
||||
SERVICE_UNJOIN,
|
||||
MediaPlayerState,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import ATTR_ENTITY_ID
|
||||
@@ -175,3 +178,116 @@ async def test_state_stream_not_found(
|
||||
|
||||
state = hass.states.get("media_player.test_client_1_snapcast_client")
|
||||
assert state.state == "off"
|
||||
|
||||
|
||||
async def test_attributes_group_is_none(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_create_server: AsyncMock,
|
||||
mock_client_1: AsyncMock,
|
||||
) -> None:
|
||||
"""Test exceptions are not thrown when a client has no group."""
|
||||
# Force nonexistent group
|
||||
mock_client_1.group = None
|
||||
|
||||
# Setup and verify the integration is loaded
|
||||
with patch("secrets.token_hex", return_value="mock_token"):
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
assert mock_config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
state = hass.states.get("media_player.test_client_1_snapcast_client")
|
||||
|
||||
# Assert accessing state and attributes doesn't throw
|
||||
assert state.state == MediaPlayerState.IDLE
|
||||
|
||||
assert state.attributes["group_members"] is None
|
||||
assert "source" not in state.attributes
|
||||
assert "source_list" not in state.attributes
|
||||
assert "metadata" not in state.attributes
|
||||
assert "media_position" not in state.attributes
|
||||
|
||||
|
||||
async def test_select_source_group_is_none(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_create_server: AsyncMock,
|
||||
mock_client_1: AsyncMock,
|
||||
mock_group_1: AsyncMock,
|
||||
) -> None:
|
||||
"""Test the select source action throws a service validation error when a client has no group."""
|
||||
# Force nonexistent group
|
||||
mock_client_1.group = None
|
||||
|
||||
# Setup and verify the integration is loaded
|
||||
with patch("secrets.token_hex", return_value="mock_token"):
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
assert mock_config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
with pytest.raises(ServiceValidationError):
|
||||
await hass.services.async_call(
|
||||
MEDIA_PLAYER_DOMAIN,
|
||||
SERVICE_SELECT_SOURCE,
|
||||
{
|
||||
ATTR_ENTITY_ID: "media_player.test_client_1_snapcast_client",
|
||||
ATTR_INPUT_SOURCE: "fake_source",
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
mock_group_1.set_stream.assert_not_awaited()
|
||||
|
||||
|
||||
async def test_join_group_is_none(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_create_server: AsyncMock,
|
||||
mock_group_1: AsyncMock,
|
||||
mock_client_1: AsyncMock,
|
||||
) -> None:
|
||||
"""Test join action throws a service validation error when a client has no group."""
|
||||
# Force nonexistent group
|
||||
mock_client_1.group = None
|
||||
|
||||
# Setup and verify the integration is loaded
|
||||
with patch("secrets.token_hex", return_value="mock_token"):
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
assert mock_config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
with pytest.raises(ServiceValidationError):
|
||||
await hass.services.async_call(
|
||||
MEDIA_PLAYER_DOMAIN,
|
||||
SERVICE_JOIN,
|
||||
{
|
||||
ATTR_ENTITY_ID: "media_player.test_client_1_snapcast_client",
|
||||
ATTR_GROUP_MEMBERS: ["media_player.test_client_2_snapcast_client"],
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
mock_group_1.add_client.assert_not_awaited()
|
||||
|
||||
|
||||
async def test_unjoin_group_is_none(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_create_server: AsyncMock,
|
||||
mock_client_1: AsyncMock,
|
||||
mock_group_1: AsyncMock,
|
||||
) -> None:
|
||||
"""Test the unjoin action throws a service validation error when a client has no group."""
|
||||
# Force nonexistent group
|
||||
mock_client_1.group = None
|
||||
|
||||
# Setup and verify the integration is loaded
|
||||
with patch("secrets.token_hex", return_value="mock_token"):
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
assert mock_config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
with pytest.raises(ServiceValidationError):
|
||||
await hass.services.async_call(
|
||||
MEDIA_PLAYER_DOMAIN,
|
||||
SERVICE_UNJOIN,
|
||||
{
|
||||
ATTR_ENTITY_ID: "media_player.test_client_1_snapcast_client",
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
mock_group_1.remove_client.assert_not_awaited()
|
||||
|
||||
@@ -10,7 +10,6 @@ from spotifyaio.models import (
|
||||
Artist,
|
||||
Devices,
|
||||
FollowedArtistResponse,
|
||||
NewReleasesResponse,
|
||||
NewReleasesResponseInner,
|
||||
PlaybackState,
|
||||
PlayedTrackResponse,
|
||||
@@ -142,9 +141,6 @@ def mock_spotify() -> Generator[AsyncMock]:
|
||||
client.get_followed_artists.return_value = FollowedArtistResponse.from_json(
|
||||
load_fixture("followed_artists.json", DOMAIN)
|
||||
).artists.items
|
||||
client.get_new_releases.return_value = NewReleasesResponse.from_json(
|
||||
load_fixture("new_releases.json", DOMAIN)
|
||||
).albums.items
|
||||
client.get_devices.return_value = Devices.from_json(
|
||||
load_fixture("devices.json", DOMAIN)
|
||||
).devices
|
||||
|
||||
@@ -1,469 +0,0 @@
|
||||
{
|
||||
"albums": {
|
||||
"href": "https://api.spotify.com/v1/browse/new-releases?offset=0&limit=20&locale=en-US,en;q%3D0.5",
|
||||
"items": [
|
||||
{
|
||||
"album_type": "album",
|
||||
"artists": [
|
||||
{
|
||||
"external_urls": {
|
||||
"spotify": "https://open.spotify.com/artist/4gzpq5DPGxSnKTe4SA8HAU"
|
||||
},
|
||||
"href": "https://api.spotify.com/v1/artists/4gzpq5DPGxSnKTe4SA8HAU",
|
||||
"id": "4gzpq5DPGxSnKTe4SA8HAU",
|
||||
"name": "Coldplay",
|
||||
"type": "artist",
|
||||
"uri": "spotify:artist:4gzpq5DPGxSnKTe4SA8HAU"
|
||||
}
|
||||
],
|
||||
"available_markets": [
|
||||
"AR",
|
||||
"AU",
|
||||
"AT",
|
||||
"BE",
|
||||
"BO",
|
||||
"BR",
|
||||
"BG",
|
||||
"CA",
|
||||
"CL",
|
||||
"CO",
|
||||
"CR",
|
||||
"CY",
|
||||
"CZ",
|
||||
"DK",
|
||||
"DO",
|
||||
"DE",
|
||||
"EC",
|
||||
"EE",
|
||||
"SV",
|
||||
"FI",
|
||||
"FR",
|
||||
"GR",
|
||||
"GT",
|
||||
"HN",
|
||||
"HK",
|
||||
"HU",
|
||||
"IS",
|
||||
"IE",
|
||||
"IT",
|
||||
"LV",
|
||||
"LT",
|
||||
"LU",
|
||||
"MY",
|
||||
"MT",
|
||||
"MX",
|
||||
"NL",
|
||||
"NZ",
|
||||
"NI",
|
||||
"NO",
|
||||
"PA",
|
||||
"PY",
|
||||
"PE",
|
||||
"PH",
|
||||
"PL",
|
||||
"PT",
|
||||
"SG",
|
||||
"SK",
|
||||
"ES",
|
||||
"SE",
|
||||
"CH",
|
||||
"TW",
|
||||
"TR",
|
||||
"UY",
|
||||
"US",
|
||||
"GB",
|
||||
"AD",
|
||||
"LI",
|
||||
"MC",
|
||||
"ID",
|
||||
"JP",
|
||||
"TH",
|
||||
"VN",
|
||||
"RO",
|
||||
"IL",
|
||||
"ZA",
|
||||
"SA",
|
||||
"AE",
|
||||
"BH",
|
||||
"QA",
|
||||
"OM",
|
||||
"KW",
|
||||
"EG",
|
||||
"MA",
|
||||
"DZ",
|
||||
"TN",
|
||||
"LB",
|
||||
"JO",
|
||||
"PS",
|
||||
"IN",
|
||||
"KZ",
|
||||
"MD",
|
||||
"UA",
|
||||
"AL",
|
||||
"BA",
|
||||
"HR",
|
||||
"ME",
|
||||
"MK",
|
||||
"RS",
|
||||
"SI",
|
||||
"KR",
|
||||
"BD",
|
||||
"PK",
|
||||
"LK",
|
||||
"GH",
|
||||
"KE",
|
||||
"NG",
|
||||
"TZ",
|
||||
"UG",
|
||||
"AG",
|
||||
"AM",
|
||||
"BS",
|
||||
"BB",
|
||||
"BZ",
|
||||
"BT",
|
||||
"BW",
|
||||
"BF",
|
||||
"CV",
|
||||
"CW",
|
||||
"DM",
|
||||
"FJ",
|
||||
"GM",
|
||||
"GE",
|
||||
"GD",
|
||||
"GW",
|
||||
"GY",
|
||||
"HT",
|
||||
"JM",
|
||||
"KI",
|
||||
"LS",
|
||||
"LR",
|
||||
"MW",
|
||||
"MV",
|
||||
"ML",
|
||||
"MH",
|
||||
"FM",
|
||||
"NA",
|
||||
"NR",
|
||||
"NE",
|
||||
"PW",
|
||||
"PG",
|
||||
"PR",
|
||||
"WS",
|
||||
"SM",
|
||||
"ST",
|
||||
"SN",
|
||||
"SC",
|
||||
"SL",
|
||||
"SB",
|
||||
"KN",
|
||||
"LC",
|
||||
"VC",
|
||||
"SR",
|
||||
"TL",
|
||||
"TO",
|
||||
"TT",
|
||||
"TV",
|
||||
"VU",
|
||||
"AZ",
|
||||
"BN",
|
||||
"BI",
|
||||
"KH",
|
||||
"CM",
|
||||
"TD",
|
||||
"KM",
|
||||
"GQ",
|
||||
"SZ",
|
||||
"GA",
|
||||
"GN",
|
||||
"KG",
|
||||
"LA",
|
||||
"MO",
|
||||
"MR",
|
||||
"MN",
|
||||
"NP",
|
||||
"RW",
|
||||
"TG",
|
||||
"UZ",
|
||||
"ZW",
|
||||
"BJ",
|
||||
"MG",
|
||||
"MU",
|
||||
"MZ",
|
||||
"AO",
|
||||
"CI",
|
||||
"DJ",
|
||||
"ZM",
|
||||
"CD",
|
||||
"CG",
|
||||
"IQ",
|
||||
"LY",
|
||||
"TJ",
|
||||
"VE",
|
||||
"ET",
|
||||
"XK"
|
||||
],
|
||||
"external_urls": {
|
||||
"spotify": "https://open.spotify.com/album/5SGtrmYbIo0Dsg4kJ4qjM6"
|
||||
},
|
||||
"href": "https://api.spotify.com/v1/albums/5SGtrmYbIo0Dsg4kJ4qjM6",
|
||||
"id": "5SGtrmYbIo0Dsg4kJ4qjM6",
|
||||
"images": [
|
||||
{
|
||||
"height": 300,
|
||||
"url": "https://i.scdn.co/image/ab67616d00001e0209ba52a5116e0c3e8461f58b",
|
||||
"width": 300
|
||||
},
|
||||
{
|
||||
"height": 64,
|
||||
"url": "https://i.scdn.co/image/ab67616d0000485109ba52a5116e0c3e8461f58b",
|
||||
"width": 64
|
||||
},
|
||||
{
|
||||
"height": 640,
|
||||
"url": "https://i.scdn.co/image/ab67616d0000b27309ba52a5116e0c3e8461f58b",
|
||||
"width": 640
|
||||
}
|
||||
],
|
||||
"name": "Moon Music",
|
||||
"release_date": "2024-10-04",
|
||||
"release_date_precision": "day",
|
||||
"total_tracks": 10,
|
||||
"type": "album",
|
||||
"uri": "spotify:album:5SGtrmYbIo0Dsg4kJ4qjM6"
|
||||
},
|
||||
{
|
||||
"album_type": "album",
|
||||
"artists": [
|
||||
{
|
||||
"external_urls": {
|
||||
"spotify": "https://open.spotify.com/artist/4U9nsRTH2mr9L4UXEWqG5e"
|
||||
},
|
||||
"href": "https://api.spotify.com/v1/artists/4U9nsRTH2mr9L4UXEWqG5e",
|
||||
"id": "4U9nsRTH2mr9L4UXEWqG5e",
|
||||
"name": "Bente",
|
||||
"type": "artist",
|
||||
"uri": "spotify:artist:4U9nsRTH2mr9L4UXEWqG5e"
|
||||
}
|
||||
],
|
||||
"available_markets": [
|
||||
"AR",
|
||||
"AU",
|
||||
"AT",
|
||||
"BE",
|
||||
"BO",
|
||||
"BR",
|
||||
"BG",
|
||||
"CA",
|
||||
"CL",
|
||||
"CO",
|
||||
"CR",
|
||||
"CY",
|
||||
"CZ",
|
||||
"DK",
|
||||
"DO",
|
||||
"DE",
|
||||
"EC",
|
||||
"EE",
|
||||
"SV",
|
||||
"FI",
|
||||
"FR",
|
||||
"GR",
|
||||
"GT",
|
||||
"HN",
|
||||
"HK",
|
||||
"HU",
|
||||
"IS",
|
||||
"IE",
|
||||
"IT",
|
||||
"LV",
|
||||
"LT",
|
||||
"LU",
|
||||
"MY",
|
||||
"MT",
|
||||
"MX",
|
||||
"NL",
|
||||
"NZ",
|
||||
"NI",
|
||||
"NO",
|
||||
"PA",
|
||||
"PY",
|
||||
"PE",
|
||||
"PH",
|
||||
"PL",
|
||||
"PT",
|
||||
"SG",
|
||||
"SK",
|
||||
"ES",
|
||||
"SE",
|
||||
"CH",
|
||||
"TW",
|
||||
"TR",
|
||||
"UY",
|
||||
"US",
|
||||
"GB",
|
||||
"AD",
|
||||
"LI",
|
||||
"MC",
|
||||
"ID",
|
||||
"JP",
|
||||
"TH",
|
||||
"VN",
|
||||
"RO",
|
||||
"IL",
|
||||
"ZA",
|
||||
"SA",
|
||||
"AE",
|
||||
"BH",
|
||||
"QA",
|
||||
"OM",
|
||||
"KW",
|
||||
"EG",
|
||||
"MA",
|
||||
"DZ",
|
||||
"TN",
|
||||
"LB",
|
||||
"JO",
|
||||
"PS",
|
||||
"IN",
|
||||
"KZ",
|
||||
"MD",
|
||||
"UA",
|
||||
"AL",
|
||||
"BA",
|
||||
"HR",
|
||||
"ME",
|
||||
"MK",
|
||||
"RS",
|
||||
"SI",
|
||||
"KR",
|
||||
"BD",
|
||||
"PK",
|
||||
"LK",
|
||||
"GH",
|
||||
"KE",
|
||||
"NG",
|
||||
"TZ",
|
||||
"UG",
|
||||
"AG",
|
||||
"AM",
|
||||
"BS",
|
||||
"BB",
|
||||
"BZ",
|
||||
"BT",
|
||||
"BW",
|
||||
"BF",
|
||||
"CV",
|
||||
"CW",
|
||||
"DM",
|
||||
"FJ",
|
||||
"GM",
|
||||
"GE",
|
||||
"GD",
|
||||
"GW",
|
||||
"GY",
|
||||
"HT",
|
||||
"JM",
|
||||
"KI",
|
||||
"LS",
|
||||
"LR",
|
||||
"MW",
|
||||
"MV",
|
||||
"ML",
|
||||
"MH",
|
||||
"FM",
|
||||
"NA",
|
||||
"NR",
|
||||
"NE",
|
||||
"PW",
|
||||
"PG",
|
||||
"WS",
|
||||
"SM",
|
||||
"ST",
|
||||
"SN",
|
||||
"SC",
|
||||
"SL",
|
||||
"SB",
|
||||
"KN",
|
||||
"LC",
|
||||
"VC",
|
||||
"SR",
|
||||
"TL",
|
||||
"TO",
|
||||
"TT",
|
||||
"TV",
|
||||
"VU",
|
||||
"AZ",
|
||||
"BN",
|
||||
"BI",
|
||||
"KH",
|
||||
"CM",
|
||||
"TD",
|
||||
"KM",
|
||||
"GQ",
|
||||
"SZ",
|
||||
"GA",
|
||||
"GN",
|
||||
"KG",
|
||||
"LA",
|
||||
"MO",
|
||||
"MR",
|
||||
"MN",
|
||||
"NP",
|
||||
"RW",
|
||||
"TG",
|
||||
"UZ",
|
||||
"ZW",
|
||||
"BJ",
|
||||
"MG",
|
||||
"MU",
|
||||
"MZ",
|
||||
"AO",
|
||||
"CI",
|
||||
"DJ",
|
||||
"ZM",
|
||||
"CD",
|
||||
"CG",
|
||||
"IQ",
|
||||
"LY",
|
||||
"TJ",
|
||||
"VE",
|
||||
"ET",
|
||||
"XK"
|
||||
],
|
||||
"external_urls": {
|
||||
"spotify": "https://open.spotify.com/album/713lZ7AF55fEFSQgcttj9y"
|
||||
},
|
||||
"href": "https://api.spotify.com/v1/albums/713lZ7AF55fEFSQgcttj9y",
|
||||
"id": "713lZ7AF55fEFSQgcttj9y",
|
||||
"images": [
|
||||
{
|
||||
"height": 300,
|
||||
"url": "https://i.scdn.co/image/ab67616d00001e02ab9953b1d18f8233f6b26027",
|
||||
"width": 300
|
||||
},
|
||||
{
|
||||
"height": 64,
|
||||
"url": "https://i.scdn.co/image/ab67616d00004851ab9953b1d18f8233f6b26027",
|
||||
"width": 64
|
||||
},
|
||||
{
|
||||
"height": 640,
|
||||
"url": "https://i.scdn.co/image/ab67616d0000b273ab9953b1d18f8233f6b26027",
|
||||
"width": 640
|
||||
}
|
||||
],
|
||||
"name": "drift",
|
||||
"release_date": "2024-10-03",
|
||||
"release_date_precision": "day",
|
||||
"total_tracks": 14,
|
||||
"type": "album",
|
||||
"uri": "spotify:album:713lZ7AF55fEFSQgcttj9y"
|
||||
}
|
||||
],
|
||||
"limit": 20,
|
||||
"next": "https://api.spotify.com/v1/browse/new-releases?offset=20&limit=20&locale=en-US,en;q%3D0.5",
|
||||
"offset": 0,
|
||||
"previous": null,
|
||||
"total": 100
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user