mirror of
https://github.com/home-assistant/core.git
synced 2026-03-25 17:10:32 +01:00
Compare commits
78 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
00d5e89951 | ||
|
|
557d072a4d | ||
|
|
6c3917e927 | ||
|
|
e895c1b2fd | ||
|
|
dae971cd98 | ||
|
|
807df50eab | ||
|
|
aa05ff03b3 | ||
|
|
622b92682e | ||
|
|
a81146a227 | ||
|
|
530dcadf19 | ||
|
|
4aa67ddf22 | ||
|
|
8e95b19c4c | ||
|
|
5558b33600 | ||
|
|
0130ac6770 | ||
|
|
26d22e4d62 | ||
|
|
532bc02d66 | ||
|
|
893eac0e84 | ||
|
|
c1bd83c9c0 | ||
|
|
b3c27e9f93 | ||
|
|
92e237ade2 | ||
|
|
cbc573a6b1 | ||
|
|
0c059cfc27 | ||
|
|
143ce9d7b3 | ||
|
|
a6aa837d40 | ||
|
|
c58b4a0066 | ||
|
|
5155242ba7 | ||
|
|
085680f6bf | ||
|
|
98ecaaa6d2 | ||
|
|
5ad199fe16 | ||
|
|
413cb98424 | ||
|
|
b38c5bcaf2 | ||
|
|
fa85dfb3b5 | ||
|
|
f0c6a035db | ||
|
|
3f0c200e56 | ||
|
|
a2259ede28 | ||
|
|
24c2b6fe81 | ||
|
|
efc7350e6f | ||
|
|
5f525fc2a1 | ||
|
|
f619a3e7af | ||
|
|
4e43492342 | ||
|
|
39e70071d3 | ||
|
|
6da0936a66 | ||
|
|
5257702530 | ||
|
|
93da5be052 | ||
|
|
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 |
@@ -1,6 +1,5 @@
|
||||
"""Defines a base Alexa Devices entity."""
|
||||
|
||||
from aioamazondevices.const.devices import SPEAKER_GROUP_DEVICE_TYPE
|
||||
from aioamazondevices.structures import AmazonDevice
|
||||
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
@@ -25,20 +24,15 @@ class AmazonEntity(CoordinatorEntity[AmazonDevicesCoordinator]):
|
||||
"""Initialize the entity."""
|
||||
super().__init__(coordinator)
|
||||
self._serial_num = serial_num
|
||||
model = self.device.model
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, serial_num)},
|
||||
name=self.device.account_name,
|
||||
model=model,
|
||||
model=self.device.model,
|
||||
model_id=self.device.device_type,
|
||||
manufacturer=self.device.manufacturer or "Amazon",
|
||||
hw_version=self.device.hardware_version,
|
||||
sw_version=(
|
||||
self.device.software_version
|
||||
if model != SPEAKER_GROUP_DEVICE_TYPE
|
||||
else None
|
||||
),
|
||||
serial_number=serial_num if model != SPEAKER_GROUP_DEVICE_TYPE else None,
|
||||
sw_version=self.device.software_version,
|
||||
serial_number=serial_num,
|
||||
)
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{serial_num}-{description.key}"
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aioamazondevices"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["aioamazondevices==13.0.0"]
|
||||
"requirements": ["aioamazondevices==13.0.1"]
|
||||
}
|
||||
|
||||
@@ -101,7 +101,10 @@ class AmazonSwitchEntity(AmazonEntity, SwitchEntity):
|
||||
assert method is not None
|
||||
|
||||
await method(self.device, state)
|
||||
await self.coordinator.async_request_refresh()
|
||||
self.coordinator.data[self.device.serial_number].sensors[
|
||||
self.entity_description.key
|
||||
].value = state
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the switch on."""
|
||||
|
||||
@@ -9,5 +9,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["pyanglianwater"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["pyanglianwater==3.1.0"]
|
||||
"requirements": ["pyanglianwater==3.1.1"]
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ from __future__ import annotations
|
||||
from pathlib import Path
|
||||
from typing import cast
|
||||
|
||||
from aiohttp import ClientResponseError
|
||||
from aiohttp import ClientError
|
||||
from yalexs.exceptions import AugustApiAIOHTTPError
|
||||
from yalexs.manager.exceptions import CannotConnect, InvalidAuth, RequireValidation
|
||||
from yalexs.manager.gateway import Config as YaleXSConfig
|
||||
@@ -13,7 +13,12 @@ from yalexs.manager.gateway import Config as YaleXSConfig
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.exceptions import (
|
||||
ConfigEntryAuthFailed,
|
||||
ConfigEntryNotReady,
|
||||
OAuth2TokenRequestError,
|
||||
OAuth2TokenRequestReauthError,
|
||||
)
|
||||
from homeassistant.helpers import device_registry as dr, issue_registry as ir
|
||||
from homeassistant.helpers.config_entry_oauth2_flow import (
|
||||
ImplementationUnavailableError,
|
||||
@@ -45,11 +50,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: AugustConfigEntry) -> bo
|
||||
august_gateway = AugustGateway(Path(hass.config.config_dir), session, oauth_session)
|
||||
try:
|
||||
await async_setup_august(hass, entry, august_gateway)
|
||||
except OAuth2TokenRequestReauthError as err:
|
||||
raise ConfigEntryAuthFailed from err
|
||||
except (RequireValidation, InvalidAuth) as err:
|
||||
raise ConfigEntryAuthFailed from err
|
||||
except TimeoutError as err:
|
||||
raise ConfigEntryNotReady("Timed out connecting to august api") from err
|
||||
except (AugustApiAIOHTTPError, ClientResponseError, CannotConnect) as err:
|
||||
except (
|
||||
AugustApiAIOHTTPError,
|
||||
OAuth2TokenRequestError,
|
||||
ClientError,
|
||||
CannotConnect,
|
||||
) as err:
|
||||
raise ConfigEntryNotReady from err
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
return True
|
||||
|
||||
@@ -30,5 +30,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["pubnub", "yalexs"],
|
||||
"requirements": ["yalexs==9.2.0", "yalexs-ble==3.2.7"]
|
||||
"requirements": ["yalexs==9.2.0", "yalexs-ble==3.2.8"]
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["axis"],
|
||||
"requirements": ["axis==66"],
|
||||
"requirements": ["axis==67"],
|
||||
"ssdp": [
|
||||
{
|
||||
"manufacturer": "AXIS"
|
||||
|
||||
@@ -246,6 +246,8 @@ def decrypt_backup(
|
||||
except (DecryptError, SecureTarError, tarfile.TarError) as err:
|
||||
LOGGER.warning("Error decrypting backup: %s", err)
|
||||
error = err
|
||||
except Abort:
|
||||
raise
|
||||
except Exception as err: # noqa: BLE001
|
||||
LOGGER.exception("Unexpected error when decrypting backup: %s", err)
|
||||
error = err
|
||||
@@ -332,8 +334,10 @@ def encrypt_backup(
|
||||
except (EncryptError, SecureTarError, tarfile.TarError) as err:
|
||||
LOGGER.warning("Error encrypting backup: %s", err)
|
||||
error = err
|
||||
except Abort:
|
||||
raise
|
||||
except Exception as err: # noqa: BLE001
|
||||
LOGGER.exception("Unexpected error when decrypting backup: %s", err)
|
||||
LOGGER.exception("Unexpected error when encrypting backup: %s", err)
|
||||
error = err
|
||||
else:
|
||||
# Pad the output stream to the requested minimum size
|
||||
|
||||
@@ -32,7 +32,7 @@ from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import CONF_PASSKEY, DOMAIN
|
||||
from .const import CONF_PASSKEY, DOMAIN, LOGGER
|
||||
from .coordinator import BSBLanFastCoordinator, BSBLanSlowCoordinator
|
||||
from .services import async_setup_services
|
||||
|
||||
@@ -52,7 +52,7 @@ class BSBLanData:
|
||||
client: BSBLAN
|
||||
device: Device
|
||||
info: Info
|
||||
static: StaticState
|
||||
static: StaticState | None
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
@@ -82,11 +82,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: BSBLanConfigEntry) -> bo
|
||||
# the connection by fetching firmware version
|
||||
await bsblan.initialize()
|
||||
|
||||
# Fetch device metadata in parallel for faster startup
|
||||
device, info, static = await asyncio.gather(
|
||||
# Fetch required device metadata in parallel for faster startup
|
||||
device, info = await asyncio.gather(
|
||||
bsblan.device(),
|
||||
bsblan.info(),
|
||||
bsblan.static_values(),
|
||||
)
|
||||
except BSBLANConnectionError as err:
|
||||
raise ConfigEntryNotReady(
|
||||
@@ -111,6 +110,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: BSBLanConfigEntry) -> bo
|
||||
translation_key="setup_general_error",
|
||||
) from err
|
||||
|
||||
try:
|
||||
static = await bsblan.static_values()
|
||||
except (BSBLANError, TimeoutError) as err:
|
||||
LOGGER.debug(
|
||||
"Static values not available for %s: %s",
|
||||
entry.data[CONF_HOST],
|
||||
err,
|
||||
)
|
||||
static = None
|
||||
|
||||
# Create coordinators with the already-initialized client
|
||||
fast_coordinator = BSBLanFastCoordinator(hass, entry, bsblan)
|
||||
slow_coordinator = BSBLanSlowCoordinator(hass, entry, bsblan)
|
||||
|
||||
@@ -90,10 +90,11 @@ class BSBLANClimate(BSBLanEntity, ClimateEntity):
|
||||
self._attr_unique_id = f"{format_mac(data.device.MAC)}-climate"
|
||||
|
||||
# Set temperature range if available, otherwise use Home Assistant defaults
|
||||
if data.static.min_temp is not None and data.static.min_temp.value is not None:
|
||||
self._attr_min_temp = data.static.min_temp.value
|
||||
if data.static.max_temp is not None and data.static.max_temp.value is not None:
|
||||
self._attr_max_temp = data.static.max_temp.value
|
||||
if (static := data.static) is not None:
|
||||
if (min_temp := static.min_temp) is not None and min_temp.value is not None:
|
||||
self._attr_min_temp = min_temp.value
|
||||
if (max_temp := static.max_temp) is not None and max_temp.value is not None:
|
||||
self._attr_max_temp = max_temp.value
|
||||
self._attr_temperature_unit = data.fast_coordinator.client.get_temperature_unit
|
||||
|
||||
@property
|
||||
|
||||
@@ -24,7 +24,7 @@ async def async_get_config_entry_diagnostics(
|
||||
"sensor": data.fast_coordinator.data.sensor.model_dump(),
|
||||
"dhw": data.fast_coordinator.data.dhw.model_dump(),
|
||||
},
|
||||
"static": data.static.model_dump(),
|
||||
"static": data.static.model_dump() if data.static is not None else None,
|
||||
}
|
||||
|
||||
# Add DHW config and schedule from slow coordinator if available
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["casttube", "pychromecast"],
|
||||
"requirements": ["PyChromecast==14.0.9"],
|
||||
"requirements": ["PyChromecast==14.0.10"],
|
||||
"single_config_entry": true,
|
||||
"zeroconf": ["_googlecast._tcp.local."]
|
||||
}
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["aiocomelit"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["aiocomelit==2.0.0"]
|
||||
"requirements": ["aiocomelit==2.0.1"]
|
||||
}
|
||||
|
||||
@@ -283,6 +283,7 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
self._username = user_input[CONF_USERNAME]
|
||||
self._password = user_input[CONF_PASSWORD]
|
||||
self._use_tls = user_input[CONF_SSL]
|
||||
self._feature_device_discovery = user_input[CONF_FEATURE_DEVICE_TRACKING]
|
||||
|
||||
self._port = self._determine_port(user_input)
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ from fritzconnection.core.exceptions import (
|
||||
FritzSecurityError,
|
||||
FritzServiceError,
|
||||
)
|
||||
from requests.exceptions import ConnectionError
|
||||
|
||||
from homeassistant.const import Platform
|
||||
|
||||
@@ -68,6 +69,7 @@ BUTTON_TYPE_WOL = "WakeOnLan"
|
||||
UPTIME_DEVIATION = 5
|
||||
|
||||
FRITZ_EXCEPTIONS = (
|
||||
ConnectionError,
|
||||
FritzActionError,
|
||||
FritzActionFailedError,
|
||||
FritzConnectionException,
|
||||
|
||||
@@ -133,26 +133,20 @@ async def _async_wifi_entities_list(
|
||||
]
|
||||
)
|
||||
_LOGGER.debug("WiFi networks count: %s", wifi_count)
|
||||
networks: dict = {}
|
||||
networks: dict[int, dict[str, Any]] = {}
|
||||
for i in range(1, wifi_count + 1):
|
||||
network_info = await avm_wrapper.async_get_wlan_configuration(i)
|
||||
# Devices with 4 WLAN services, use the 2nd for internal communications
|
||||
if not (wifi_count == 4 and i == 2):
|
||||
networks[i] = {
|
||||
"ssid": network_info["NewSSID"],
|
||||
"bssid": network_info["NewBSSID"],
|
||||
"standard": network_info["NewStandard"],
|
||||
"enabled": network_info["NewEnable"],
|
||||
"status": network_info["NewStatus"],
|
||||
}
|
||||
networks[i] = network_info
|
||||
for i, network in networks.copy().items():
|
||||
networks[i]["switch_name"] = network["ssid"]
|
||||
networks[i]["switch_name"] = network["NewSSID"]
|
||||
if (
|
||||
len(
|
||||
[
|
||||
j
|
||||
for j, n in networks.items()
|
||||
if slugify(n["ssid"]) == slugify(network["ssid"])
|
||||
if slugify(n["NewSSID"]) == slugify(network["NewSSID"])
|
||||
]
|
||||
)
|
||||
> 1
|
||||
@@ -434,13 +428,11 @@ class FritzBoxPortSwitch(FritzBoxBaseSwitch):
|
||||
for key, attr in attributes_dict.items():
|
||||
self._attributes[attr] = self.port_mapping[key]
|
||||
|
||||
async def _async_switch_on_off_executor(self, turn_on: bool) -> bool:
|
||||
async def _async_switch_on_off_executor(self, turn_on: bool) -> None:
|
||||
self.port_mapping["NewEnabled"] = "1" if turn_on else "0"
|
||||
|
||||
resp = await self._avm_wrapper.async_add_port_mapping(
|
||||
await self._avm_wrapper.async_add_port_mapping(
|
||||
self.connection_type, self.port_mapping
|
||||
)
|
||||
return bool(resp is not None)
|
||||
|
||||
|
||||
class FritzBoxDeflectionSwitch(FritzBoxBaseCoordinatorSwitch):
|
||||
@@ -525,12 +517,11 @@ class FritzBoxProfileSwitch(FritzDeviceBase, SwitchEntity):
|
||||
"""Turn off switch."""
|
||||
await self._async_handle_turn_on_off(turn_on=False)
|
||||
|
||||
async def _async_handle_turn_on_off(self, turn_on: bool) -> bool:
|
||||
async def _async_handle_turn_on_off(self, turn_on: bool) -> None:
|
||||
"""Handle switch state change request."""
|
||||
await self._avm_wrapper.async_set_allow_wan_access(self.ip_address, turn_on)
|
||||
self._avm_wrapper.devices[self._mac].wan_access = turn_on
|
||||
self.async_write_ha_state()
|
||||
return True
|
||||
|
||||
|
||||
class FritzBoxWifiSwitch(FritzBoxBaseSwitch):
|
||||
@@ -541,10 +532,11 @@ class FritzBoxWifiSwitch(FritzBoxBaseSwitch):
|
||||
avm_wrapper: AvmWrapper,
|
||||
device_friendly_name: str,
|
||||
network_num: int,
|
||||
network_data: dict,
|
||||
network_data: dict[str, Any],
|
||||
) -> None:
|
||||
"""Init Fritz Wifi switch."""
|
||||
self._avm_wrapper = avm_wrapper
|
||||
self._wifi_info = network_data
|
||||
|
||||
self._attributes = {}
|
||||
self._attr_entity_category = EntityCategory.CONFIG
|
||||
@@ -560,7 +552,7 @@ class FritzBoxWifiSwitch(FritzBoxBaseSwitch):
|
||||
type=SWITCH_TYPE_WIFINETWORK,
|
||||
callback_update=self._async_fetch_update,
|
||||
callback_switch=self._async_switch_on_off_executor,
|
||||
init_state=network_data["enabled"],
|
||||
init_state=network_data["NewEnable"],
|
||||
)
|
||||
super().__init__(self._avm_wrapper, device_friendly_name, switch_info)
|
||||
|
||||
@@ -587,7 +579,9 @@ class FritzBoxWifiSwitch(FritzBoxBaseSwitch):
|
||||
self._attributes["mac_address_control"] = wifi_info[
|
||||
"NewMACAddressControlEnabled"
|
||||
]
|
||||
self._wifi_info = wifi_info
|
||||
|
||||
async def _async_switch_on_off_executor(self, turn_on: bool) -> None:
|
||||
"""Handle wifi switch."""
|
||||
self._wifi_info["NewEnable"] = turn_on
|
||||
await self._avm_wrapper.async_set_wlan_configuration(self._network_num, turn_on)
|
||||
|
||||
@@ -21,5 +21,5 @@
|
||||
"integration_type": "system",
|
||||
"preview_features": { "winter_mode": {} },
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["home-assistant-frontend==20260304.0"]
|
||||
"requirements": ["home-assistant-frontend==20260312.1"]
|
||||
}
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["googleapiclient"],
|
||||
"requirements": ["gcal-sync==8.0.0", "oauth2client==4.1.3", "ical==13.2.0"]
|
||||
"requirements": ["gcal-sync==8.0.0", "oauth2client==4.1.3", "ical==13.2.2"]
|
||||
}
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["google_weather_api"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["python-google-weather-api==0.0.4"]
|
||||
"requirements": ["python-google-weather-api==0.0.6"]
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"dependencies": ["network"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/govee_light_local",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["govee-local-api==2.3.0"]
|
||||
"requirements": ["govee-local-api==2.4.0"]
|
||||
}
|
||||
|
||||
@@ -7,6 +7,6 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["greenplanet-energy-api==0.1.4"],
|
||||
"requirements": ["greenplanet-energy-api==0.1.10"],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
||||
@@ -117,13 +117,21 @@ async def async_setup_entry(hass: HomeAssistant, entry: HikvisionConfigEntry) ->
|
||||
# Map raw event type names to friendly names using SENSOR_MAP
|
||||
mapped_events: dict[str, list[int]] = {}
|
||||
for event_type, channels in nvr_events.items():
|
||||
friendly_name = SENSOR_MAP.get(event_type.lower(), event_type)
|
||||
event_key = event_type.lower()
|
||||
# Skip videoloss - used as watchdog by pyhik, not a real sensor
|
||||
if event_key == "videoloss":
|
||||
continue
|
||||
friendly_name = SENSOR_MAP.get(event_key)
|
||||
if friendly_name is None:
|
||||
_LOGGER.debug("Skipping unmapped event type: %s", event_type)
|
||||
continue
|
||||
if friendly_name in mapped_events:
|
||||
mapped_events[friendly_name].extend(channels)
|
||||
else:
|
||||
mapped_events[friendly_name] = list(channels)
|
||||
_LOGGER.debug("Mapped NVR events: %s", mapped_events)
|
||||
camera.inject_events(mapped_events)
|
||||
if mapped_events:
|
||||
camera.inject_events(mapped_events)
|
||||
else:
|
||||
_LOGGER.debug(
|
||||
"No event triggers returned from %s. "
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from apyhiveapi import Auth
|
||||
@@ -26,6 +27,8 @@ from homeassistant.core import callback
|
||||
from . import HiveConfigEntry
|
||||
from .const import CONF_CODE, CONF_DEVICE_NAME, CONFIG_ENTRY_VERSION, DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class HiveFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a Hive config flow."""
|
||||
@@ -36,7 +39,7 @@ class HiveFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
def __init__(self) -> None:
|
||||
"""Initialize the config flow."""
|
||||
self.data: dict[str, Any] = {}
|
||||
self.tokens: dict[str, str] = {}
|
||||
self.tokens: dict[str, Any] = {}
|
||||
self.device_registration: bool = False
|
||||
self.device_name = "Home Assistant"
|
||||
|
||||
@@ -67,11 +70,22 @@ class HiveFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
except HiveApiError:
|
||||
errors["base"] = "no_internet_available"
|
||||
|
||||
if (
|
||||
auth_result := self.tokens.get("AuthenticationResult", {})
|
||||
) and auth_result.get("NewDeviceMetadata"):
|
||||
_LOGGER.debug("Login successful, New device detected")
|
||||
self.device_registration = True
|
||||
return await self.async_step_configuration()
|
||||
|
||||
if self.tokens.get("ChallengeName") == "SMS_MFA":
|
||||
_LOGGER.debug("Login successful, SMS 2FA required")
|
||||
# Complete SMS 2FA.
|
||||
return await self.async_step_2fa()
|
||||
|
||||
if not errors:
|
||||
_LOGGER.debug(
|
||||
"Login successful, no new device detected, no 2FA required"
|
||||
)
|
||||
# Complete the entry.
|
||||
try:
|
||||
return await self.async_setup_hive_entry()
|
||||
@@ -103,6 +117,7 @@ class HiveFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
errors["base"] = "no_internet_available"
|
||||
|
||||
if not errors:
|
||||
_LOGGER.debug("2FA successful")
|
||||
if self.source == SOURCE_REAUTH:
|
||||
return await self.async_setup_hive_entry()
|
||||
self.device_registration = True
|
||||
@@ -119,10 +134,11 @@ class HiveFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
if user_input:
|
||||
if self.device_registration:
|
||||
_LOGGER.debug("Attempting to register device")
|
||||
self.device_name = user_input["device_name"]
|
||||
await self.hive_auth.device_registration(user_input["device_name"])
|
||||
self.data["device_data"] = await self.hive_auth.get_device_data()
|
||||
|
||||
_LOGGER.debug("Device registration successful")
|
||||
try:
|
||||
return await self.async_setup_hive_entry()
|
||||
except UnknownHiveError:
|
||||
@@ -142,6 +158,7 @@ class HiveFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
raise UnknownHiveError
|
||||
|
||||
# Setup the config entry
|
||||
_LOGGER.debug("Setting up Hive entry")
|
||||
self.data["tokens"] = self.tokens
|
||||
if self.source == SOURCE_REAUTH:
|
||||
return self.async_update_reload_and_abort(
|
||||
@@ -160,6 +177,7 @@ class HiveFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
CONF_USERNAME: entry_data[CONF_USERNAME],
|
||||
CONF_PASSWORD: entry_data[CONF_PASSWORD],
|
||||
}
|
||||
_LOGGER.debug("Reauthenticating user")
|
||||
return await self.async_step_user(data)
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_polling",
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["hyponcloud==0.3.0"]
|
||||
"requirements": ["hyponcloud==0.9.0"]
|
||||
}
|
||||
|
||||
@@ -21,11 +21,17 @@ from .coordinator import HypontechConfigEntry, HypontechDataCoordinator
|
||||
from .entity import HypontechEntity, HypontechPlantEntity
|
||||
|
||||
|
||||
def _power_unit(data: OverviewData | PlantData) -> str:
|
||||
"""Return the unit of measurement for power based on the API unit."""
|
||||
return UnitOfPower.KILO_WATT if data.company.upper() == "KW" else UnitOfPower.WATT
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class HypontechSensorDescription(SensorEntityDescription):
|
||||
"""Describes Hypontech overview sensor entity."""
|
||||
|
||||
value_fn: Callable[[OverviewData], float | None]
|
||||
unit_fn: Callable[[OverviewData], str] | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
@@ -33,15 +39,16 @@ class HypontechPlantSensorDescription(SensorEntityDescription):
|
||||
"""Describes Hypontech plant sensor entity."""
|
||||
|
||||
value_fn: Callable[[PlantData], float | None]
|
||||
unit_fn: Callable[[PlantData], str] | None = None
|
||||
|
||||
|
||||
OVERVIEW_SENSORS: tuple[HypontechSensorDescription, ...] = (
|
||||
HypontechSensorDescription(
|
||||
key="pv_power",
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda c: c.power,
|
||||
unit_fn=_power_unit,
|
||||
),
|
||||
HypontechSensorDescription(
|
||||
key="lifetime_energy",
|
||||
@@ -64,10 +71,10 @@ OVERVIEW_SENSORS: tuple[HypontechSensorDescription, ...] = (
|
||||
PLANT_SENSORS: tuple[HypontechPlantSensorDescription, ...] = (
|
||||
HypontechPlantSensorDescription(
|
||||
key="pv_power",
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda c: c.power,
|
||||
unit_fn=_power_unit,
|
||||
),
|
||||
HypontechPlantSensorDescription(
|
||||
key="lifetime_energy",
|
||||
@@ -124,6 +131,13 @@ class HypontechOverviewSensor(HypontechEntity, SensorEntity):
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{coordinator.account_id}_{description.key}"
|
||||
|
||||
@property
|
||||
def native_unit_of_measurement(self) -> str | None:
|
||||
"""Return the unit of measurement."""
|
||||
if self.entity_description.unit_fn is not None:
|
||||
return self.entity_description.unit_fn(self.coordinator.data.overview)
|
||||
return super().native_unit_of_measurement
|
||||
|
||||
@property
|
||||
def native_value(self) -> float | None:
|
||||
"""Return the state of the sensor."""
|
||||
@@ -146,6 +160,13 @@ class HypontechPlantSensor(HypontechPlantEntity, SensorEntity):
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{plant_id}_{description.key}"
|
||||
|
||||
@property
|
||||
def native_unit_of_measurement(self) -> str | None:
|
||||
"""Return the unit of measurement."""
|
||||
if self.entity_description.unit_fn is not None:
|
||||
return self.entity_description.unit_fn(self.plant)
|
||||
return super().native_unit_of_measurement
|
||||
|
||||
@property
|
||||
def native_value(self) -> float | None:
|
||||
"""Return the state of the sensor."""
|
||||
|
||||
@@ -97,7 +97,6 @@ INTELLIFIRE_SENSORS: tuple[IntellifireSensorEntityDescription, ...] = (
|
||||
IntellifireSensorEntityDescription(
|
||||
key="timer_end_timestamp",
|
||||
translation_key="timer_end_timestamp",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
device_class=SensorDeviceClass.TIMESTAMP,
|
||||
value_fn=_time_remaining_to_timestamp,
|
||||
),
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["jvcprojector"],
|
||||
"requirements": ["pyjvcprojector==2.0.1"]
|
||||
"requirements": ["pyjvcprojector==2.0.3"]
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ class LGDevice(MediaPlayerEntity):
|
||||
"""Representation of an LG soundbar device."""
|
||||
|
||||
_attr_should_poll = False
|
||||
_attr_state = MediaPlayerState.ON
|
||||
_attr_state = MediaPlayerState.OFF
|
||||
_attr_supported_features = (
|
||||
MediaPlayerEntityFeature.VOLUME_SET
|
||||
| MediaPlayerEntityFeature.VOLUME_MUTE
|
||||
@@ -79,6 +79,8 @@ class LGDevice(MediaPlayerEntity):
|
||||
self._treble = 0
|
||||
self._device = None
|
||||
self._support_play_control = False
|
||||
self._device_on = False
|
||||
self._stream_type = 0
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, unique_id)}, name=host
|
||||
)
|
||||
@@ -113,6 +115,7 @@ class LGDevice(MediaPlayerEntity):
|
||||
if "i_curr_func" in data:
|
||||
self._function = data["i_curr_func"]
|
||||
if "b_powerstatus" in data:
|
||||
self._device_on = data["b_powerstatus"]
|
||||
if data["b_powerstatus"]:
|
||||
self._attr_state = MediaPlayerState.ON
|
||||
else:
|
||||
@@ -157,17 +160,34 @@ class LGDevice(MediaPlayerEntity):
|
||||
|
||||
def _update_playinfo(self, data: dict[str, Any]) -> None:
|
||||
"""Update the player info."""
|
||||
if "i_stream_type" in data:
|
||||
if self._stream_type != data["i_stream_type"]:
|
||||
self._stream_type = data["i_stream_type"]
|
||||
# Ask device for current play info when stream type changed.
|
||||
self._device.get_play()
|
||||
if data["i_stream_type"] == 0:
|
||||
# If the stream type is 0 (aka the soundbar is used as an actual soundbar)
|
||||
# the last track info should be cleared and the state should only be on or off,
|
||||
# as all playing/paused are not applicable in this mode
|
||||
self._attr_media_image_url = None
|
||||
self._attr_media_artist = None
|
||||
self._attr_media_title = None
|
||||
if self._device_on:
|
||||
self._attr_state = MediaPlayerState.ON
|
||||
else:
|
||||
self._attr_state = MediaPlayerState.OFF
|
||||
if "i_play_ctrl" in data:
|
||||
if data["i_play_ctrl"] == 0:
|
||||
self._attr_state = MediaPlayerState.PLAYING
|
||||
else:
|
||||
self._attr_state = MediaPlayerState.PAUSED
|
||||
if self._device_on and self._stream_type != 0:
|
||||
if data["i_play_ctrl"] == 0:
|
||||
self._attr_state = MediaPlayerState.PLAYING
|
||||
else:
|
||||
self._attr_state = MediaPlayerState.PAUSED
|
||||
if "s_albumart" in data:
|
||||
self._attr_media_image_url = data["s_albumart"]
|
||||
self._attr_media_image_url = data["s_albumart"].strip() or None
|
||||
if "s_artist" in data:
|
||||
self._attr_media_artist = data["s_artist"]
|
||||
self._attr_media_artist = data["s_artist"].strip() or None
|
||||
if "s_title" in data:
|
||||
self._attr_media_title = data["s_title"]
|
||||
self._attr_media_title = data["s_title"].strip() or None
|
||||
if "b_support_play_ctrl" in data:
|
||||
self._support_play_control = data["b_support_play_ctrl"]
|
||||
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/local_calendar",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["ical"],
|
||||
"requirements": ["ical==13.2.0"]
|
||||
"requirements": ["ical==13.2.2"]
|
||||
}
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/local_todo",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["ical==13.2.0"]
|
||||
"requirements": ["ical==13.2.2"]
|
||||
}
|
||||
|
||||
@@ -80,6 +80,7 @@ class MatterUpdate(MatterEntity, UpdateEntity):
|
||||
# Matter server.
|
||||
_attr_should_poll = True
|
||||
_software_update: MatterSoftwareVersion | None = None
|
||||
_installed_software_version: int | None = None
|
||||
_cancel_update: CALLBACK_TYPE | None = None
|
||||
_attr_supported_features = (
|
||||
UpdateEntityFeature.INSTALL
|
||||
@@ -92,6 +93,9 @@ class MatterUpdate(MatterEntity, UpdateEntity):
|
||||
def _update_from_device(self) -> None:
|
||||
"""Update from device."""
|
||||
|
||||
self._installed_software_version = self.get_matter_attribute_value(
|
||||
clusters.BasicInformation.Attributes.SoftwareVersion
|
||||
)
|
||||
self._attr_installed_version = self.get_matter_attribute_value(
|
||||
clusters.BasicInformation.Attributes.SoftwareVersionString
|
||||
)
|
||||
@@ -123,6 +127,22 @@ class MatterUpdate(MatterEntity, UpdateEntity):
|
||||
else:
|
||||
self._attr_update_percentage = None
|
||||
|
||||
def _format_latest_version(
|
||||
self, update_information: MatterSoftwareVersion
|
||||
) -> str | None:
|
||||
"""Return the version string to expose in Home Assistant."""
|
||||
latest_version = update_information.software_version_string
|
||||
if self._installed_software_version is None:
|
||||
return latest_version
|
||||
|
||||
if update_information.software_version == self._installed_software_version:
|
||||
return self._attr_installed_version or latest_version
|
||||
|
||||
if latest_version == self._attr_installed_version:
|
||||
return f"{latest_version} ({update_information.software_version})"
|
||||
|
||||
return latest_version
|
||||
|
||||
async def async_update(self) -> None:
|
||||
"""Call when the entity needs to be updated."""
|
||||
try:
|
||||
@@ -130,11 +150,13 @@ class MatterUpdate(MatterEntity, UpdateEntity):
|
||||
node_id=self._endpoint.node.node_id
|
||||
)
|
||||
if not update_information:
|
||||
self._software_update = None
|
||||
self._attr_latest_version = self._attr_installed_version
|
||||
self._attr_release_url = None
|
||||
return
|
||||
|
||||
self._software_update = update_information
|
||||
self._attr_latest_version = update_information.software_version_string
|
||||
self._attr_latest_version = self._format_latest_version(update_information)
|
||||
self._attr_release_url = update_information.release_notes_url
|
||||
|
||||
except UpdateCheckError as err:
|
||||
@@ -212,7 +234,12 @@ class MatterUpdate(MatterEntity, UpdateEntity):
|
||||
|
||||
software_version: str | int | None = version
|
||||
if self._software_update is not None and (
|
||||
version is None or version == self._software_update.software_version_string
|
||||
version is None
|
||||
or version
|
||||
in {
|
||||
self._software_update.software_version_string,
|
||||
self._attr_latest_version,
|
||||
}
|
||||
):
|
||||
# Update to the version previously fetched and shown.
|
||||
# We can pass the integer version directly to speedup download.
|
||||
|
||||
@@ -188,6 +188,7 @@ class ProgramPhaseTumbleDryer(MieleEnum, missing_to_none=True):
|
||||
finished = 522, 11012
|
||||
extra_dry = 523
|
||||
hand_iron = 524
|
||||
hygiene_drying = 525
|
||||
moisten = 526
|
||||
thermo_spin = 527
|
||||
timed_drying = 528
|
||||
@@ -615,8 +616,10 @@ class OvenProgramId(MieleEnum, missing_to_none=True):
|
||||
pyrolytic = 323
|
||||
descale = 326
|
||||
evaporate_water = 327
|
||||
rinse = 333
|
||||
shabbat_program = 335
|
||||
yom_tov = 336
|
||||
hydroclean = 341
|
||||
drying = 357, 2028
|
||||
heat_crockery = 358
|
||||
prove_dough = 359, 2023
|
||||
@@ -721,7 +724,7 @@ class OvenProgramId(MieleEnum, missing_to_none=True):
|
||||
belgian_sponge_cake = 624
|
||||
goose_unstuffed = 625
|
||||
rack_of_lamb_with_vegetables = 634
|
||||
yorkshire_pudding = 635
|
||||
yorkshire_pudding = 635, 2352
|
||||
meat_loaf = 636
|
||||
defrost_meat = 647
|
||||
defrost_vegetables = 654
|
||||
@@ -1121,7 +1124,7 @@ class OvenProgramId(MieleEnum, missing_to_none=True):
|
||||
wholegrain_rice = 3376
|
||||
parboiled_rice_steam_cooking = 3380
|
||||
parboiled_rice_rapid_steam_cooking = 3381
|
||||
basmati_rice_steam_cooking = 3383
|
||||
basmati_rice_steam_cooking = 3382, 3383
|
||||
basmati_rice_rapid_steam_cooking = 3384
|
||||
jasmine_rice_steam_cooking = 3386
|
||||
jasmine_rice_rapid_steam_cooking = 3387
|
||||
@@ -1129,7 +1132,7 @@ class OvenProgramId(MieleEnum, missing_to_none=True):
|
||||
huanghuanian_rapid_steam_cooking = 3390
|
||||
simiao_steam_cooking = 3392
|
||||
simiao_rapid_steam_cooking = 3393
|
||||
long_grain_rice_general_steam_cooking = 3395
|
||||
long_grain_rice_general_steam_cooking = 3394, 3395
|
||||
long_grain_rice_general_rapid_steam_cooking = 3396
|
||||
chongming_steam_cooking = 3398
|
||||
chongming_rapid_steam_cooking = 3399
|
||||
|
||||
@@ -560,6 +560,7 @@
|
||||
"hot_water": "Hot water",
|
||||
"huanghuanian_rapid_steam_cooking": "Huanghuanian (rapid steam cooking)",
|
||||
"huanghuanian_steam_cooking": "Huanghuanian (steam cooking)",
|
||||
"hydroclean": "HydroClean",
|
||||
"hygiene": "Hygiene",
|
||||
"intensive": "Intensive",
|
||||
"intensive_bake": "Intensive bake",
|
||||
@@ -1006,6 +1007,7 @@
|
||||
"heating_up_phase": "Heating up phase",
|
||||
"hot_milk": "Hot milk",
|
||||
"hygiene": "Hygiene",
|
||||
"hygiene_drying": "Hygiene drying",
|
||||
"interim_rinse": "Interim rinse",
|
||||
"keep_warm": "Keep warm",
|
||||
"keeping_warm": "Keeping warm",
|
||||
|
||||
@@ -163,8 +163,6 @@ class MqttDeviceTracker(MqttEntity, TrackerEntity):
|
||||
latitude: float | None
|
||||
longitude: float | None
|
||||
gps_accuracy: float
|
||||
# Reset manually set location to allow automatic zone detection
|
||||
self._attr_location_name = None
|
||||
if isinstance(
|
||||
latitude := extra_state_attributes.get(ATTR_LATITUDE), (int, float)
|
||||
) and isinstance(
|
||||
|
||||
@@ -19,6 +19,5 @@ async def async_get_config_entry_diagnostics(
|
||||
return {
|
||||
"device_info": client.device_info,
|
||||
"vehicles": client.vehicles,
|
||||
"ct_connected": client.ct_connected,
|
||||
"cap_available": client.cap_available,
|
||||
}
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "cloud_polling",
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["ohme==1.6.0"]
|
||||
"requirements": ["ohme==1.7.0"]
|
||||
}
|
||||
|
||||
@@ -10,5 +10,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["onedrive_personal_sdk"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["onedrive-personal-sdk==0.1.5"]
|
||||
"requirements": ["onedrive-personal-sdk==0.1.7"]
|
||||
}
|
||||
|
||||
@@ -10,5 +10,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["onedrive_personal_sdk"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["onedrive-personal-sdk==0.1.5"]
|
||||
"requirements": ["onedrive-personal-sdk==0.1.7"]
|
||||
}
|
||||
|
||||
@@ -9,5 +9,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["opower"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["opower==0.17.0"]
|
||||
"requirements": ["opower==0.17.1"]
|
||||
}
|
||||
|
||||
@@ -13,5 +13,5 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["oralb_ble"],
|
||||
"requirements": ["oralb-ble==1.0.2"]
|
||||
"requirements": ["oralb-ble==1.1.0"]
|
||||
}
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/otbr",
|
||||
"integration_type": "service",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["python-otbr-api==2.8.0"]
|
||||
"requirements": ["python-otbr-api==2.9.0"]
|
||||
}
|
||||
|
||||
@@ -159,8 +159,12 @@ class PortainerConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
await self.async_set_unique_id(user_input[CONF_API_TOKEN])
|
||||
self._abort_if_unique_id_configured()
|
||||
# Logic that can be reverted back once the new unique ID is in
|
||||
existing_entry = await self.async_set_unique_id(
|
||||
user_input[CONF_API_TOKEN]
|
||||
)
|
||||
if existing_entry and existing_entry.entry_id != reconf_entry.entry_id:
|
||||
return self.async_abort(reason="already_configured")
|
||||
return self.async_update_reload_and_abort(
|
||||
reconf_entry,
|
||||
data_updates={
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["pyportainer==1.0.31"]
|
||||
"requirements": ["pyportainer==1.0.33"]
|
||||
}
|
||||
|
||||
@@ -104,7 +104,7 @@ VM_BUTTONS: tuple[ProxmoxVMButtonEntityDescription, ...] = (
|
||||
ProxmoxVMButtonEntityDescription(
|
||||
key="restart",
|
||||
press_action=lambda coordinator, node, vmid: (
|
||||
coordinator.proxmox.nodes(node).qemu(vmid).status.restart.post()
|
||||
coordinator.proxmox.nodes(node).qemu(vmid).status.reboot.post()
|
||||
),
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
device_class=ButtonDeviceClass.RESTART,
|
||||
@@ -147,7 +147,7 @@ CONTAINER_BUTTONS: tuple[ProxmoxContainerButtonEntityDescription, ...] = (
|
||||
ProxmoxContainerButtonEntityDescription(
|
||||
key="restart",
|
||||
press_action=lambda coordinator, node, vmid: (
|
||||
coordinator.proxmox.nodes(node).lxc(vmid).status.restart.post()
|
||||
coordinator.proxmox.nodes(node).lxc(vmid).status.reboot.post()
|
||||
),
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
device_class=ButtonDeviceClass.RESTART,
|
||||
@@ -277,7 +277,8 @@ class ProxmoxNodeButtonEntity(ProxmoxNodeEntity, ProxmoxBaseButton):
|
||||
|
||||
async def _async_press_call(self) -> None:
|
||||
"""Execute the node button action via executor."""
|
||||
if not is_granted(self.coordinator.permissions, p_type="nodes"):
|
||||
node_id = self._node_data.node["node"]
|
||||
if not is_granted(self.coordinator.permissions, p_type="nodes", p_id=node_id):
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="no_permission_node_power",
|
||||
@@ -285,7 +286,7 @@ class ProxmoxNodeButtonEntity(ProxmoxNodeEntity, ProxmoxBaseButton):
|
||||
await self.hass.async_add_executor_job(
|
||||
self.entity_description.press_action,
|
||||
self.coordinator,
|
||||
self._node_data.node["node"],
|
||||
node_id,
|
||||
)
|
||||
|
||||
|
||||
@@ -309,7 +310,8 @@ class ProxmoxVMButtonEntity(ProxmoxVMEntity, ProxmoxBaseButton):
|
||||
|
||||
async def _async_press_call(self) -> None:
|
||||
"""Execute the VM button action via executor."""
|
||||
if not is_granted(self.coordinator.permissions, p_type="vms"):
|
||||
vmid = self.vm_data["vmid"]
|
||||
if not is_granted(self.coordinator.permissions, p_type="vms", p_id=vmid):
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="no_permission_vm_lxc_power",
|
||||
@@ -318,7 +320,7 @@ class ProxmoxVMButtonEntity(ProxmoxVMEntity, ProxmoxBaseButton):
|
||||
self.entity_description.press_action,
|
||||
self.coordinator,
|
||||
self._node_name,
|
||||
self.vm_data["vmid"],
|
||||
vmid,
|
||||
)
|
||||
|
||||
|
||||
@@ -342,8 +344,9 @@ class ProxmoxContainerButtonEntity(ProxmoxContainerEntity, ProxmoxBaseButton):
|
||||
|
||||
async def _async_press_call(self) -> None:
|
||||
"""Execute the container button action via executor."""
|
||||
vmid = self.container_data["vmid"]
|
||||
# Container power actions fall under vms
|
||||
if not is_granted(self.coordinator.permissions, p_type="vms"):
|
||||
if not is_granted(self.coordinator.permissions, p_type="vms", p_id=vmid):
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="no_permission_vm_lxc_power",
|
||||
@@ -352,5 +355,5 @@ class ProxmoxContainerButtonEntity(ProxmoxContainerEntity, ProxmoxBaseButton):
|
||||
self.entity_description.press_action,
|
||||
self.coordinator,
|
||||
self._node_name,
|
||||
self.container_data["vmid"],
|
||||
vmid,
|
||||
)
|
||||
|
||||
@@ -6,8 +6,13 @@ from .const import PERM_POWER
|
||||
def is_granted(
|
||||
permissions: dict[str, dict[str, int]],
|
||||
p_type: str = "vms",
|
||||
p_id: str | int | None = None, # can be str for nodes
|
||||
permission: str = PERM_POWER,
|
||||
) -> bool:
|
||||
"""Validate user permissions for the given type and permission."""
|
||||
path = f"/{p_type}"
|
||||
return permissions.get(path, {}).get(permission) == 1
|
||||
paths = [f"/{p_type}/{p_id}", f"/{p_type}", "/"]
|
||||
for path in paths:
|
||||
value = permissions.get(path, {}).get(permission)
|
||||
if value is not None:
|
||||
return value == 1
|
||||
return False
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["ical"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["ical==13.2.0"]
|
||||
"requirements": ["ical==13.2.2"]
|
||||
}
|
||||
|
||||
@@ -34,5 +34,5 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["pysmartthings"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["pysmartthings==3.6.0"]
|
||||
"requirements": ["pysmartthings==3.7.2"]
|
||||
}
|
||||
|
||||
@@ -43,6 +43,10 @@ async def async_setup_entry(
|
||||
for component in device.status
|
||||
if component in ("cooler", "freezer", "onedoor")
|
||||
and Capability.THERMOSTAT_COOLING_SETPOINT in device.status[component]
|
||||
and device.status[component][Capability.THERMOSTAT_COOLING_SETPOINT][
|
||||
Attribute.COOLING_SETPOINT_RANGE
|
||||
].value
|
||||
is not None
|
||||
)
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Support for SLZB-06 buttons."""
|
||||
"""Support for SLZB buttons."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -35,24 +35,25 @@ class SmButtonDescription(ButtonEntityDescription):
|
||||
press_fn: Callable[[CmdWrapper, int], Awaitable[None]]
|
||||
|
||||
|
||||
BUTTONS: list[SmButtonDescription] = [
|
||||
SmButtonDescription(
|
||||
key="core_restart",
|
||||
translation_key="core_restart",
|
||||
device_class=ButtonDeviceClass.RESTART,
|
||||
press_fn=lambda cmd, idx: cmd.reboot(),
|
||||
),
|
||||
CORE_BUTTON = SmButtonDescription(
|
||||
key="core_restart",
|
||||
translation_key="core_restart",
|
||||
device_class=ButtonDeviceClass.RESTART,
|
||||
press_fn=lambda cmd, idx: cmd.reboot(),
|
||||
)
|
||||
|
||||
RADIO_BUTTONS: list[SmButtonDescription] = [
|
||||
SmButtonDescription(
|
||||
key="zigbee_restart",
|
||||
translation_key="zigbee_restart",
|
||||
device_class=ButtonDeviceClass.RESTART,
|
||||
press_fn=lambda cmd, idx: cmd.zb_restart(),
|
||||
press_fn=lambda cmd, idx: cmd.zb_restart(idx=idx),
|
||||
),
|
||||
SmButtonDescription(
|
||||
key="zigbee_flash_mode",
|
||||
translation_key="zigbee_flash_mode",
|
||||
entity_registry_enabled_default=False,
|
||||
press_fn=lambda cmd, idx: cmd.zb_bootloader(),
|
||||
press_fn=lambda cmd, idx: cmd.zb_bootloader(idx=idx),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -73,7 +74,13 @@ async def async_setup_entry(
|
||||
coordinator = entry.runtime_data.data
|
||||
radios = coordinator.data.info.radios
|
||||
|
||||
async_add_entities(SmButton(coordinator, button) for button in BUTTONS)
|
||||
entities = [SmButton(coordinator, CORE_BUTTON)]
|
||||
count = len(radios) if coordinator.data.info.u_device else 1
|
||||
|
||||
for idx in range(count):
|
||||
entities.extend(SmButton(coordinator, button, idx) for button in RADIO_BUTTONS)
|
||||
|
||||
async_add_entities(entities)
|
||||
entity_created = [False] * len(radios)
|
||||
|
||||
@callback
|
||||
@@ -103,7 +110,7 @@ async def async_setup_entry(
|
||||
|
||||
|
||||
class SmButton(SmEntity, ButtonEntity):
|
||||
"""Defines a SLZB-06 button."""
|
||||
"""Defines a SLZB button."""
|
||||
|
||||
coordinator: SmDataUpdateCoordinator
|
||||
entity_description: SmButtonDescription
|
||||
@@ -115,7 +122,7 @@ class SmButton(SmEntity, ButtonEntity):
|
||||
description: SmButtonDescription,
|
||||
idx: int = 0,
|
||||
) -> None:
|
||||
"""Initialize SLZB-06 button entity."""
|
||||
"""Initialize SLZB button entity."""
|
||||
super().__init__(coordinator)
|
||||
|
||||
self.entity_description = description
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["pysmlight==0.2.16"],
|
||||
"requirements": ["pysmlight==0.3.1"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"type": "_slzb-06._tcp.local."
|
||||
|
||||
@@ -290,16 +290,29 @@ class SnapcastClientDevice(SnapcastCoordinatorEntity, MediaPlayerEntity):
|
||||
and entity.unique_id != self.unique_id
|
||||
]
|
||||
|
||||
# Get unique ID prefix for this host
|
||||
unique_id_prefix = self.get_unique_id(self.coordinator.host_id, "")
|
||||
for client in clients:
|
||||
# Valid entity is a snapcast client
|
||||
# Validate entity is a snapcast client
|
||||
if not client.unique_id.startswith(CLIENT_PREFIX):
|
||||
raise ServiceValidationError(
|
||||
f"Entity '{client.entity_id}' is not a Snapcast client device."
|
||||
)
|
||||
|
||||
# Validate client belongs to the same server
|
||||
if not client.unique_id.startswith(unique_id_prefix):
|
||||
raise ServiceValidationError(
|
||||
f"Entity '{client.entity_id}' does not belong to the same Snapcast server."
|
||||
)
|
||||
|
||||
# Extract client ID and join it to the current group
|
||||
identifier = client.unique_id.split("_")[-1]
|
||||
await self._current_group.add_client(identifier)
|
||||
identifier = client.unique_id.removeprefix(unique_id_prefix)
|
||||
try:
|
||||
await self._current_group.add_client(identifier)
|
||||
except KeyError as e:
|
||||
raise ServiceValidationError(
|
||||
f"Client with identifier '{identifier}' does not exist on the server."
|
||||
) from e
|
||||
|
||||
self.async_write_ha_state()
|
||||
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/starlink",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["starlink-grpc-core==1.2.3"]
|
||||
"requirements": ["starlink-grpc-core==1.2.4"]
|
||||
}
|
||||
|
||||
@@ -9,5 +9,5 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["aiotedee"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["aiotedee==0.2.25"]
|
||||
"requirements": ["aiotedee==0.2.27"]
|
||||
}
|
||||
|
||||
@@ -80,6 +80,16 @@ LEGACY_FIELDS = {
|
||||
CONF_VALUE_TEMPLATE: CONF_STATE,
|
||||
}
|
||||
|
||||
SCRIPT_FIELDS = (
|
||||
CONF_ARM_AWAY_ACTION,
|
||||
CONF_ARM_CUSTOM_BYPASS_ACTION,
|
||||
CONF_ARM_HOME_ACTION,
|
||||
CONF_ARM_NIGHT_ACTION,
|
||||
CONF_ARM_VACATION_ACTION,
|
||||
CONF_DISARM_ACTION,
|
||||
CONF_TRIGGER_ACTION,
|
||||
)
|
||||
|
||||
DEFAULT_NAME = "Template Alarm Control Panel"
|
||||
|
||||
ALARM_CONTROL_PANEL_COMMON_SCHEMA = vol.Schema(
|
||||
@@ -152,6 +162,7 @@ async def async_setup_entry(
|
||||
StateAlarmControlPanelEntity,
|
||||
ALARM_CONTROL_PANEL_CONFIG_ENTRY_SCHEMA,
|
||||
True,
|
||||
script_options=SCRIPT_FIELDS,
|
||||
)
|
||||
|
||||
|
||||
@@ -172,6 +183,7 @@ async def async_setup_platform(
|
||||
discovery_info,
|
||||
LEGACY_FIELDS,
|
||||
legacy_key=CONF_ALARM_CONTROL_PANELS,
|
||||
script_options=SCRIPT_FIELDS,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -36,6 +36,8 @@ _LOGGER = logging.getLogger(__name__)
|
||||
DEFAULT_NAME = "Template Button"
|
||||
DEFAULT_OPTIMISTIC = False
|
||||
|
||||
SCRIPT_FIELDS = (CONF_PRESS,)
|
||||
|
||||
BUTTON_YAML_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_PRESS): cv.SCRIPT_SCHEMA,
|
||||
@@ -66,6 +68,7 @@ async def async_setup_platform(
|
||||
None,
|
||||
async_add_entities,
|
||||
discovery_info,
|
||||
script_options=SCRIPT_FIELDS,
|
||||
)
|
||||
|
||||
|
||||
@@ -81,6 +84,7 @@ async def async_setup_entry(
|
||||
async_add_entities,
|
||||
StateButtonEntity,
|
||||
BUTTON_CONFIG_ENTRY_SCHEMA,
|
||||
script_options=SCRIPT_FIELDS,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -71,6 +71,14 @@ CONF_TILT_OPTIMISTIC = "tilt_optimistic"
|
||||
|
||||
CONF_OPEN_AND_CLOSE = "open_or_close"
|
||||
|
||||
SCRIPT_FIELDS = (
|
||||
CLOSE_ACTION,
|
||||
OPEN_ACTION,
|
||||
POSITION_ACTION,
|
||||
STOP_ACTION,
|
||||
TILT_ACTION,
|
||||
)
|
||||
|
||||
TILT_FEATURES = (
|
||||
CoverEntityFeature.OPEN_TILT
|
||||
| CoverEntityFeature.CLOSE_TILT
|
||||
@@ -165,6 +173,7 @@ async def async_setup_platform(
|
||||
discovery_info,
|
||||
LEGACY_FIELDS,
|
||||
legacy_key=CONF_COVERS,
|
||||
script_options=SCRIPT_FIELDS,
|
||||
)
|
||||
|
||||
|
||||
@@ -181,6 +190,7 @@ async def async_setup_entry(
|
||||
StateCoverEntity,
|
||||
COVER_CONFIG_ENTRY_SCHEMA,
|
||||
True,
|
||||
script_options=SCRIPT_FIELDS,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -87,6 +87,15 @@ LEGACY_FIELDS = {
|
||||
|
||||
DEFAULT_NAME = "Template Fan"
|
||||
|
||||
SCRIPT_FIELDS = (
|
||||
CONF_OFF_ACTION,
|
||||
CONF_ON_ACTION,
|
||||
CONF_SET_DIRECTION_ACTION,
|
||||
CONF_SET_OSCILLATING_ACTION,
|
||||
CONF_SET_PERCENTAGE_ACTION,
|
||||
CONF_SET_PRESET_MODE_ACTION,
|
||||
)
|
||||
|
||||
FAN_COMMON_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_DIRECTION): cv.template,
|
||||
@@ -159,6 +168,7 @@ async def async_setup_platform(
|
||||
discovery_info,
|
||||
LEGACY_FIELDS,
|
||||
legacy_key=CONF_FANS,
|
||||
script_options=SCRIPT_FIELDS,
|
||||
)
|
||||
|
||||
|
||||
@@ -174,6 +184,7 @@ async def async_setup_entry(
|
||||
async_add_entities,
|
||||
StateFanEntity,
|
||||
FAN_CONFIG_ENTRY_SCHEMA,
|
||||
script_options=SCRIPT_FIELDS,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import logging
|
||||
from typing import Any
|
||||
|
||||
import voluptuous as vol
|
||||
from voluptuous.humanize import humanize_error
|
||||
|
||||
from homeassistant.components import blueprint
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
@@ -25,7 +26,7 @@ from homeassistant.const import (
|
||||
SERVICE_RELOAD,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import PlatformNotReady
|
||||
from homeassistant.exceptions import HomeAssistantError, PlatformNotReady
|
||||
from homeassistant.helpers import issue_registry as ir, template
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.entity_platform import (
|
||||
@@ -34,6 +35,7 @@ from homeassistant.helpers.entity_platform import (
|
||||
async_get_platforms,
|
||||
)
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity
|
||||
from homeassistant.helpers.script import async_validate_actions_config
|
||||
from homeassistant.helpers.script_variables import ScriptVariables
|
||||
from homeassistant.helpers.singleton import singleton
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
@@ -208,6 +210,21 @@ def _format_template(value: Any, field: str | None = None) -> Any:
|
||||
return str(value)
|
||||
|
||||
|
||||
def _get_config_breadcrumbs(config: ConfigType) -> str:
|
||||
"""Try to coerce entity information from the config."""
|
||||
breadcrumb = "Template Entity"
|
||||
# Default entity id should be in most legacy configuration because
|
||||
# it's created from the legacy slug. Vacuum and Lock do not have a
|
||||
# slug, therefore we need to use the name or unique_id.
|
||||
if (default_entity_id := config.get(CONF_DEFAULT_ENTITY_ID)) is not None:
|
||||
breadcrumb = default_entity_id.split(".")[-1]
|
||||
elif (unique_id := config.get(CONF_UNIQUE_ID)) is not None:
|
||||
breadcrumb = f"unique_id: {unique_id}"
|
||||
elif (name := config.get(CONF_NAME)) and isinstance(name, template.Template):
|
||||
breadcrumb = name.template
|
||||
return breadcrumb
|
||||
|
||||
|
||||
def format_migration_config(
|
||||
config: ConfigType | list[ConfigType], depth: int = 0
|
||||
) -> ConfigType | list[ConfigType]:
|
||||
@@ -252,16 +269,7 @@ def create_legacy_template_issue(
|
||||
if domain not in PLATFORMS:
|
||||
return
|
||||
|
||||
breadcrumb = "Template Entity"
|
||||
# Default entity id should be in most legacy configuration because
|
||||
# it's created from the legacy slug. Vacuum and Lock do not have a
|
||||
# slug, therefore we need to use the name or unique_id.
|
||||
if (default_entity_id := config.get(CONF_DEFAULT_ENTITY_ID)) is not None:
|
||||
breadcrumb = default_entity_id.split(".")[-1]
|
||||
elif (unique_id := config.get(CONF_UNIQUE_ID)) is not None:
|
||||
breadcrumb = f"unique_id: {unique_id}"
|
||||
elif (name := config.get(CONF_NAME)) and isinstance(name, template.Template):
|
||||
breadcrumb = name.template
|
||||
breadcrumb = _get_config_breadcrumbs(config)
|
||||
|
||||
issue_id = f"{LEGACY_TEMPLATE_DEPRECATION_KEY}_{domain}_{breadcrumb}_{hashlib.md5(','.join(config.keys()).encode()).hexdigest()}"
|
||||
|
||||
@@ -296,6 +304,39 @@ def create_legacy_template_issue(
|
||||
)
|
||||
|
||||
|
||||
async def validate_template_scripts(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
script_options: tuple[str, ...] | None = None,
|
||||
) -> None:
|
||||
"""Validate template scripts."""
|
||||
if not script_options:
|
||||
return
|
||||
|
||||
def _humanize(err: Exception, data: Any) -> str:
|
||||
"""Humanize vol.Invalid, stringify other exceptions."""
|
||||
if isinstance(err, vol.Invalid):
|
||||
return humanize_error(data, err)
|
||||
return str(err)
|
||||
|
||||
breadcrumb: str | None = None
|
||||
for script_option in script_options:
|
||||
if (script_config := config.pop(script_option, None)) is not None:
|
||||
try:
|
||||
config[script_option] = await async_validate_actions_config(
|
||||
hass, script_config
|
||||
)
|
||||
except (vol.Invalid, HomeAssistantError) as err:
|
||||
if not breadcrumb:
|
||||
breadcrumb = _get_config_breadcrumbs(config)
|
||||
_LOGGER.error(
|
||||
"The '%s' actions for %s failed to setup: %s",
|
||||
script_option,
|
||||
breadcrumb,
|
||||
_humanize(err, script_config),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_template_platform(
|
||||
hass: HomeAssistant,
|
||||
domain: str,
|
||||
@@ -306,6 +347,7 @@ async def async_setup_template_platform(
|
||||
discovery_info: DiscoveryInfoType | None,
|
||||
legacy_fields: dict[str, str] | None = None,
|
||||
legacy_key: str | None = None,
|
||||
script_options: tuple[str, ...] | None = None,
|
||||
) -> None:
|
||||
"""Set up the Template platform."""
|
||||
if discovery_info is None:
|
||||
@@ -337,10 +379,14 @@ async def async_setup_template_platform(
|
||||
# Trigger Configuration
|
||||
if "coordinator" in discovery_info:
|
||||
if trigger_entity_cls:
|
||||
entities = [
|
||||
trigger_entity_cls(hass, discovery_info["coordinator"], config)
|
||||
for config in discovery_info["entities"]
|
||||
]
|
||||
entities = []
|
||||
for entity_config in discovery_info["entities"]:
|
||||
await validate_template_scripts(hass, entity_config, script_options)
|
||||
entities.append(
|
||||
trigger_entity_cls(
|
||||
hass, discovery_info["coordinator"], entity_config
|
||||
)
|
||||
)
|
||||
async_add_entities(entities)
|
||||
else:
|
||||
raise PlatformNotReady(
|
||||
@@ -349,6 +395,9 @@ async def async_setup_template_platform(
|
||||
return
|
||||
|
||||
# Modern Configuration
|
||||
for entity_config in discovery_info["entities"]:
|
||||
await validate_template_scripts(hass, entity_config, script_options)
|
||||
|
||||
async_create_template_tracking_entities(
|
||||
state_entity_cls,
|
||||
async_add_entities,
|
||||
@@ -365,6 +414,7 @@ async def async_setup_template_entry(
|
||||
state_entity_cls: type[TemplateEntity],
|
||||
config_schema: vol.Schema | vol.All,
|
||||
replace_value_template: bool = False,
|
||||
script_options: tuple[str, ...] | None = None,
|
||||
) -> None:
|
||||
"""Setup the Template from a config entry."""
|
||||
options = dict(config_entry.options)
|
||||
@@ -377,6 +427,7 @@ async def async_setup_template_entry(
|
||||
options[CONF_STATE] = options.pop(CONF_VALUE_TEMPLATE)
|
||||
|
||||
validated_config = config_schema(options)
|
||||
await validate_template_scripts(hass, validated_config, script_options)
|
||||
|
||||
async_add_entities(
|
||||
[state_entity_cls(hass, validated_config, config_entry.entry_id)]
|
||||
|
||||
@@ -129,6 +129,18 @@ LEGACY_FIELDS = {
|
||||
|
||||
DEFAULT_NAME = "Template Light"
|
||||
|
||||
SCRIPT_FIELDS = (
|
||||
CONF_EFFECT_ACTION,
|
||||
CONF_HS_ACTION,
|
||||
CONF_LEVEL_ACTION,
|
||||
CONF_OFF_ACTION,
|
||||
CONF_ON_ACTION,
|
||||
CONF_RGB_ACTION,
|
||||
CONF_RGBW_ACTION,
|
||||
CONF_RGBWW_ACTION,
|
||||
CONF_TEMPERATURE_ACTION,
|
||||
)
|
||||
|
||||
LIGHT_COMMON_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Inclusive(CONF_EFFECT_ACTION, "effect"): cv.SCRIPT_SCHEMA,
|
||||
@@ -142,8 +154,6 @@ LIGHT_COMMON_SCHEMA = vol.Schema(
|
||||
vol.Optional(CONF_MIN_MIREDS): cv.template,
|
||||
vol.Required(CONF_OFF_ACTION): cv.SCRIPT_SCHEMA,
|
||||
vol.Required(CONF_ON_ACTION): cv.SCRIPT_SCHEMA,
|
||||
vol.Required(CONF_OFF_ACTION): cv.SCRIPT_SCHEMA,
|
||||
vol.Required(CONF_ON_ACTION): cv.SCRIPT_SCHEMA,
|
||||
vol.Optional(CONF_RGB_ACTION): cv.SCRIPT_SCHEMA,
|
||||
vol.Optional(CONF_RGB): cv.template,
|
||||
vol.Optional(CONF_RGBW_ACTION): cv.SCRIPT_SCHEMA,
|
||||
@@ -226,6 +236,7 @@ async def async_setup_platform(
|
||||
discovery_info,
|
||||
LEGACY_FIELDS,
|
||||
legacy_key=CONF_LIGHTS,
|
||||
script_options=SCRIPT_FIELDS,
|
||||
)
|
||||
|
||||
|
||||
@@ -242,6 +253,7 @@ async def async_setup_entry(
|
||||
StateLightEntity,
|
||||
LIGHT_CONFIG_ENTRY_SCHEMA,
|
||||
True,
|
||||
script_options=SCRIPT_FIELDS,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -64,6 +64,13 @@ LEGACY_FIELDS = {
|
||||
CONF_VALUE_TEMPLATE: CONF_STATE,
|
||||
}
|
||||
|
||||
SCRIPT_FIELDS = (
|
||||
CONF_LOCK,
|
||||
CONF_OPEN,
|
||||
CONF_UNLOCK,
|
||||
)
|
||||
|
||||
|
||||
LOCK_COMMON_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_CODE_FORMAT): cv.template,
|
||||
@@ -112,6 +119,7 @@ async def async_setup_platform(
|
||||
async_add_entities,
|
||||
discovery_info,
|
||||
LEGACY_FIELDS,
|
||||
script_options=SCRIPT_FIELDS,
|
||||
)
|
||||
|
||||
|
||||
@@ -127,6 +135,7 @@ async def async_setup_entry(
|
||||
async_add_entities,
|
||||
StateLockEntity,
|
||||
LOCK_CONFIG_ENTRY_SCHEMA,
|
||||
script_options=SCRIPT_FIELDS,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -46,6 +46,8 @@ CONF_SET_VALUE = "set_value"
|
||||
DEFAULT_NAME = "Template Number"
|
||||
DEFAULT_OPTIMISTIC = False
|
||||
|
||||
SCRIPT_FIELDS = (CONF_SET_VALUE,)
|
||||
|
||||
NUMBER_COMMON_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_MAX, default=DEFAULT_MAX_VALUE): cv.template,
|
||||
@@ -81,6 +83,7 @@ async def async_setup_platform(
|
||||
TriggerNumberEntity,
|
||||
async_add_entities,
|
||||
discovery_info,
|
||||
script_options=SCRIPT_FIELDS,
|
||||
)
|
||||
|
||||
|
||||
@@ -96,6 +99,7 @@ async def async_setup_entry(
|
||||
async_add_entities,
|
||||
StateNumberEntity,
|
||||
NUMBER_CONFIG_ENTRY_SCHEMA,
|
||||
script_options=SCRIPT_FIELDS,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -47,6 +47,8 @@ CONF_SELECT_OPTION = "select_option"
|
||||
|
||||
DEFAULT_NAME = "Template Select"
|
||||
|
||||
SCRIPT_FIELDS = (CONF_SELECT_OPTION,)
|
||||
|
||||
SELECT_COMMON_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_OPTIONS): cv.template,
|
||||
@@ -79,6 +81,7 @@ async def async_setup_platform(
|
||||
TriggerSelectEntity,
|
||||
async_add_entities,
|
||||
discovery_info,
|
||||
script_options=SCRIPT_FIELDS,
|
||||
)
|
||||
|
||||
|
||||
@@ -94,6 +97,7 @@ async def async_setup_entry(
|
||||
async_add_entities,
|
||||
TemplateSelect,
|
||||
SELECT_CONFIG_ENTRY_SCHEMA,
|
||||
script_options=SCRIPT_FIELDS,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -57,11 +57,16 @@ LEGACY_FIELDS = {
|
||||
|
||||
DEFAULT_NAME = "Template Switch"
|
||||
|
||||
SCRIPT_FIELDS = (
|
||||
CONF_TURN_OFF,
|
||||
CONF_TURN_ON,
|
||||
)
|
||||
|
||||
SWITCH_COMMON_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_STATE): cv.template,
|
||||
vol.Optional(CONF_TURN_ON): cv.SCRIPT_SCHEMA,
|
||||
vol.Optional(CONF_TURN_OFF): cv.SCRIPT_SCHEMA,
|
||||
vol.Optional(CONF_TURN_ON): cv.SCRIPT_SCHEMA,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -109,6 +114,7 @@ async def async_setup_platform(
|
||||
discovery_info,
|
||||
LEGACY_FIELDS,
|
||||
legacy_key=CONF_SWITCHES,
|
||||
script_options=SCRIPT_FIELDS,
|
||||
)
|
||||
|
||||
|
||||
@@ -125,6 +131,7 @@ async def async_setup_entry(
|
||||
StateSwitchEntity,
|
||||
SWITCH_CONFIG_ENTRY_SCHEMA,
|
||||
True,
|
||||
script_options=SCRIPT_FIELDS,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -65,6 +65,8 @@ CONF_SPECIFIC_VERSION = "specific_version"
|
||||
CONF_TITLE = "title"
|
||||
CONF_UPDATE_PERCENTAGE = "update_percentage"
|
||||
|
||||
SCRIPT_FIELDS = (CONF_INSTALL,)
|
||||
|
||||
UPDATE_COMMON_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_BACKUP, default=False): cv.boolean,
|
||||
@@ -105,6 +107,7 @@ async def async_setup_platform(
|
||||
TriggerUpdateEntity,
|
||||
async_add_entities,
|
||||
discovery_info,
|
||||
script_options=SCRIPT_FIELDS,
|
||||
)
|
||||
|
||||
|
||||
@@ -120,6 +123,7 @@ async def async_setup_entry(
|
||||
async_add_entities,
|
||||
StateUpdateEntity,
|
||||
UPDATE_CONFIG_ENTRY_SCHEMA,
|
||||
script_options=SCRIPT_FIELDS,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -76,6 +76,16 @@ LEGACY_FIELDS = {
|
||||
CONF_VALUE_TEMPLATE: CONF_STATE,
|
||||
}
|
||||
|
||||
SCRIPT_FIELDS = (
|
||||
SERVICE_CLEAN_SPOT,
|
||||
SERVICE_LOCATE,
|
||||
SERVICE_PAUSE,
|
||||
SERVICE_RETURN_TO_BASE,
|
||||
SERVICE_SET_FAN_SPEED,
|
||||
SERVICE_START,
|
||||
SERVICE_STOP,
|
||||
)
|
||||
|
||||
VACUUM_COMMON_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_BATTERY_LEVEL): cv.template,
|
||||
@@ -150,6 +160,7 @@ async def async_setup_platform(
|
||||
discovery_info,
|
||||
LEGACY_FIELDS,
|
||||
legacy_key=CONF_VACUUMS,
|
||||
script_options=SCRIPT_FIELDS,
|
||||
)
|
||||
|
||||
|
||||
@@ -165,6 +176,7 @@ async def async_setup_entry(
|
||||
async_add_entities,
|
||||
TemplateStateVacuumEntity,
|
||||
VACUUM_CONFIG_ENTRY_SCHEMA,
|
||||
script_options=SCRIPT_FIELDS,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/thread",
|
||||
"integration_type": "service",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["python-otbr-api==2.8.0", "pyroute2==0.7.5"],
|
||||
"requirements": ["python-otbr-api==2.9.0", "pyroute2==0.7.5"],
|
||||
"single_config_entry": true,
|
||||
"zeroconf": ["_meshcop._udp.local."]
|
||||
}
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
import asyncio
|
||||
from datetime import datetime, timedelta
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, cast
|
||||
from typing import TYPE_CHECKING, TypedDict, cast
|
||||
|
||||
from aiohttp.client_exceptions import ClientError
|
||||
import tibber
|
||||
@@ -38,6 +39,58 @@ FIVE_YEARS = 5 * 365 * 24
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TibberHomeData(TypedDict):
|
||||
"""Data for a Tibber home used by the price sensor."""
|
||||
|
||||
currency: str
|
||||
price_unit: str
|
||||
current_price: float | None
|
||||
current_price_time: datetime | None
|
||||
intraday_price_ranking: float | None
|
||||
max_price: float
|
||||
avg_price: float
|
||||
min_price: float
|
||||
off_peak_1: float
|
||||
peak: float
|
||||
off_peak_2: float
|
||||
month_cost: float | None
|
||||
peak_hour: float | None
|
||||
peak_hour_time: datetime | None
|
||||
month_cons: float | None
|
||||
app_nickname: str | None
|
||||
grid_company: str | None
|
||||
estimated_annual_consumption: int | None
|
||||
|
||||
|
||||
def _build_home_data(home: tibber.TibberHome) -> TibberHomeData:
|
||||
"""Build TibberHomeData from a TibberHome for the price sensor."""
|
||||
current_price, last_updated, price_rank = home.current_price_data()
|
||||
attributes = home.current_attributes()
|
||||
result: TibberHomeData = {
|
||||
"currency": home.currency,
|
||||
"price_unit": home.price_unit,
|
||||
"current_price": current_price,
|
||||
"current_price_time": last_updated,
|
||||
"intraday_price_ranking": price_rank,
|
||||
"max_price": attributes["max_price"],
|
||||
"avg_price": attributes["avg_price"],
|
||||
"min_price": attributes["min_price"],
|
||||
"off_peak_1": attributes["off_peak_1"],
|
||||
"peak": attributes["peak"],
|
||||
"off_peak_2": attributes["off_peak_2"],
|
||||
"month_cost": home.month_cost,
|
||||
"peak_hour": home.peak_hour,
|
||||
"peak_hour_time": home.peak_hour_time,
|
||||
"month_cons": home.month_cons,
|
||||
"app_nickname": home.info["viewer"]["home"].get("appNickname"),
|
||||
"grid_company": home.info["viewer"]["home"]["meteringPointData"]["gridCompany"],
|
||||
"estimated_annual_consumption": home.info["viewer"]["home"][
|
||||
"meteringPointData"
|
||||
]["estimatedAnnualConsumption"],
|
||||
}
|
||||
return result
|
||||
|
||||
|
||||
class TibberDataCoordinator(DataUpdateCoordinator[None]):
|
||||
"""Handle Tibber data and insert statistics."""
|
||||
|
||||
@@ -57,13 +110,16 @@ class TibberDataCoordinator(DataUpdateCoordinator[None]):
|
||||
name=f"Tibber {tibber_connection.name}",
|
||||
update_interval=timedelta(minutes=20),
|
||||
)
|
||||
self._tibber_connection = tibber_connection
|
||||
|
||||
async def _async_update_data(self) -> None:
|
||||
"""Update data via API."""
|
||||
tibber_connection = await self.config_entry.runtime_data.async_get_client(
|
||||
self.hass
|
||||
)
|
||||
|
||||
try:
|
||||
await self._tibber_connection.fetch_consumption_data_active_homes()
|
||||
await self._tibber_connection.fetch_production_data_active_homes()
|
||||
await tibber_connection.fetch_consumption_data_active_homes()
|
||||
await tibber_connection.fetch_production_data_active_homes()
|
||||
await self._insert_statistics()
|
||||
except tibber.RetryableHttpExceptionError as err:
|
||||
raise UpdateFailed(f"Error communicating with API ({err.status})") from err
|
||||
@@ -75,7 +131,10 @@ class TibberDataCoordinator(DataUpdateCoordinator[None]):
|
||||
|
||||
async def _insert_statistics(self) -> None:
|
||||
"""Insert Tibber statistics."""
|
||||
for home in self._tibber_connection.get_homes():
|
||||
tibber_connection = await self.config_entry.runtime_data.async_get_client(
|
||||
self.hass
|
||||
)
|
||||
for home in tibber_connection.get_homes():
|
||||
sensors: list[tuple[str, bool, str | None, str]] = []
|
||||
if home.hourly_consumption_data:
|
||||
sensors.append(
|
||||
@@ -194,6 +253,76 @@ class TibberDataCoordinator(DataUpdateCoordinator[None]):
|
||||
async_add_external_statistics(self.hass, metadata, statistics)
|
||||
|
||||
|
||||
class TibberPriceCoordinator(DataUpdateCoordinator[dict[str, TibberHomeData]]):
|
||||
"""Handle Tibber price data and insert statistics."""
|
||||
|
||||
config_entry: TibberConfigEntry
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
config_entry: TibberConfigEntry,
|
||||
) -> None:
|
||||
"""Initialize the price coordinator."""
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
config_entry=config_entry,
|
||||
name=f"{DOMAIN} price",
|
||||
update_interval=timedelta(minutes=1),
|
||||
)
|
||||
|
||||
def _seconds_until_next_15_minute(self) -> float:
|
||||
"""Return seconds until the next 15-minute boundary (0, 15, 30, 45) in UTC."""
|
||||
now = dt_util.utcnow()
|
||||
next_minute = ((now.minute // 15) + 1) * 15
|
||||
if next_minute >= 60:
|
||||
next_run = now.replace(minute=0, second=0, microsecond=0) + timedelta(
|
||||
hours=1
|
||||
)
|
||||
else:
|
||||
next_run = now.replace(
|
||||
minute=next_minute, second=0, microsecond=0, tzinfo=dt_util.UTC
|
||||
)
|
||||
return (next_run - now).total_seconds()
|
||||
|
||||
async def _async_update_data(self) -> dict[str, TibberHomeData]:
|
||||
"""Update data via API and return per-home data for sensors."""
|
||||
tibber_connection = await self.config_entry.runtime_data.async_get_client(
|
||||
self.hass
|
||||
)
|
||||
active_homes = tibber_connection.get_homes(only_active=True)
|
||||
try:
|
||||
await asyncio.gather(
|
||||
tibber_connection.fetch_consumption_data_active_homes(),
|
||||
tibber_connection.fetch_production_data_active_homes(),
|
||||
)
|
||||
|
||||
now = dt_util.now()
|
||||
homes_to_update = [
|
||||
home
|
||||
for home in active_homes
|
||||
if (
|
||||
(last_data_timestamp := home.last_data_timestamp) is None
|
||||
or (last_data_timestamp - now).total_seconds() < 11 * 3600
|
||||
)
|
||||
]
|
||||
|
||||
if homes_to_update:
|
||||
await asyncio.gather(
|
||||
*(home.update_info_and_price_info() for home in homes_to_update)
|
||||
)
|
||||
except tibber.RetryableHttpExceptionError as err:
|
||||
raise UpdateFailed(f"Error communicating with API ({err.status})") from err
|
||||
except tibber.FatalHttpExceptionError as err:
|
||||
raise UpdateFailed(f"Error communicating with API ({err.status})") from err
|
||||
|
||||
result = {home.home_id: _build_home_data(home) for home in active_homes}
|
||||
|
||||
self.update_interval = timedelta(seconds=self._seconds_until_next_15_minute())
|
||||
return result
|
||||
|
||||
|
||||
class TibberDataAPICoordinator(DataUpdateCoordinator[dict[str, TibberDevice]]):
|
||||
"""Fetch and cache Tibber Data API device capabilities."""
|
||||
|
||||
|
||||
@@ -3,10 +3,8 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
import datetime
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from random import randrange
|
||||
from typing import Any
|
||||
|
||||
import aiohttp
|
||||
@@ -42,18 +40,20 @@ from homeassistant.helpers.update_coordinator import (
|
||||
CoordinatorEntity,
|
||||
DataUpdateCoordinator,
|
||||
)
|
||||
from homeassistant.util import Throttle, dt as dt_util
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .const import DOMAIN, MANUFACTURER, TibberConfigEntry
|
||||
from .coordinator import TibberDataAPICoordinator, TibberDataCoordinator
|
||||
from .coordinator import (
|
||||
TibberDataAPICoordinator,
|
||||
TibberDataCoordinator,
|
||||
TibberPriceCoordinator,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ICON = "mdi:currency-usd"
|
||||
SCAN_INTERVAL = timedelta(minutes=1)
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5)
|
||||
PARALLEL_UPDATES = 0
|
||||
TWENTY_MINUTES = 20 * 60
|
||||
|
||||
RT_SENSORS_UNIQUE_ID_MIGRATION = {
|
||||
"accumulated_consumption_last_hour": "accumulated consumption current hour",
|
||||
@@ -610,6 +610,7 @@ async def _async_setup_graphql_sensors(
|
||||
entity_registry = er.async_get(hass)
|
||||
|
||||
coordinator: TibberDataCoordinator | None = None
|
||||
price_coordinator: TibberPriceCoordinator | None = None
|
||||
entities: list[TibberSensor] = []
|
||||
for home in tibber_connection.get_homes(only_active=False):
|
||||
try:
|
||||
@@ -626,7 +627,9 @@ async def _async_setup_graphql_sensors(
|
||||
raise PlatformNotReady from err
|
||||
|
||||
if home.has_active_subscription:
|
||||
entities.append(TibberSensorElPrice(home))
|
||||
if price_coordinator is None:
|
||||
price_coordinator = TibberPriceCoordinator(hass, entry)
|
||||
entities.append(TibberSensorElPrice(price_coordinator, home))
|
||||
if coordinator is None:
|
||||
coordinator = TibberDataCoordinator(hass, entry, tibber_connection)
|
||||
entities.extend(
|
||||
@@ -737,19 +740,21 @@ class TibberSensor(SensorEntity):
|
||||
return device_info
|
||||
|
||||
|
||||
class TibberSensorElPrice(TibberSensor):
|
||||
class TibberSensorElPrice(TibberSensor, CoordinatorEntity[TibberPriceCoordinator]):
|
||||
"""Representation of a Tibber sensor for el price."""
|
||||
|
||||
_attr_state_class = SensorStateClass.MEASUREMENT
|
||||
_attr_translation_key = "electricity_price"
|
||||
|
||||
def __init__(self, tibber_home: TibberHome) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: TibberPriceCoordinator,
|
||||
tibber_home: TibberHome,
|
||||
) -> None:
|
||||
"""Initialize the sensor."""
|
||||
super().__init__(tibber_home=tibber_home)
|
||||
self._last_updated: datetime.datetime | None = None
|
||||
self._spread_load_constant = randrange(TWENTY_MINUTES)
|
||||
|
||||
super().__init__(coordinator=coordinator, tibber_home=tibber_home)
|
||||
self._attr_available = False
|
||||
self._attr_native_unit_of_measurement = tibber_home.price_unit
|
||||
self._attr_extra_state_attributes = {
|
||||
"app_nickname": None,
|
||||
"grid_company": None,
|
||||
@@ -768,51 +773,38 @@ class TibberSensorElPrice(TibberSensor):
|
||||
|
||||
self._device_name = self._home_name
|
||||
|
||||
async def async_update(self) -> None:
|
||||
"""Get the latest data and updates the states."""
|
||||
now = dt_util.now()
|
||||
if (
|
||||
not self._tibber_home.last_data_timestamp
|
||||
or (self._tibber_home.last_data_timestamp - now).total_seconds()
|
||||
< 10 * 3600 - self._spread_load_constant
|
||||
or not self.available
|
||||
):
|
||||
_LOGGER.debug("Asking for new data")
|
||||
await self._fetch_data()
|
||||
|
||||
elif (
|
||||
self._tibber_home.price_total
|
||||
and self._last_updated
|
||||
and self._last_updated.hour == now.hour
|
||||
and now - self._last_updated < timedelta(minutes=15)
|
||||
and self._tibber_home.last_data_timestamp
|
||||
@callback
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Handle updated data from the coordinator."""
|
||||
data = self.coordinator.data
|
||||
if not data or (
|
||||
(home_data := data.get(self._tibber_home.home_id)) is None
|
||||
or (current_price := home_data.get("current_price")) is None
|
||||
):
|
||||
self._attr_available = False
|
||||
self.async_write_ha_state()
|
||||
return
|
||||
|
||||
res = self._tibber_home.current_price_data()
|
||||
self._attr_native_value, self._last_updated, price_rank = res
|
||||
self._attr_extra_state_attributes["intraday_price_ranking"] = price_rank
|
||||
|
||||
attrs = self._tibber_home.current_attributes()
|
||||
self._attr_extra_state_attributes.update(attrs)
|
||||
self._attr_available = self._attr_native_value is not None
|
||||
self._attr_native_unit_of_measurement = self._tibber_home.price_unit
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
async def _fetch_data(self) -> None:
|
||||
_LOGGER.debug("Fetching data")
|
||||
try:
|
||||
await self._tibber_home.update_info_and_price_info()
|
||||
except TimeoutError, aiohttp.ClientError:
|
||||
return
|
||||
data = self._tibber_home.info["viewer"]["home"]
|
||||
self._attr_extra_state_attributes["app_nickname"] = data["appNickname"]
|
||||
self._attr_extra_state_attributes["grid_company"] = data["meteringPointData"][
|
||||
"gridCompany"
|
||||
self._attr_native_unit_of_measurement = home_data.get(
|
||||
"price_unit", self._tibber_home.price_unit
|
||||
)
|
||||
self._attr_native_value = current_price
|
||||
self._attr_extra_state_attributes["intraday_price_ranking"] = home_data.get(
|
||||
"intraday_price_ranking"
|
||||
)
|
||||
self._attr_extra_state_attributes["max_price"] = home_data["max_price"]
|
||||
self._attr_extra_state_attributes["avg_price"] = home_data["avg_price"]
|
||||
self._attr_extra_state_attributes["min_price"] = home_data["min_price"]
|
||||
self._attr_extra_state_attributes["off_peak_1"] = home_data["off_peak_1"]
|
||||
self._attr_extra_state_attributes["peak"] = home_data["peak"]
|
||||
self._attr_extra_state_attributes["off_peak_2"] = home_data["off_peak_2"]
|
||||
self._attr_extra_state_attributes["app_nickname"] = home_data["app_nickname"]
|
||||
self._attr_extra_state_attributes["grid_company"] = home_data["grid_company"]
|
||||
self._attr_extra_state_attributes["estimated_annual_consumption"] = home_data[
|
||||
"estimated_annual_consumption"
|
||||
]
|
||||
self._attr_extra_state_attributes["estimated_annual_consumption"] = data[
|
||||
"meteringPointData"
|
||||
]["estimatedAnnualConsumption"]
|
||||
self._attr_available = True
|
||||
self.async_write_ha_state()
|
||||
|
||||
|
||||
class TibberDataSensor(TibberSensor, CoordinatorEntity[TibberDataCoordinator]):
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["tplink-omada-client==1.5.3"]
|
||||
"requirements": ["tplink-omada-client==1.5.6"]
|
||||
}
|
||||
|
||||
@@ -81,6 +81,7 @@ clean_area:
|
||||
selector:
|
||||
area:
|
||||
multiple: true
|
||||
reorder: true
|
||||
|
||||
send_command:
|
||||
target:
|
||||
|
||||
@@ -398,7 +398,7 @@ SENSOR_DESCRIPTIONS = {
|
||||
Keys.WARNING: VictronBLESensorEntityDescription(
|
||||
key=Keys.WARNING,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
translation_key="alarm",
|
||||
translation_key="warning",
|
||||
options=ALARM_OPTIONS,
|
||||
),
|
||||
Keys.YIELD_TODAY: VictronBLESensorEntityDescription(
|
||||
@@ -410,7 +410,7 @@ SENSOR_DESCRIPTIONS = {
|
||||
),
|
||||
}
|
||||
|
||||
for i in range(1, 8):
|
||||
for i in range(1, 9):
|
||||
cell_key = getattr(Keys, f"CELL_{i}_VOLTAGE")
|
||||
SENSOR_DESCRIPTIONS[cell_key] = VictronBLESensorEntityDescription(
|
||||
key=cell_key,
|
||||
@@ -418,6 +418,7 @@ for i in range(1, 8):
|
||||
device_class=SensorDeviceClass.VOLTAGE,
|
||||
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
translation_placeholders={"cell": str(i)},
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -248,7 +248,24 @@
|
||||
"name": "[%key:component::victron_ble::common::starter_voltage%]"
|
||||
},
|
||||
"warning": {
|
||||
"name": "Warning"
|
||||
"name": "Warning",
|
||||
"state": {
|
||||
"bms_lockout": "[%key:component::victron_ble::entity::sensor::alarm::state::bms_lockout%]",
|
||||
"dc_ripple": "[%key:component::victron_ble::entity::sensor::alarm::state::dc_ripple%]",
|
||||
"high_starter_voltage": "[%key:component::victron_ble::entity::sensor::alarm::state::high_starter_voltage%]",
|
||||
"high_temperature": "[%key:component::victron_ble::entity::sensor::alarm::state::high_temperature%]",
|
||||
"high_v_ac_out": "[%key:component::victron_ble::entity::sensor::alarm::state::high_v_ac_out%]",
|
||||
"high_voltage": "[%key:component::victron_ble::entity::sensor::alarm::state::high_voltage%]",
|
||||
"low_soc": "[%key:component::victron_ble::entity::sensor::alarm::state::low_soc%]",
|
||||
"low_starter_voltage": "[%key:component::victron_ble::entity::sensor::alarm::state::low_starter_voltage%]",
|
||||
"low_temperature": "[%key:component::victron_ble::entity::sensor::alarm::state::low_temperature%]",
|
||||
"low_v_ac_out": "[%key:component::victron_ble::entity::sensor::alarm::state::low_v_ac_out%]",
|
||||
"low_voltage": "[%key:component::victron_ble::entity::sensor::alarm::state::low_voltage%]",
|
||||
"mid_voltage": "[%key:component::victron_ble::common::midpoint_voltage%]",
|
||||
"no_alarm": "[%key:component::victron_ble::entity::sensor::alarm::state::no_alarm%]",
|
||||
"overload": "[%key:component::victron_ble::entity::sensor::alarm::state::overload%]",
|
||||
"short_circuit": "[%key:component::victron_ble::entity::sensor::alarm::state::short_circuit%]"
|
||||
}
|
||||
},
|
||||
"yield_today": {
|
||||
"name": "Yield today"
|
||||
|
||||
@@ -78,6 +78,7 @@ class VodafoneStationRouter(DataUpdateCoordinator[UpdateCoordinatorDataType]):
|
||||
data,
|
||||
session,
|
||||
)
|
||||
self._session = session
|
||||
|
||||
# Last resort as no MAC or S/N can be retrieved via API
|
||||
self._id = config_entry.unique_id
|
||||
@@ -135,11 +136,15 @@ class VodafoneStationRouter(DataUpdateCoordinator[UpdateCoordinatorDataType]):
|
||||
_LOGGER.debug("Polling Vodafone Station host: %s", self.api.base_url.host)
|
||||
|
||||
try:
|
||||
await self.api.login()
|
||||
if not self._session.cookie_jar.filter_cookies(self.api.base_url):
|
||||
_LOGGER.debug(
|
||||
"Session cookies missing for host %s, re-login",
|
||||
self.api.base_url.host,
|
||||
)
|
||||
await self.api.login()
|
||||
raw_data_devices = await self.api.get_devices_data()
|
||||
data_sensors = await self.api.get_sensor_data()
|
||||
data_wifi = await self.api.get_wifi_data()
|
||||
await self.api.logout()
|
||||
except exceptions.CannotAuthenticate as err:
|
||||
raise ConfigEntryAuthFailed(
|
||||
translation_domain=DOMAIN,
|
||||
|
||||
@@ -104,6 +104,7 @@ class VodafoneSwitchEntity(CoordinatorEntity[VodafoneStationRouter], SwitchEntit
|
||||
await self.coordinator.api.set_wifi_status(
|
||||
status, self.entity_description.typology, self.entity_description.band
|
||||
)
|
||||
await self.coordinator.async_request_refresh()
|
||||
except CannotAuthenticate as err:
|
||||
self.coordinator.config_entry.async_start_reauth(self.hass)
|
||||
raise HomeAssistantError(
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["wolf_comm"],
|
||||
"requirements": ["wolf-comm==0.0.23"]
|
||||
"requirements": ["wolf-comm==0.0.48"]
|
||||
}
|
||||
|
||||
@@ -25,5 +25,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/xiaomi_ble",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["xiaomi-ble==1.6.0"]
|
||||
"requirements": ["xiaomi-ble==1.10.0"]
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ from __future__ import annotations
|
||||
from pathlib import Path
|
||||
from typing import cast
|
||||
|
||||
from aiohttp import ClientResponseError
|
||||
from aiohttp import ClientError
|
||||
from yalexs.const import Brand
|
||||
from yalexs.exceptions import YaleApiError
|
||||
from yalexs.manager.const import CONF_BRAND
|
||||
@@ -15,7 +15,12 @@ from yalexs.manager.gateway import Config as YaleXSConfig
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.exceptions import (
|
||||
ConfigEntryAuthFailed,
|
||||
ConfigEntryNotReady,
|
||||
OAuth2TokenRequestError,
|
||||
OAuth2TokenRequestReauthError,
|
||||
)
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.config_entry_oauth2_flow import (
|
||||
ImplementationUnavailableError,
|
||||
@@ -42,11 +47,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: YaleConfigEntry) -> bool
|
||||
yale_gateway = YaleGateway(Path(hass.config.config_dir), session, oauth_session)
|
||||
try:
|
||||
await async_setup_yale(hass, entry, yale_gateway)
|
||||
except OAuth2TokenRequestReauthError as err:
|
||||
raise ConfigEntryAuthFailed from err
|
||||
except (RequireValidation, InvalidAuth) as err:
|
||||
raise ConfigEntryAuthFailed from err
|
||||
except TimeoutError as err:
|
||||
raise ConfigEntryNotReady("Timed out connecting to yale api") from err
|
||||
except (YaleApiError, ClientResponseError, CannotConnect) as err:
|
||||
except (
|
||||
YaleApiError,
|
||||
OAuth2TokenRequestError,
|
||||
ClientError,
|
||||
CannotConnect,
|
||||
) as err:
|
||||
raise ConfigEntryNotReady from err
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
return True
|
||||
|
||||
@@ -14,5 +14,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["socketio", "engineio", "yalexs"],
|
||||
"requirements": ["yalexs==9.2.0", "yalexs-ble==3.2.7"]
|
||||
"requirements": ["yalexs==9.2.0", "yalexs-ble==3.2.8"]
|
||||
}
|
||||
|
||||
@@ -13,5 +13,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/yalexs_ble",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["yalexs-ble==3.2.7"]
|
||||
"requirements": ["yalexs-ble==3.2.8"]
|
||||
}
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/yolink",
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_push",
|
||||
"requirements": ["yolink-api==0.6.1"]
|
||||
"requirements": ["yolink-api==0.6.3"]
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
"universal_silabs_flasher",
|
||||
"serialx"
|
||||
],
|
||||
"requirements": ["zha==1.0.1", "serialx==0.6.2"],
|
||||
"requirements": ["zha==1.0.2", "serialx==0.6.2"],
|
||||
"usb": [
|
||||
{
|
||||
"description": "*2652*",
|
||||
|
||||
@@ -9,6 +9,7 @@ from zwave_js_server.const import (
|
||||
SET_VALUE_SUCCESS,
|
||||
TARGET_STATE_PROPERTY,
|
||||
TARGET_VALUE_PROPERTY,
|
||||
CommandClass,
|
||||
SetValueStatus,
|
||||
)
|
||||
from zwave_js_server.const.command_class.barrier_operator import BarrierState
|
||||
@@ -87,6 +88,8 @@ class CoverPositionMixin(ZWaveBaseEntity, CoverEntity):
|
||||
_current_position_value: ZwaveValue | None = None
|
||||
_target_position_value: ZwaveValue | None = None
|
||||
_stop_position_value: ZwaveValue | None = None
|
||||
# Remember whether the moving state can be tracked reliably for this device.
|
||||
_moving_state_disabled: bool = False
|
||||
|
||||
def _set_position_values(
|
||||
self,
|
||||
@@ -153,11 +156,13 @@ class CoverPositionMixin(ZWaveBaseEntity, CoverEntity):
|
||||
if not self._attr_is_opening and not self._attr_is_closing:
|
||||
return
|
||||
|
||||
if (current := self._current_position_value) is None or current.value is None:
|
||||
return
|
||||
|
||||
if (
|
||||
(current := self._current_position_value) is not None
|
||||
and (target := self._target_position_value) is not None
|
||||
and current.value is not None
|
||||
and current.value == target.value
|
||||
(t := self._target_position_value) is not None
|
||||
and t.value is not None
|
||||
and current.value == t.value
|
||||
):
|
||||
self._attr_is_opening = False
|
||||
self._attr_is_closing = False
|
||||
@@ -182,9 +187,10 @@ class CoverPositionMixin(ZWaveBaseEntity, CoverEntity):
|
||||
self._target_position_value, target_position
|
||||
)
|
||||
if (
|
||||
self._moving_state_disabled
|
||||
# If the command is unsupervised, or the device reported that it started
|
||||
# working, we can assume the cover is moving in the desired direction.
|
||||
result is None
|
||||
or result is None
|
||||
or result.status
|
||||
not in (SetValueStatus.WORKING, SetValueStatus.SUCCESS_UNSUPERVISED)
|
||||
# If we don't know the current position, we don't know which direction
|
||||
@@ -350,6 +356,17 @@ class ZWaveMultilevelSwitchCover(CoverPositionMixin):
|
||||
),
|
||||
)
|
||||
|
||||
# Multilevel Switch CC v3 and earlier don't report targetValue,
|
||||
# so we cannot determine when the cover stops moving,
|
||||
# especially when the device is controlled physically.
|
||||
# OPENING/CLOSING states must not be used for these devices,
|
||||
# because they will become stale/incorrect.
|
||||
if (
|
||||
self.info.primary_value.command_class == CommandClass.SWITCH_MULTILEVEL
|
||||
and self.info.primary_value.cc_version < 4
|
||||
):
|
||||
self._moving_state_disabled = True
|
||||
|
||||
# Entity class attributes
|
||||
self._attr_device_class = CoverDeviceClass.WINDOW
|
||||
if (
|
||||
|
||||
@@ -205,15 +205,13 @@ DISCOVERY_SCHEMAS = [
|
||||
FanValueMapping(speeds=[(1, 33), (34, 67), (68, 99)]),
|
||||
),
|
||||
),
|
||||
# GE/Jasco - In-Wall Smart Fan Controls
|
||||
# GE/Jasco - In-Wall Smart Fan Controls - 14314 / ZW4002
|
||||
ZWaveDiscoverySchema(
|
||||
platform=Platform.FAN,
|
||||
hint="has_fan_value_mapping",
|
||||
manufacturer_id={0x0063},
|
||||
product_id={
|
||||
0x3131,
|
||||
0x3337, # 14287 / 55258 / ZW4002
|
||||
0x3533, # 58446 / ZWA4013
|
||||
0x3138, # 14314 / ZW4002
|
||||
},
|
||||
product_type={0x4944},
|
||||
@@ -222,6 +220,30 @@ DISCOVERY_SCHEMAS = [
|
||||
FanValueMapping(speeds=[(1, 32), (33, 66), (67, 99)]),
|
||||
),
|
||||
),
|
||||
# GE/Jasco - In-Wall Smart Fan Controls - 14287 / 55258 / ZW4002
|
||||
ZWaveDiscoverySchema(
|
||||
platform=Platform.FAN,
|
||||
hint="has_fan_value_mapping",
|
||||
manufacturer_id={0x0063},
|
||||
product_id={0x3337},
|
||||
product_type={0x4944},
|
||||
primary_value=SWITCH_MULTILEVEL_CURRENT_VALUE_SCHEMA,
|
||||
data_template=FixedFanValueMappingDataTemplate(
|
||||
FanValueMapping(speeds=[(1, 33), (34, 66), (67, 99)]),
|
||||
),
|
||||
),
|
||||
# GE/Jasco - In-Wall Smart Fan Controls - 58446 / ZWA4013
|
||||
ZWaveDiscoverySchema(
|
||||
platform=Platform.FAN,
|
||||
hint="has_fan_value_mapping",
|
||||
manufacturer_id={0x0063},
|
||||
product_id={0x3533},
|
||||
product_type={0x4944},
|
||||
primary_value=SWITCH_MULTILEVEL_CURRENT_VALUE_SCHEMA,
|
||||
data_template=FixedFanValueMappingDataTemplate(
|
||||
FanValueMapping(speeds=[(1, 25), (26, 50), (51, 75), (76, 99)]),
|
||||
),
|
||||
),
|
||||
# Leviton ZW4SF fan controllers using switch multilevel CC
|
||||
ZWaveDiscoverySchema(
|
||||
platform=Platform.FAN,
|
||||
|
||||
@@ -2245,9 +2245,10 @@ class ConfigEntries:
|
||||
self._entries = entries
|
||||
self.async_update_issues()
|
||||
|
||||
self.hass.bus.async_listen_once(
|
||||
EVENT_HOMEASSISTANT_STARTED, self._async_scan_orphan_ignored_entries
|
||||
)
|
||||
if not self.hass.config.recovery_mode and not self.hass.config.safe_mode:
|
||||
self.hass.bus.async_listen_once(
|
||||
EVENT_HOMEASSISTANT_STARTED, self._async_scan_orphan_ignored_entries
|
||||
)
|
||||
|
||||
async def _async_scan_orphan_ignored_entries(
|
||||
self, event: Event[NoEventData]
|
||||
|
||||
@@ -17,7 +17,7 @@ if TYPE_CHECKING:
|
||||
APPLICATION_NAME: Final = "HomeAssistant"
|
||||
MAJOR_VERSION: Final = 2026
|
||||
MINOR_VERSION: Final = 3
|
||||
PATCH_VERSION: Final = "1"
|
||||
PATCH_VERSION: Final = "4"
|
||||
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
||||
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
|
||||
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 14, 2)
|
||||
|
||||
@@ -181,15 +181,24 @@ class RestoreStateData:
|
||||
}
|
||||
|
||||
# Start with the currently registered states
|
||||
stored_states = [
|
||||
StoredState(
|
||||
current_states_by_entity_id[entity_id],
|
||||
entity.extra_restore_state_data,
|
||||
now,
|
||||
stored_states: list[StoredState] = []
|
||||
for entity_id, entity in self.entities.items():
|
||||
if entity_id not in current_states_by_entity_id:
|
||||
continue
|
||||
try:
|
||||
extra_data = entity.extra_restore_state_data
|
||||
except Exception:
|
||||
_LOGGER.exception(
|
||||
"Error getting extra restore state data for %s", entity_id
|
||||
)
|
||||
continue
|
||||
stored_states.append(
|
||||
StoredState(
|
||||
current_states_by_entity_id[entity_id],
|
||||
extra_data,
|
||||
now,
|
||||
)
|
||||
)
|
||||
for entity_id, entity in self.entities.items()
|
||||
if entity_id in current_states_by_entity_id
|
||||
]
|
||||
expiration_time = now - STATE_EXPIRATION
|
||||
|
||||
for entity_id, stored_state in self.last_states.items():
|
||||
@@ -219,6 +228,8 @@ class RestoreStateData:
|
||||
)
|
||||
except HomeAssistantError as exc:
|
||||
_LOGGER.error("Error saving current states", exc_info=exc)
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected error saving current states")
|
||||
|
||||
@callback
|
||||
def async_setup_dump(self, *args: Any) -> None:
|
||||
@@ -258,13 +269,15 @@ class RestoreStateData:
|
||||
|
||||
@callback
|
||||
def async_restore_entity_removed(
|
||||
self, entity_id: str, extra_data: ExtraStoredData | None
|
||||
self,
|
||||
entity_id: str,
|
||||
state: State | None,
|
||||
extra_data: ExtraStoredData | None,
|
||||
) -> None:
|
||||
"""Unregister this entity from saving state."""
|
||||
# When an entity is being removed from hass, store its last state. This
|
||||
# allows us to support state restoration if the entity is removed, then
|
||||
# re-added while hass is still running.
|
||||
state = self.hass.states.get(entity_id)
|
||||
# To fully mimic all the attribute data types when loaded from storage,
|
||||
# we're going to serialize it to JSON and then re-load it.
|
||||
if state is not None:
|
||||
@@ -287,8 +300,18 @@ class RestoreEntity(Entity):
|
||||
|
||||
async def async_internal_will_remove_from_hass(self) -> None:
|
||||
"""Run when entity will be removed from hass."""
|
||||
try:
|
||||
extra_data = self.extra_restore_state_data
|
||||
except Exception:
|
||||
_LOGGER.exception(
|
||||
"Error getting extra restore state data for %s", self.entity_id
|
||||
)
|
||||
state = None
|
||||
extra_data = None
|
||||
else:
|
||||
state = self.hass.states.get(self.entity_id)
|
||||
async_get(self.hass).async_restore_entity_removed(
|
||||
self.entity_id, self.extra_restore_state_data
|
||||
self.entity_id, state, extra_data
|
||||
)
|
||||
await super().async_internal_will_remove_from_hass()
|
||||
|
||||
|
||||
@@ -301,6 +301,7 @@ class AreaSelectorConfig(BaseSelectorConfig, total=False):
|
||||
entity: EntityFilterSelectorConfig | list[EntityFilterSelectorConfig]
|
||||
device: DeviceFilterSelectorConfig | list[DeviceFilterSelectorConfig]
|
||||
multiple: bool
|
||||
reorder: bool
|
||||
|
||||
|
||||
@SELECTORS.register("area")
|
||||
@@ -320,6 +321,7 @@ class AreaSelector(Selector[AreaSelectorConfig]):
|
||||
[DEVICE_FILTER_SELECTOR_CONFIG_SCHEMA],
|
||||
),
|
||||
vol.Optional("multiple", default=False): cv.boolean,
|
||||
vol.Optional("reorder", default=False): cv.boolean,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ habluetooth==5.8.0
|
||||
hass-nabucasa==1.15.0
|
||||
hassil==3.5.0
|
||||
home-assistant-bluetooth==1.13.1
|
||||
home-assistant-frontend==20260304.0
|
||||
home-assistant-frontend==20260312.1
|
||||
home-assistant-intents==2026.3.3
|
||||
httpx==0.28.1
|
||||
ifaddr==0.2.0
|
||||
@@ -48,7 +48,7 @@ Jinja2==3.1.6
|
||||
lru-dict==1.3.0
|
||||
mutagen==1.47.0
|
||||
openai==2.21.0
|
||||
orjson==3.11.5
|
||||
orjson==3.11.7
|
||||
packaging>=23.1
|
||||
paho-mqtt==2.1.0
|
||||
Pillow==12.1.1
|
||||
@@ -57,7 +57,7 @@ psutil-home-assistant==0.0.1
|
||||
PyJWT==2.10.1
|
||||
pymicro-vad==1.0.1
|
||||
PyNaCl==1.6.2
|
||||
pyOpenSSL==25.3.0
|
||||
pyOpenSSL==26.0.0
|
||||
pyserial==3.5
|
||||
pyspeex-noise==1.0.2
|
||||
python-slugify==8.0.4
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "homeassistant"
|
||||
version = "2026.3.1"
|
||||
version = "2026.3.4"
|
||||
license = "Apache-2.0"
|
||||
license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"]
|
||||
description = "Open-source home automation platform running on Python 3."
|
||||
@@ -64,8 +64,8 @@ dependencies = [
|
||||
"cryptography==46.0.5",
|
||||
"Pillow==12.1.1",
|
||||
"propcache==0.4.1",
|
||||
"pyOpenSSL==25.3.0",
|
||||
"orjson==3.11.5",
|
||||
"pyOpenSSL==26.0.0",
|
||||
"orjson==3.11.7",
|
||||
"packaging>=23.1",
|
||||
"psutil-home-assistant==0.0.1",
|
||||
"python-slugify==8.0.4",
|
||||
|
||||
4
requirements.txt
generated
4
requirements.txt
generated
@@ -34,14 +34,14 @@ ifaddr==0.2.0
|
||||
Jinja2==3.1.6
|
||||
lru-dict==1.3.0
|
||||
mutagen==1.47.0
|
||||
orjson==3.11.5
|
||||
orjson==3.11.7
|
||||
packaging>=23.1
|
||||
Pillow==12.1.1
|
||||
propcache==0.4.1
|
||||
psutil-home-assistant==0.0.1
|
||||
PyJWT==2.10.1
|
||||
pymicro-vad==1.0.1
|
||||
pyOpenSSL==25.3.0
|
||||
pyOpenSSL==26.0.0
|
||||
pyspeex-noise==1.0.2
|
||||
python-slugify==8.0.4
|
||||
PyTurboJPEG==1.8.0
|
||||
|
||||
56
requirements_all.txt
generated
56
requirements_all.txt
generated
@@ -47,7 +47,7 @@ PlexAPI==4.15.16
|
||||
ProgettiHWSW==0.1.3
|
||||
|
||||
# homeassistant.components.cast
|
||||
PyChromecast==14.0.9
|
||||
PyChromecast==14.0.10
|
||||
|
||||
# homeassistant.components.flume
|
||||
PyFlume==0.6.5
|
||||
@@ -190,7 +190,7 @@ aioairzone-cloud==0.7.2
|
||||
aioairzone==1.0.5
|
||||
|
||||
# homeassistant.components.alexa_devices
|
||||
aioamazondevices==13.0.0
|
||||
aioamazondevices==13.0.1
|
||||
|
||||
# homeassistant.components.ambient_network
|
||||
# homeassistant.components.ambient_station
|
||||
@@ -224,7 +224,7 @@ aiobafi6==0.9.0
|
||||
aiobotocore==2.21.1
|
||||
|
||||
# homeassistant.components.comelit
|
||||
aiocomelit==2.0.0
|
||||
aiocomelit==2.0.1
|
||||
|
||||
# homeassistant.components.dhcp
|
||||
aiodhcpwatcher==1.2.1
|
||||
@@ -422,7 +422,7 @@ aiosyncthing==0.7.1
|
||||
aiotankerkoenig==0.5.1
|
||||
|
||||
# homeassistant.components.tedee
|
||||
aiotedee==0.2.25
|
||||
aiotedee==0.2.27
|
||||
|
||||
# homeassistant.components.tractive
|
||||
aiotractive==1.0.0
|
||||
@@ -593,7 +593,7 @@ avea==1.6.1
|
||||
# avion==0.10
|
||||
|
||||
# homeassistant.components.axis
|
||||
axis==66
|
||||
axis==67
|
||||
|
||||
# homeassistant.components.fujitsu_fglair
|
||||
ayla-iot-unofficial==1.4.7
|
||||
@@ -1122,7 +1122,7 @@ gotailwind==0.3.0
|
||||
govee-ble==0.44.0
|
||||
|
||||
# homeassistant.components.govee_light_local
|
||||
govee-local-api==2.3.0
|
||||
govee-local-api==2.4.0
|
||||
|
||||
# homeassistant.components.remote_rpi_gpio
|
||||
gpiozero==1.6.2
|
||||
@@ -1137,7 +1137,7 @@ greeclimate==2.1.1
|
||||
greeneye_monitor==3.0.3
|
||||
|
||||
# homeassistant.components.green_planet_energy
|
||||
greenplanet-energy-api==0.1.4
|
||||
greenplanet-energy-api==0.1.10
|
||||
|
||||
# homeassistant.components.greenwave
|
||||
greenwavereality==0.5.1
|
||||
@@ -1226,7 +1226,7 @@ hole==0.9.0
|
||||
holidays==0.84
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20260304.0
|
||||
home-assistant-frontend==20260312.1
|
||||
|
||||
# homeassistant.components.conversation
|
||||
home-assistant-intents==2026.3.3
|
||||
@@ -1256,7 +1256,7 @@ huum==0.8.1
|
||||
hyperion-py==0.7.6
|
||||
|
||||
# homeassistant.components.hypontech
|
||||
hyponcloud==0.3.0
|
||||
hyponcloud==0.9.0
|
||||
|
||||
# homeassistant.components.iammeter
|
||||
iammeter==0.2.1
|
||||
@@ -1271,7 +1271,7 @@ ibeacon-ble==1.2.0
|
||||
# homeassistant.components.local_calendar
|
||||
# homeassistant.components.local_todo
|
||||
# homeassistant.components.remote_calendar
|
||||
ical==13.2.0
|
||||
ical==13.2.2
|
||||
|
||||
# homeassistant.components.caldav
|
||||
icalendar==6.3.1
|
||||
@@ -1663,7 +1663,7 @@ odp-amsterdam==6.1.2
|
||||
oemthermostat==1.1.1
|
||||
|
||||
# homeassistant.components.ohme
|
||||
ohme==1.6.0
|
||||
ohme==1.7.0
|
||||
|
||||
# homeassistant.components.ollama
|
||||
ollama==0.5.1
|
||||
@@ -1676,7 +1676,7 @@ ondilo==0.5.0
|
||||
|
||||
# homeassistant.components.onedrive
|
||||
# homeassistant.components.onedrive_for_business
|
||||
onedrive-personal-sdk==0.1.5
|
||||
onedrive-personal-sdk==0.1.7
|
||||
|
||||
# homeassistant.components.onvif
|
||||
onvif-zeep-async==4.0.4
|
||||
@@ -1714,10 +1714,10 @@ openwrt-luci-rpc==1.1.17
|
||||
openwrt-ubus-rpc==0.0.2
|
||||
|
||||
# homeassistant.components.opower
|
||||
opower==0.17.0
|
||||
opower==0.17.1
|
||||
|
||||
# homeassistant.components.oralb
|
||||
oralb-ble==1.0.2
|
||||
oralb-ble==1.1.0
|
||||
|
||||
# homeassistant.components.oru
|
||||
oru==0.1.11
|
||||
@@ -1938,7 +1938,7 @@ pyairobotrest==0.3.0
|
||||
pyairvisual==2023.08.1
|
||||
|
||||
# homeassistant.components.anglian_water
|
||||
pyanglianwater==3.1.0
|
||||
pyanglianwater==3.1.1
|
||||
|
||||
# homeassistant.components.aprilaire
|
||||
pyaprilaire==0.9.1
|
||||
@@ -2179,7 +2179,7 @@ pyitachip2ir==0.0.7
|
||||
pyituran==0.1.5
|
||||
|
||||
# homeassistant.components.jvc_projector
|
||||
pyjvcprojector==2.0.1
|
||||
pyjvcprojector==2.0.3
|
||||
|
||||
# homeassistant.components.kaleidescape
|
||||
pykaleidescape==1.1.3
|
||||
@@ -2370,7 +2370,7 @@ pyplaato==0.0.19
|
||||
pypoint==3.0.0
|
||||
|
||||
# homeassistant.components.portainer
|
||||
pyportainer==1.0.31
|
||||
pyportainer==1.0.33
|
||||
|
||||
# homeassistant.components.probe_plus
|
||||
pyprobeplus==1.1.2
|
||||
@@ -2473,7 +2473,7 @@ pysmappee==0.2.29
|
||||
pysmarlaapi==1.0.1
|
||||
|
||||
# homeassistant.components.smartthings
|
||||
pysmartthings==3.6.0
|
||||
pysmartthings==3.7.2
|
||||
|
||||
# homeassistant.components.smarty
|
||||
pysmarty2==0.10.3
|
||||
@@ -2485,7 +2485,7 @@ pysmhi==1.1.0
|
||||
pysml==0.1.5
|
||||
|
||||
# homeassistant.components.smlight
|
||||
pysmlight==0.2.16
|
||||
pysmlight==0.3.1
|
||||
|
||||
# homeassistant.components.snmp
|
||||
pysnmp==7.1.22
|
||||
@@ -2563,7 +2563,7 @@ python-gitlab==1.6.0
|
||||
python-google-drive-api==0.1.0
|
||||
|
||||
# homeassistant.components.google_weather
|
||||
python-google-weather-api==0.0.4
|
||||
python-google-weather-api==0.0.6
|
||||
|
||||
# homeassistant.components.analytics_insights
|
||||
python-homeassistant-analytics==0.9.0
|
||||
@@ -2612,7 +2612,7 @@ python-opensky==1.0.1
|
||||
|
||||
# homeassistant.components.otbr
|
||||
# homeassistant.components.thread
|
||||
python-otbr-api==2.8.0
|
||||
python-otbr-api==2.9.0
|
||||
|
||||
# homeassistant.components.overseerr
|
||||
python-overseerr==0.9.0
|
||||
@@ -2984,7 +2984,7 @@ starline==0.1.5
|
||||
starlingbank==3.2
|
||||
|
||||
# homeassistant.components.starlink
|
||||
starlink-grpc-core==1.2.3
|
||||
starlink-grpc-core==1.2.4
|
||||
|
||||
# homeassistant.components.statsd
|
||||
statsd==3.2.1
|
||||
@@ -3106,7 +3106,7 @@ total-connect-client==2025.12.2
|
||||
tp-connected==0.0.4
|
||||
|
||||
# homeassistant.components.tplink_omada
|
||||
tplink-omada-client==1.5.3
|
||||
tplink-omada-client==1.5.6
|
||||
|
||||
# homeassistant.components.transmission
|
||||
transmission-rpc==7.0.3
|
||||
@@ -3274,7 +3274,7 @@ wirelesstagpy==0.8.1
|
||||
wled==0.21.0
|
||||
|
||||
# homeassistant.components.wolflink
|
||||
wolf-comm==0.0.23
|
||||
wolf-comm==0.0.48
|
||||
|
||||
# homeassistant.components.wsdot
|
||||
wsdot==0.0.1
|
||||
@@ -3283,7 +3283,7 @@ wsdot==0.0.1
|
||||
wyoming==1.7.2
|
||||
|
||||
# homeassistant.components.xiaomi_ble
|
||||
xiaomi-ble==1.6.0
|
||||
xiaomi-ble==1.10.0
|
||||
|
||||
# homeassistant.components.knx
|
||||
xknx==3.15.0
|
||||
@@ -3307,7 +3307,7 @@ yalesmartalarmclient==0.4.3
|
||||
# homeassistant.components.august
|
||||
# homeassistant.components.yale
|
||||
# homeassistant.components.yalexs_ble
|
||||
yalexs-ble==3.2.7
|
||||
yalexs-ble==3.2.8
|
||||
|
||||
# homeassistant.components.august
|
||||
# homeassistant.components.yale
|
||||
@@ -3320,7 +3320,7 @@ yeelight==0.7.16
|
||||
yeelightsunflower==0.0.10
|
||||
|
||||
# homeassistant.components.yolink
|
||||
yolink-api==0.6.1
|
||||
yolink-api==0.6.3
|
||||
|
||||
# homeassistant.components.youless
|
||||
youless-api==2.2.0
|
||||
@@ -3347,7 +3347,7 @@ zeroconf==0.148.0
|
||||
zeversolar==0.3.2
|
||||
|
||||
# homeassistant.components.zha
|
||||
zha==1.0.1
|
||||
zha==1.0.2
|
||||
|
||||
# homeassistant.components.zhong_hong
|
||||
zhong-hong-hvac==1.0.13
|
||||
|
||||
56
requirements_test_all.txt
generated
56
requirements_test_all.txt
generated
@@ -47,7 +47,7 @@ PlexAPI==4.15.16
|
||||
ProgettiHWSW==0.1.3
|
||||
|
||||
# homeassistant.components.cast
|
||||
PyChromecast==14.0.9
|
||||
PyChromecast==14.0.10
|
||||
|
||||
# homeassistant.components.flume
|
||||
PyFlume==0.6.5
|
||||
@@ -181,7 +181,7 @@ aioairzone-cloud==0.7.2
|
||||
aioairzone==1.0.5
|
||||
|
||||
# homeassistant.components.alexa_devices
|
||||
aioamazondevices==13.0.0
|
||||
aioamazondevices==13.0.1
|
||||
|
||||
# homeassistant.components.ambient_network
|
||||
# homeassistant.components.ambient_station
|
||||
@@ -215,7 +215,7 @@ aiobafi6==0.9.0
|
||||
aiobotocore==2.21.1
|
||||
|
||||
# homeassistant.components.comelit
|
||||
aiocomelit==2.0.0
|
||||
aiocomelit==2.0.1
|
||||
|
||||
# homeassistant.components.dhcp
|
||||
aiodhcpwatcher==1.2.1
|
||||
@@ -407,7 +407,7 @@ aiosyncthing==0.7.1
|
||||
aiotankerkoenig==0.5.1
|
||||
|
||||
# homeassistant.components.tedee
|
||||
aiotedee==0.2.25
|
||||
aiotedee==0.2.27
|
||||
|
||||
# homeassistant.components.tractive
|
||||
aiotractive==1.0.0
|
||||
@@ -545,7 +545,7 @@ automower-ble==0.2.8
|
||||
av==16.0.1
|
||||
|
||||
# homeassistant.components.axis
|
||||
axis==66
|
||||
axis==67
|
||||
|
||||
# homeassistant.components.fujitsu_fglair
|
||||
ayla-iot-unofficial==1.4.7
|
||||
@@ -998,7 +998,7 @@ gotailwind==0.3.0
|
||||
govee-ble==0.44.0
|
||||
|
||||
# homeassistant.components.govee_light_local
|
||||
govee-local-api==2.3.0
|
||||
govee-local-api==2.4.0
|
||||
|
||||
# homeassistant.components.gpsd
|
||||
gps3==0.33.3
|
||||
@@ -1010,7 +1010,7 @@ greeclimate==2.1.1
|
||||
greeneye_monitor==3.0.3
|
||||
|
||||
# homeassistant.components.green_planet_energy
|
||||
greenplanet-energy-api==0.1.4
|
||||
greenplanet-energy-api==0.1.10
|
||||
|
||||
# homeassistant.components.pure_energie
|
||||
gridnet==5.0.1
|
||||
@@ -1087,7 +1087,7 @@ hole==0.9.0
|
||||
holidays==0.84
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20260304.0
|
||||
home-assistant-frontend==20260312.1
|
||||
|
||||
# homeassistant.components.conversation
|
||||
home-assistant-intents==2026.3.3
|
||||
@@ -1114,7 +1114,7 @@ huum==0.8.1
|
||||
hyperion-py==0.7.6
|
||||
|
||||
# homeassistant.components.hypontech
|
||||
hyponcloud==0.3.0
|
||||
hyponcloud==0.9.0
|
||||
|
||||
# homeassistant.components.iaqualink
|
||||
iaqualink==0.6.0
|
||||
@@ -1126,7 +1126,7 @@ ibeacon-ble==1.2.0
|
||||
# homeassistant.components.local_calendar
|
||||
# homeassistant.components.local_todo
|
||||
# homeassistant.components.remote_calendar
|
||||
ical==13.2.0
|
||||
ical==13.2.2
|
||||
|
||||
# homeassistant.components.caldav
|
||||
icalendar==6.3.1
|
||||
@@ -1449,7 +1449,7 @@ objgraph==3.5.0
|
||||
odp-amsterdam==6.1.2
|
||||
|
||||
# homeassistant.components.ohme
|
||||
ohme==1.6.0
|
||||
ohme==1.7.0
|
||||
|
||||
# homeassistant.components.ollama
|
||||
ollama==0.5.1
|
||||
@@ -1462,7 +1462,7 @@ ondilo==0.5.0
|
||||
|
||||
# homeassistant.components.onedrive
|
||||
# homeassistant.components.onedrive_for_business
|
||||
onedrive-personal-sdk==0.1.5
|
||||
onedrive-personal-sdk==0.1.7
|
||||
|
||||
# homeassistant.components.onvif
|
||||
onvif-zeep-async==4.0.4
|
||||
@@ -1491,10 +1491,10 @@ openrgb-python==0.3.6
|
||||
openwebifpy==4.3.1
|
||||
|
||||
# homeassistant.components.opower
|
||||
opower==0.17.0
|
||||
opower==0.17.1
|
||||
|
||||
# homeassistant.components.oralb
|
||||
oralb-ble==1.0.2
|
||||
oralb-ble==1.1.0
|
||||
|
||||
# homeassistant.components.ourgroceries
|
||||
ourgroceries==1.5.4
|
||||
@@ -1669,7 +1669,7 @@ pyairobotrest==0.3.0
|
||||
pyairvisual==2023.08.1
|
||||
|
||||
# homeassistant.components.anglian_water
|
||||
pyanglianwater==3.1.0
|
||||
pyanglianwater==3.1.1
|
||||
|
||||
# homeassistant.components.aprilaire
|
||||
pyaprilaire==0.9.1
|
||||
@@ -1856,7 +1856,7 @@ pyisy==3.4.1
|
||||
pyituran==0.1.5
|
||||
|
||||
# homeassistant.components.jvc_projector
|
||||
pyjvcprojector==2.0.1
|
||||
pyjvcprojector==2.0.3
|
||||
|
||||
# homeassistant.components.kaleidescape
|
||||
pykaleidescape==1.1.3
|
||||
@@ -2020,7 +2020,7 @@ pyplaato==0.0.19
|
||||
pypoint==3.0.0
|
||||
|
||||
# homeassistant.components.portainer
|
||||
pyportainer==1.0.31
|
||||
pyportainer==1.0.33
|
||||
|
||||
# homeassistant.components.probe_plus
|
||||
pyprobeplus==1.1.2
|
||||
@@ -2102,7 +2102,7 @@ pysmappee==0.2.29
|
||||
pysmarlaapi==1.0.1
|
||||
|
||||
# homeassistant.components.smartthings
|
||||
pysmartthings==3.6.0
|
||||
pysmartthings==3.7.2
|
||||
|
||||
# homeassistant.components.smarty
|
||||
pysmarty2==0.10.3
|
||||
@@ -2114,7 +2114,7 @@ pysmhi==1.1.0
|
||||
pysml==0.1.5
|
||||
|
||||
# homeassistant.components.smlight
|
||||
pysmlight==0.2.16
|
||||
pysmlight==0.3.1
|
||||
|
||||
# homeassistant.components.snmp
|
||||
pysnmp==7.1.22
|
||||
@@ -2165,7 +2165,7 @@ python-fullykiosk==0.0.14
|
||||
python-google-drive-api==0.1.0
|
||||
|
||||
# homeassistant.components.google_weather
|
||||
python-google-weather-api==0.0.4
|
||||
python-google-weather-api==0.0.6
|
||||
|
||||
# homeassistant.components.analytics_insights
|
||||
python-homeassistant-analytics==0.9.0
|
||||
@@ -2208,7 +2208,7 @@ python-opensky==1.0.1
|
||||
|
||||
# homeassistant.components.otbr
|
||||
# homeassistant.components.thread
|
||||
python-otbr-api==2.8.0
|
||||
python-otbr-api==2.9.0
|
||||
|
||||
# homeassistant.components.overseerr
|
||||
python-overseerr==0.9.0
|
||||
@@ -2514,7 +2514,7 @@ srpenergy==1.3.6
|
||||
starline==0.1.5
|
||||
|
||||
# homeassistant.components.starlink
|
||||
starlink-grpc-core==1.2.3
|
||||
starlink-grpc-core==1.2.4
|
||||
|
||||
# homeassistant.components.statsd
|
||||
statsd==3.2.1
|
||||
@@ -2606,7 +2606,7 @@ toonapi==0.3.0
|
||||
total-connect-client==2025.12.2
|
||||
|
||||
# homeassistant.components.tplink_omada
|
||||
tplink-omada-client==1.5.3
|
||||
tplink-omada-client==1.5.6
|
||||
|
||||
# homeassistant.components.transmission
|
||||
transmission-rpc==7.0.3
|
||||
@@ -2753,7 +2753,7 @@ wiffi==1.1.2
|
||||
wled==0.21.0
|
||||
|
||||
# homeassistant.components.wolflink
|
||||
wolf-comm==0.0.23
|
||||
wolf-comm==0.0.48
|
||||
|
||||
# homeassistant.components.wsdot
|
||||
wsdot==0.0.1
|
||||
@@ -2762,7 +2762,7 @@ wsdot==0.0.1
|
||||
wyoming==1.7.2
|
||||
|
||||
# homeassistant.components.xiaomi_ble
|
||||
xiaomi-ble==1.6.0
|
||||
xiaomi-ble==1.10.0
|
||||
|
||||
# homeassistant.components.knx
|
||||
xknx==3.15.0
|
||||
@@ -2783,7 +2783,7 @@ yalesmartalarmclient==0.4.3
|
||||
# homeassistant.components.august
|
||||
# homeassistant.components.yale
|
||||
# homeassistant.components.yalexs_ble
|
||||
yalexs-ble==3.2.7
|
||||
yalexs-ble==3.2.8
|
||||
|
||||
# homeassistant.components.august
|
||||
# homeassistant.components.yale
|
||||
@@ -2793,7 +2793,7 @@ yalexs==9.2.0
|
||||
yeelight==0.7.16
|
||||
|
||||
# homeassistant.components.yolink
|
||||
yolink-api==0.6.1
|
||||
yolink-api==0.6.3
|
||||
|
||||
# homeassistant.components.youless
|
||||
youless-api==2.2.0
|
||||
@@ -2817,7 +2817,7 @@ zeroconf==0.148.0
|
||||
zeversolar==0.3.2
|
||||
|
||||
# homeassistant.components.zha
|
||||
zha==1.0.1
|
||||
zha==1.0.2
|
||||
|
||||
# homeassistant.components.zinvolt
|
||||
zinvolt==0.3.0
|
||||
|
||||
@@ -2,10 +2,7 @@
|
||||
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from aioamazondevices.const.devices import (
|
||||
SPEAKER_GROUP_DEVICE_TYPE,
|
||||
SPEAKER_GROUP_FAMILY,
|
||||
)
|
||||
from aioamazondevices.const.devices import SPEAKER_GROUP_FAMILY
|
||||
from aioamazondevices.exceptions import CannotConnect, CannotRetrieveData
|
||||
import pytest
|
||||
|
||||
@@ -117,7 +114,7 @@ async def test_alexa_dnd_group_removal(
|
||||
identifiers={(DOMAIN, mock_config_entry.entry_id)},
|
||||
name=mock_config_entry.title,
|
||||
manufacturer="Amazon",
|
||||
model=SPEAKER_GROUP_DEVICE_TYPE,
|
||||
model="Speaker Group",
|
||||
entry_type=dr.DeviceEntryType.SERVICE,
|
||||
)
|
||||
|
||||
@@ -156,7 +153,7 @@ async def test_alexa_unsupported_notification_sensor_removal(
|
||||
identifiers={(DOMAIN, mock_config_entry.entry_id)},
|
||||
name=mock_config_entry.title,
|
||||
manufacturer="Amazon",
|
||||
model=SPEAKER_GROUP_DEVICE_TYPE,
|
||||
model="Speaker Group",
|
||||
entry_type=dr.DeviceEntryType.SERVICE,
|
||||
)
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
from aiohttp import ClientResponseError
|
||||
from aiohttp import ClientError, ClientResponseError
|
||||
import pytest
|
||||
from yalexs.const import Brand
|
||||
from yalexs.exceptions import AugustApiAIOHTTPError, InvalidAuth
|
||||
@@ -18,7 +18,11 @@ from homeassistant.const import (
|
||||
STATE_ON,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.exceptions import (
|
||||
HomeAssistantError,
|
||||
OAuth2TokenRequestReauthError,
|
||||
OAuth2TokenRequestTransientError,
|
||||
)
|
||||
from homeassistant.helpers import (
|
||||
device_registry as dr,
|
||||
entity_registry as er,
|
||||
@@ -304,3 +308,59 @@ async def test_oauth_implementation_not_available(hass: HomeAssistant) -> None:
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert entry.state is ConfigEntryState.SETUP_RETRY
|
||||
|
||||
|
||||
async def test_oauth_token_request_reauth_error(hass: HomeAssistant) -> None:
|
||||
"""Test OAuth token request reauth error starts a reauth flow."""
|
||||
entry = await mock_august_config_entry(hass)
|
||||
|
||||
with patch(
|
||||
"homeassistant.helpers.config_entry_oauth2_flow.OAuth2Session.async_ensure_token_valid",
|
||||
side_effect=OAuth2TokenRequestReauthError(
|
||||
request_info=Mock(real_url="https://auth.august.com/access_token"),
|
||||
status=401,
|
||||
domain=DOMAIN,
|
||||
),
|
||||
):
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert entry.state is ConfigEntryState.SETUP_ERROR
|
||||
flows = hass.config_entries.flow.async_progress()
|
||||
assert len(flows) == 1
|
||||
assert flows[0]["step_id"] == "pick_implementation"
|
||||
assert flows[0]["context"]["source"] == "reauth"
|
||||
|
||||
|
||||
async def test_oauth_token_request_transient_error_is_retryable(
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
"""Test OAuth token transient request error marks entry for setup retry."""
|
||||
entry = await mock_august_config_entry(hass)
|
||||
|
||||
with patch(
|
||||
"homeassistant.helpers.config_entry_oauth2_flow.OAuth2Session.async_ensure_token_valid",
|
||||
side_effect=OAuth2TokenRequestTransientError(
|
||||
request_info=Mock(real_url="https://auth.august.com/access_token"),
|
||||
status=500,
|
||||
domain=DOMAIN,
|
||||
),
|
||||
):
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert entry.state is ConfigEntryState.SETUP_RETRY
|
||||
|
||||
|
||||
async def test_oauth_client_error_is_retryable(hass: HomeAssistant) -> None:
|
||||
"""Test OAuth transport client errors mark entry for setup retry."""
|
||||
entry = await mock_august_config_entry(hass)
|
||||
|
||||
with patch(
|
||||
"homeassistant.helpers.config_entry_oauth2_flow.OAuth2Session.async_ensure_token_valid",
|
||||
side_effect=ClientError("connection error"),
|
||||
):
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert entry.state is ConfigEntryState.SETUP_RETRY
|
||||
|
||||
@@ -45,6 +45,21 @@ async def test_celsius_fahrenheit(
|
||||
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
|
||||
|
||||
|
||||
async def test_climate_entity_loads_without_static_values(
|
||||
hass: HomeAssistant,
|
||||
mock_bsblan: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test the climate entity still loads when static values are unavailable."""
|
||||
mock_bsblan.static_values.side_effect = BSBLANError("General error")
|
||||
|
||||
await setup_with_selected_platforms(hass, mock_config_entry, [Platform.CLIMATE])
|
||||
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state is not None
|
||||
assert state.attributes["current_temperature"] is not None
|
||||
|
||||
|
||||
async def test_climate_entity_properties(
|
||||
hass: HomeAssistant,
|
||||
mock_bsblan: AsyncMock,
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from bsblan import BSBLANError
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -27,3 +28,26 @@ async def test_diagnostics(
|
||||
hass, hass_client, mock_config_entry
|
||||
)
|
||||
assert diagnostics_data == snapshot
|
||||
|
||||
|
||||
async def test_diagnostics_without_static_values(
|
||||
hass: HomeAssistant,
|
||||
mock_bsblan: AsyncMock,
|
||||
hass_client: ClientSessionGenerator,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test diagnostics when static values are not available."""
|
||||
mock_bsblan.static_values.side_effect = BSBLANError("General error")
|
||||
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
diagnostics_data = await get_diagnostics_for_config_entry(
|
||||
hass, hass_client, mock_config_entry
|
||||
)
|
||||
|
||||
assert "info" in diagnostics_data
|
||||
assert "device" in diagnostics_data
|
||||
assert "fast_coordinator_data" in diagnostics_data
|
||||
assert diagnostics_data["static"] is None
|
||||
|
||||
@@ -78,30 +78,50 @@ async def test_config_entry_auth_failed_triggers_reauth(
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("method", "exception", "expected_state"),
|
||||
("method", "exception", "expected_state", "assert_static_fallback"),
|
||||
[
|
||||
(
|
||||
"initialize",
|
||||
BSBLANError("General error"),
|
||||
ConfigEntryState.SETUP_ERROR,
|
||||
False,
|
||||
),
|
||||
(
|
||||
"device",
|
||||
BSBLANConnectionError("Connection failed"),
|
||||
ConfigEntryState.SETUP_RETRY,
|
||||
False,
|
||||
),
|
||||
(
|
||||
"info",
|
||||
BSBLANAuthError("Authentication failed"),
|
||||
ConfigEntryState.SETUP_ERROR,
|
||||
False,
|
||||
),
|
||||
(
|
||||
"static_values",
|
||||
BSBLANError("General error"),
|
||||
ConfigEntryState.LOADED,
|
||||
True,
|
||||
),
|
||||
(
|
||||
"static_values",
|
||||
TimeoutError("Connection timeout"),
|
||||
ConfigEntryState.LOADED,
|
||||
True,
|
||||
),
|
||||
("static_values", BSBLANError("General error"), ConfigEntryState.SETUP_ERROR),
|
||||
],
|
||||
)
|
||||
async def test_config_entry_static_data_errors(
|
||||
async def test_config_entry_setup_errors(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_bsblan: MagicMock,
|
||||
method: str,
|
||||
exception: Exception,
|
||||
expected_state: ConfigEntryState,
|
||||
assert_static_fallback: bool,
|
||||
) -> None:
|
||||
"""Test various errors during static data fetching trigger appropriate config entry states."""
|
||||
"""Test setup errors trigger appropriate config entry states."""
|
||||
# Mock the specified method to raise the exception
|
||||
getattr(mock_bsblan, method).side_effect = exception
|
||||
|
||||
@@ -110,6 +130,8 @@ async def test_config_entry_static_data_errors(
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert mock_config_entry.state is expected_state
|
||||
if assert_static_fallback:
|
||||
assert mock_config_entry.runtime_data.static is None
|
||||
|
||||
|
||||
async def test_coordinator_dhw_config_update_error(
|
||||
|
||||
@@ -57,7 +57,7 @@ from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("show_advanced_options", "user_input", "expected_config"),
|
||||
("show_advanced_options", "user_input", "expected_config", "expected_options"),
|
||||
[
|
||||
(
|
||||
True,
|
||||
@@ -69,6 +69,11 @@ from tests.common import MockConfigEntry
|
||||
CONF_PORT: 1234,
|
||||
CONF_SSL: False,
|
||||
},
|
||||
{
|
||||
CONF_OLD_DISCOVERY: False,
|
||||
CONF_CONSIDER_HOME: DEFAULT_CONSIDER_HOME.total_seconds(),
|
||||
CONF_FEATURE_DEVICE_TRACKING: True,
|
||||
},
|
||||
),
|
||||
(
|
||||
False,
|
||||
@@ -80,10 +85,19 @@ from tests.common import MockConfigEntry
|
||||
CONF_PORT: 49000,
|
||||
CONF_SSL: False,
|
||||
},
|
||||
{
|
||||
CONF_OLD_DISCOVERY: False,
|
||||
CONF_CONSIDER_HOME: DEFAULT_CONSIDER_HOME.total_seconds(),
|
||||
CONF_FEATURE_DEVICE_TRACKING: True,
|
||||
},
|
||||
),
|
||||
(
|
||||
False,
|
||||
{**MOCK_USER_INPUT_SIMPLE, CONF_SSL: True},
|
||||
{
|
||||
**MOCK_USER_INPUT_SIMPLE,
|
||||
CONF_SSL: True,
|
||||
CONF_FEATURE_DEVICE_TRACKING: False,
|
||||
},
|
||||
{
|
||||
CONF_HOST: "fake_host",
|
||||
CONF_PASSWORD: "fake_pass",
|
||||
@@ -91,6 +105,11 @@ from tests.common import MockConfigEntry
|
||||
CONF_PORT: 49443,
|
||||
CONF_SSL: True,
|
||||
},
|
||||
{
|
||||
CONF_OLD_DISCOVERY: False,
|
||||
CONF_CONSIDER_HOME: DEFAULT_CONSIDER_HOME.total_seconds(),
|
||||
CONF_FEATURE_DEVICE_TRACKING: False,
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
@@ -100,6 +119,7 @@ async def test_user(
|
||||
show_advanced_options: bool,
|
||||
user_input: dict,
|
||||
expected_config: dict,
|
||||
expected_options: dict,
|
||||
) -> None:
|
||||
"""Test starting a flow by user."""
|
||||
with (
|
||||
@@ -143,10 +163,7 @@ async def test_user(
|
||||
)
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["data"] == expected_config
|
||||
assert (
|
||||
result["options"][CONF_CONSIDER_HOME]
|
||||
== DEFAULT_CONSIDER_HOME.total_seconds()
|
||||
)
|
||||
assert result["options"] == expected_options
|
||||
assert not result["result"].unique_id
|
||||
|
||||
assert mock_setup_entry.called
|
||||
|
||||
@@ -396,6 +396,11 @@ async def test_switch_device_no_ip_address(
|
||||
"async_set_deflection_enable",
|
||||
STATE_ON,
|
||||
),
|
||||
(
|
||||
"switch.mock_title_wi_fi_mywifi",
|
||||
"async_set_wlan_configuration",
|
||||
STATE_ON,
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_switch_turn_on_off(
|
||||
|
||||
@@ -141,5 +141,5 @@ def mock_hik_nvr(mock_hikcamera: MagicMock) -> MagicMock:
|
||||
camera = mock_hikcamera.return_value
|
||||
camera.get_type = "NVR"
|
||||
camera.current_event_states = {}
|
||||
camera.get_event_triggers.return_value = {"Motion": [1, 2]}
|
||||
camera.get_event_triggers.return_value = {"VMD": [1, 2]}
|
||||
return mock_hikcamera
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user