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