mirror of
https://github.com/home-assistant/core.git
synced 2026-02-05 14:55:35 +01:00
Compare commits
98 Commits
python-3.1
...
overkiz_su
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
85336eadd6 | ||
|
|
d037b2073d | ||
|
|
ff3abb5b0b | ||
|
|
c014d32cac | ||
|
|
fa58fe5f4e | ||
|
|
46f230c487 | ||
|
|
13a987aba3 | ||
|
|
9cef323581 | ||
|
|
7ea7576188 | ||
|
|
f8abbfd42b | ||
|
|
5cd1821bc9 | ||
|
|
2ef7f26ffb | ||
|
|
184bea49e2 | ||
|
|
c853fb2068 | ||
|
|
3e8923f105 | ||
|
|
17cca3e69d | ||
|
|
12714c489f | ||
|
|
f788d61b4a | ||
|
|
5c726af00b | ||
|
|
d1d207fbb2 | ||
|
|
6c7f8df7f7 | ||
|
|
6f8c9b1504 | ||
|
|
4f9aedbc84 | ||
|
|
52fb0343e4 | ||
|
|
1050b4580a | ||
|
|
344c42172e | ||
|
|
93cc0fd7f1 | ||
|
|
05fe636b55 | ||
|
|
f22467d099 | ||
|
|
4bc3899b32 | ||
|
|
fc4d6bf5f1 | ||
|
|
8ed0672a8f | ||
|
|
282e347a1b | ||
|
|
1bfb02b440 | ||
|
|
71b03bd9ae | ||
|
|
cbd69822eb | ||
|
|
db900f4dd2 | ||
|
|
a707e695bc | ||
|
|
4feceac205 | ||
|
|
10c20faaca | ||
|
|
abcd512401 | ||
|
|
fdf8edf474 | ||
|
|
47e1a98bee | ||
|
|
2d8572b943 | ||
|
|
660cfdbd50 | ||
|
|
4208595da6 | ||
|
|
b6b2d2fc6f | ||
|
|
6c4c632848 | ||
|
|
78cf62176f | ||
|
|
df971c7a42 | ||
|
|
1fcabb7f2d | ||
|
|
9fb60c9ea2 | ||
|
|
9c11a4646f | ||
|
|
b036a78776 | ||
|
|
60bb3cb704 | ||
|
|
0e770958ac | ||
|
|
2a54c71b6c | ||
|
|
50463291ab | ||
|
|
43cc34042a | ||
|
|
a02244ccda | ||
|
|
a739619121 | ||
|
|
5db97a5f1c | ||
|
|
804ba9c9cc | ||
|
|
5ecbcea946 | ||
|
|
11be2b6289 | ||
|
|
eefae0307b | ||
|
|
d397ee28ea | ||
|
|
02c821128e | ||
|
|
71dc15d45f | ||
|
|
1078387b22 | ||
|
|
35fab27d15 | ||
|
|
915dc7a908 | ||
|
|
e5a9738983 | ||
|
|
2ff73219a2 | ||
|
|
5dc1270ed1 | ||
|
|
9e95ad5a85 | ||
|
|
9a5d4610f7 | ||
|
|
41c524fce4 | ||
|
|
5f9fa95554 | ||
|
|
6950be8ea9 | ||
|
|
c5a8bf64d0 | ||
|
|
a2b9a6e9df | ||
|
|
a0c567f0da | ||
|
|
c7feafdde6 | ||
|
|
e1e74b0aeb | ||
|
|
673411ef97 | ||
|
|
f7e5af7cb1 | ||
|
|
0ee56ce708 | ||
|
|
f93a176398 | ||
|
|
cd2394bc12 | ||
|
|
5c20b8eaff | ||
|
|
4bd499d3a6 | ||
|
|
8a53b94c5a | ||
|
|
d5aff326e3 | ||
|
|
22f66abbe7 | ||
|
|
f635228b1f | ||
|
|
4c708c143d | ||
|
|
3369459d41 |
4
.github/workflows/builder.yml
vendored
4
.github/workflows/builder.yml
vendored
@@ -10,12 +10,12 @@ on:
|
||||
|
||||
env:
|
||||
BUILD_TYPE: core
|
||||
DEFAULT_PYTHON: "3.14.3"
|
||||
DEFAULT_PYTHON: "3.14.2"
|
||||
PIP_TIMEOUT: 60
|
||||
UV_HTTP_TIMEOUT: 60
|
||||
UV_SYSTEM_PYTHON: "true"
|
||||
# Base image version from https://github.com/home-assistant/docker
|
||||
BASE_IMAGE_VERSION: "2026.02.0"
|
||||
BASE_IMAGE_VERSION: "2026.01.0"
|
||||
ARCHITECTURES: '["amd64", "aarch64"]'
|
||||
|
||||
jobs:
|
||||
|
||||
4
.github/workflows/ci.yaml
vendored
4
.github/workflows/ci.yaml
vendored
@@ -41,8 +41,8 @@ env:
|
||||
UV_CACHE_VERSION: 1
|
||||
MYPY_CACHE_VERSION: 1
|
||||
HA_SHORT_VERSION: "2026.3"
|
||||
DEFAULT_PYTHON: "3.14.3"
|
||||
ALL_PYTHON_VERSIONS: "['3.14.3']"
|
||||
DEFAULT_PYTHON: "3.14.2"
|
||||
ALL_PYTHON_VERSIONS: "['3.14.2']"
|
||||
# 10.3 is the oldest supported version
|
||||
# - 10.3.32 is the version currently shipped with Synology (as of 17 Feb 2022)
|
||||
# 10.6 is the current long-term-support
|
||||
|
||||
2
.github/workflows/translations.yml
vendored
2
.github/workflows/translations.yml
vendored
@@ -10,7 +10,7 @@ on:
|
||||
- "**strings.json"
|
||||
|
||||
env:
|
||||
DEFAULT_PYTHON: "3.14.3"
|
||||
DEFAULT_PYTHON: "3.14.2"
|
||||
|
||||
jobs:
|
||||
upload:
|
||||
|
||||
2
.github/workflows/wheels.yml
vendored
2
.github/workflows/wheels.yml
vendored
@@ -17,7 +17,7 @@ on:
|
||||
- "script/gen_requirements_all.py"
|
||||
|
||||
env:
|
||||
DEFAULT_PYTHON: "3.14.3"
|
||||
DEFAULT_PYTHON: "3.14.2"
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref_name}}
|
||||
|
||||
@@ -435,6 +435,7 @@ homeassistant.components.raspberry_pi.*
|
||||
homeassistant.components.rdw.*
|
||||
homeassistant.components.recollect_waste.*
|
||||
homeassistant.components.recorder.*
|
||||
homeassistant.components.redgtech.*
|
||||
homeassistant.components.remember_the_milk.*
|
||||
homeassistant.components.remote.*
|
||||
homeassistant.components.remote_calendar.*
|
||||
|
||||
2
CODEOWNERS
generated
2
CODEOWNERS
generated
@@ -1355,6 +1355,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/recorder/ @home-assistant/core
|
||||
/homeassistant/components/recovery_mode/ @home-assistant/core
|
||||
/tests/components/recovery_mode/ @home-assistant/core
|
||||
/homeassistant/components/redgtech/ @jonhsady @luan-nvg
|
||||
/tests/components/redgtech/ @jonhsady @luan-nvg
|
||||
/homeassistant/components/refoss/ @ashionky
|
||||
/tests/components/refoss/ @ashionky
|
||||
/homeassistant/components/rehlko/ @bdraco @peterager
|
||||
|
||||
@@ -58,14 +58,14 @@
|
||||
"name": "Enable motion detection"
|
||||
},
|
||||
"play_stream": {
|
||||
"description": "Plays the camera stream on a supported media player.",
|
||||
"description": "Plays a camera stream on a supported media player.",
|
||||
"fields": {
|
||||
"format": {
|
||||
"description": "Stream format supported by the media player.",
|
||||
"name": "Format"
|
||||
},
|
||||
"media_player": {
|
||||
"description": "Media players to stream to.",
|
||||
"description": "Media player to stream to.",
|
||||
"name": "Media player"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -13,6 +13,6 @@
|
||||
"integration_type": "system",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["acme", "hass_nabucasa", "snitun"],
|
||||
"requirements": ["hass-nabucasa==1.12.0", "openai==2.15.0"],
|
||||
"requirements": ["hass-nabucasa==1.13.0", "openai==2.15.0"],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"codeowners": ["@zxdavb"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/evohome",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["evohome", "evohomeasync", "evohomeasync2"],
|
||||
"loggers": ["evohomeasync", "evohomeasync2"],
|
||||
"quality_scale": "legacy",
|
||||
"requirements": ["evohome-async==1.0.6"]
|
||||
"requirements": ["evohome-async==1.1.3"]
|
||||
}
|
||||
|
||||
@@ -5,10 +5,9 @@ from __future__ import annotations
|
||||
from collections.abc import Iterable
|
||||
import csv
|
||||
import dataclasses
|
||||
from functools import partial
|
||||
import logging
|
||||
import os
|
||||
from typing import TYPE_CHECKING, Any, Final, Self, cast, final
|
||||
from typing import TYPE_CHECKING, Any, Self, cast, final
|
||||
|
||||
from propcache.api import cached_property
|
||||
import voluptuous as vol
|
||||
@@ -23,13 +22,6 @@ from homeassistant.const import (
|
||||
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import config_validation as cv, entity_registry as er
|
||||
from homeassistant.helpers.deprecation import (
|
||||
DeprecatedConstant,
|
||||
DeprecatedConstantEnum,
|
||||
all_with_deprecated_constants,
|
||||
check_if_deprecated_constant,
|
||||
dir_with_deprecated_constants,
|
||||
)
|
||||
from homeassistant.helpers.entity import ToggleEntity, ToggleEntityDescription
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.frame import ReportBehavior, report_usage
|
||||
@@ -56,27 +48,6 @@ PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA
|
||||
PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE
|
||||
|
||||
|
||||
# These SUPPORT_* constants are deprecated as of Home Assistant 2022.5.
|
||||
# Please use the LightEntityFeature enum instead.
|
||||
_DEPRECATED_SUPPORT_BRIGHTNESS: Final = DeprecatedConstant(
|
||||
1, "supported_color_modes", "2026.1"
|
||||
) # Deprecated, replaced by color modes
|
||||
_DEPRECATED_SUPPORT_COLOR_TEMP: Final = DeprecatedConstant(
|
||||
2, "supported_color_modes", "2026.1"
|
||||
) # Deprecated, replaced by color modes
|
||||
_DEPRECATED_SUPPORT_EFFECT: Final = DeprecatedConstantEnum(
|
||||
LightEntityFeature.EFFECT, "2026.1"
|
||||
)
|
||||
_DEPRECATED_SUPPORT_FLASH: Final = DeprecatedConstantEnum(
|
||||
LightEntityFeature.FLASH, "2026.1"
|
||||
)
|
||||
_DEPRECATED_SUPPORT_COLOR: Final = DeprecatedConstant(
|
||||
16, "supported_color_modes", "2026.1"
|
||||
) # Deprecated, replaced by color modes
|
||||
_DEPRECATED_SUPPORT_TRANSITION: Final = DeprecatedConstantEnum(
|
||||
LightEntityFeature.TRANSITION, "2026.1"
|
||||
)
|
||||
|
||||
# Color mode of the light
|
||||
ATTR_COLOR_MODE = "color_mode"
|
||||
# List of color modes supported by the light
|
||||
@@ -291,7 +262,7 @@ def filter_turn_off_params(
|
||||
if not params:
|
||||
return params
|
||||
|
||||
supported_features = light.supported_features_compat
|
||||
supported_features = light.supported_features
|
||||
|
||||
if LightEntityFeature.FLASH not in supported_features:
|
||||
params.pop(ATTR_FLASH, None)
|
||||
@@ -303,7 +274,7 @@ def filter_turn_off_params(
|
||||
|
||||
def filter_turn_on_params(light: LightEntity, params: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Filter out params not supported by the light."""
|
||||
supported_features = light.supported_features_compat
|
||||
supported_features = light.supported_features
|
||||
|
||||
if LightEntityFeature.EFFECT not in supported_features:
|
||||
params.pop(ATTR_EFFECT, None)
|
||||
@@ -956,7 +927,7 @@ class LightEntity(ToggleEntity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
def capability_attributes(self) -> dict[str, Any]:
|
||||
"""Return capability attributes."""
|
||||
data: dict[str, Any] = {}
|
||||
supported_features = self.supported_features_compat
|
||||
supported_features = self.supported_features
|
||||
supported_color_modes = self._light_internal_supported_color_modes
|
||||
|
||||
if ColorMode.COLOR_TEMP in supported_color_modes:
|
||||
@@ -1106,12 +1077,12 @@ class LightEntity(ToggleEntity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
def state_attributes(self) -> dict[str, Any] | None:
|
||||
"""Return state attributes."""
|
||||
data: dict[str, Any] = {}
|
||||
supported_features = self.supported_features_compat
|
||||
supported_features = self.supported_features
|
||||
supported_color_modes = self.supported_color_modes
|
||||
legacy_supported_color_modes = (
|
||||
supported_color_modes or self._light_internal_supported_color_modes
|
||||
)
|
||||
supported_features_value = supported_features.value
|
||||
|
||||
_is_on = self.is_on
|
||||
color_mode = self._light_internal_color_mode if _is_on else None
|
||||
|
||||
@@ -1130,26 +1101,12 @@ class LightEntity(ToggleEntity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
data[ATTR_BRIGHTNESS] = self.brightness
|
||||
else:
|
||||
data[ATTR_BRIGHTNESS] = None
|
||||
elif supported_features_value & _DEPRECATED_SUPPORT_BRIGHTNESS.value:
|
||||
# Backwards compatibility for ambiguous / incomplete states
|
||||
# Warning is printed by supported_features_compat, remove in 2025.1
|
||||
if _is_on:
|
||||
data[ATTR_BRIGHTNESS] = self.brightness
|
||||
else:
|
||||
data[ATTR_BRIGHTNESS] = None
|
||||
|
||||
if color_temp_supported(supported_color_modes):
|
||||
if color_mode == ColorMode.COLOR_TEMP:
|
||||
data[ATTR_COLOR_TEMP_KELVIN] = self.color_temp_kelvin
|
||||
else:
|
||||
data[ATTR_COLOR_TEMP_KELVIN] = None
|
||||
elif supported_features_value & _DEPRECATED_SUPPORT_COLOR_TEMP.value:
|
||||
# Backwards compatibility
|
||||
# Warning is printed by supported_features_compat, remove in 2025.1
|
||||
if _is_on:
|
||||
data[ATTR_COLOR_TEMP_KELVIN] = self.color_temp_kelvin
|
||||
else:
|
||||
data[ATTR_COLOR_TEMP_KELVIN] = None
|
||||
|
||||
if color_supported(legacy_supported_color_modes) or color_temp_supported(
|
||||
legacy_supported_color_modes
|
||||
@@ -1187,24 +1144,8 @@ class LightEntity(ToggleEntity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
type(self),
|
||||
report_issue,
|
||||
)
|
||||
supported_features = self.supported_features_compat
|
||||
supported_features_value = supported_features.value
|
||||
supported_color_modes: set[ColorMode] = set()
|
||||
|
||||
if supported_features_value & _DEPRECATED_SUPPORT_COLOR_TEMP.value:
|
||||
supported_color_modes.add(ColorMode.COLOR_TEMP)
|
||||
if supported_features_value & _DEPRECATED_SUPPORT_COLOR.value:
|
||||
supported_color_modes.add(ColorMode.HS)
|
||||
if (
|
||||
not supported_color_modes
|
||||
and supported_features_value & _DEPRECATED_SUPPORT_BRIGHTNESS.value
|
||||
):
|
||||
supported_color_modes = {ColorMode.BRIGHTNESS}
|
||||
|
||||
if not supported_color_modes:
|
||||
supported_color_modes = {ColorMode.ONOFF}
|
||||
|
||||
return supported_color_modes
|
||||
return {ColorMode.ONOFF}
|
||||
|
||||
@cached_property
|
||||
def supported_color_modes(self) -> set[ColorMode] | None:
|
||||
@@ -1216,48 +1157,9 @@ class LightEntity(ToggleEntity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
"""Flag supported features."""
|
||||
return self._attr_supported_features
|
||||
|
||||
@property
|
||||
def supported_features_compat(self) -> LightEntityFeature:
|
||||
"""Return the supported features as LightEntityFeature.
|
||||
|
||||
Remove this compatibility shim in 2025.1 or later.
|
||||
"""
|
||||
features = self.supported_features
|
||||
if type(features) is not int:
|
||||
return features
|
||||
new_features = LightEntityFeature(features)
|
||||
if self._deprecated_supported_features_reported is True:
|
||||
return new_features
|
||||
self._deprecated_supported_features_reported = True
|
||||
report_issue = self._suggest_report_issue()
|
||||
report_issue += (
|
||||
" and reference "
|
||||
"https://developers.home-assistant.io/blog/2023/12/28/support-feature-magic-numbers-deprecation"
|
||||
)
|
||||
_LOGGER.warning(
|
||||
(
|
||||
"Entity %s (%s) is using deprecated supported features"
|
||||
" values which will be removed in HA Core 2025.1. Instead it should use"
|
||||
" %s and color modes, please %s"
|
||||
),
|
||||
self.entity_id,
|
||||
type(self),
|
||||
repr(new_features),
|
||||
report_issue,
|
||||
)
|
||||
return new_features
|
||||
|
||||
def __should_report_light_issue(self) -> bool:
|
||||
"""Return if light color mode issues should be reported."""
|
||||
if not self.platform:
|
||||
return True
|
||||
# philips_js has known issues, we don't need users to open issues
|
||||
return self.platform.platform_name not in {"philips_js"}
|
||||
|
||||
|
||||
# These can be removed if no deprecated constant are in this module anymore
|
||||
__getattr__ = partial(check_if_deprecated_constant, module_globals=globals())
|
||||
__dir__ = partial(
|
||||
dir_with_deprecated_constants, module_globals_keys=[*globals().keys()]
|
||||
)
|
||||
__all__ = all_with_deprecated_constants(globals())
|
||||
|
||||
@@ -58,18 +58,95 @@ class OverkizEntity(CoordinatorEntity[OverkizDataUpdateCoordinator]):
|
||||
"""Return Overkiz device linked to this entity."""
|
||||
return self.coordinator.data[self.device_url]
|
||||
|
||||
def _get_sibling_devices(self) -> list[Device]:
|
||||
"""Return sibling devices sharing the same base device URL."""
|
||||
prefix = f"{self.base_device_url}#"
|
||||
return [
|
||||
device
|
||||
for device in self.coordinator.data.values()
|
||||
if device.device_url != self.device_url
|
||||
and device.device_url.startswith(prefix)
|
||||
]
|
||||
|
||||
def _has_siblings_with_different_place_oid(self) -> bool:
|
||||
"""Check if sibling devices have different placeOIDs.
|
||||
|
||||
Returns True if siblings have different place_oid values, indicating
|
||||
devices should be grouped by placeOID rather than by base URL.
|
||||
"""
|
||||
my_place_oid = self.device.place_oid
|
||||
if not my_place_oid:
|
||||
return False
|
||||
|
||||
return any(
|
||||
sibling.place_oid and sibling.place_oid != my_place_oid
|
||||
for sibling in self._get_sibling_devices()
|
||||
)
|
||||
|
||||
def _get_device_index(self, device_url: str) -> int | None:
|
||||
"""Extract numeric index from device URL (e.g., 'io://gw/123#4' -> 4)."""
|
||||
suffix = device_url.split("#")[-1]
|
||||
return int(suffix) if suffix.isdigit() else None
|
||||
|
||||
def _is_main_device_for_place_oid(self) -> bool:
|
||||
"""Check if this device is the main device for its placeOID group.
|
||||
|
||||
The device with the lowest URL index among siblings sharing the same
|
||||
placeOID is considered the main device and provides full device info.
|
||||
"""
|
||||
my_place_oid = self.device.place_oid
|
||||
if not my_place_oid:
|
||||
return True
|
||||
|
||||
my_index = self._get_device_index(self.device_url)
|
||||
if my_index is None:
|
||||
return True
|
||||
|
||||
return not any(
|
||||
(sibling_index := self._get_device_index(sibling.device_url)) is not None
|
||||
and sibling_index < my_index
|
||||
for sibling in self._get_sibling_devices()
|
||||
if sibling.place_oid == my_place_oid
|
||||
)
|
||||
|
||||
def _get_via_device_id(self, use_place_oid_grouping: bool) -> str:
|
||||
"""Return the via_device identifier for device registry hierarchy.
|
||||
|
||||
Sub-devices link to the main actuator (#1 device) when using placeOID
|
||||
grouping, otherwise they link directly to the gateway.
|
||||
"""
|
||||
gateway_id = self.executor.get_gateway_id()
|
||||
|
||||
if not use_place_oid_grouping or self.device_url.endswith("#1"):
|
||||
return gateway_id
|
||||
|
||||
main_device = self.coordinator.data.get(f"{self.base_device_url}#1")
|
||||
if main_device and main_device.place_oid:
|
||||
return f"{self.base_device_url}#{main_device.place_oid}"
|
||||
|
||||
return gateway_id
|
||||
|
||||
def generate_device_info(self) -> DeviceInfo:
|
||||
"""Return device registry information for this entity."""
|
||||
# Some devices, such as the Smart Thermostat have several devices
|
||||
# in one physical device, with same device url, terminated by '#' and a number.
|
||||
# In this case, we use the base device url as the device identifier.
|
||||
if self.is_sub_device:
|
||||
# Only return the url of the base device, to inherit device name
|
||||
# and model from parent device.
|
||||
# Some devices, such as the Smart Thermostat, have several sub-devices
|
||||
# sharing the same base URL (terminated by '#' and a number).
|
||||
use_place_oid_grouping = self._has_siblings_with_different_place_oid()
|
||||
|
||||
# Sub-devices without placeOID grouping inherit info from parent device
|
||||
if self.is_sub_device and not use_place_oid_grouping:
|
||||
return DeviceInfo(
|
||||
identifiers={(DOMAIN, self.executor.base_device_url)},
|
||||
)
|
||||
|
||||
# Determine identifier based on grouping strategy
|
||||
if use_place_oid_grouping:
|
||||
identifier = f"{self.base_device_url}#{self.device.place_oid}"
|
||||
# Non-main devices only reference the identifier
|
||||
if not self._is_main_device_for_place_oid():
|
||||
return DeviceInfo(identifiers={(DOMAIN, identifier)})
|
||||
else:
|
||||
identifier = self.executor.base_device_url
|
||||
|
||||
manufacturer = (
|
||||
self.executor.select_attribute(OverkizAttribute.CORE_MANUFACTURER)
|
||||
or self.executor.select_state(OverkizState.CORE_MANUFACTURER_NAME)
|
||||
@@ -92,7 +169,7 @@ class OverkizEntity(CoordinatorEntity[OverkizDataUpdateCoordinator]):
|
||||
)
|
||||
|
||||
return DeviceInfo(
|
||||
identifiers={(DOMAIN, self.executor.base_device_url)},
|
||||
identifiers={(DOMAIN, identifier)},
|
||||
name=self.device.label,
|
||||
manufacturer=str(manufacturer),
|
||||
model=str(model),
|
||||
@@ -102,7 +179,7 @@ class OverkizEntity(CoordinatorEntity[OverkizDataUpdateCoordinator]):
|
||||
),
|
||||
hw_version=self.device.controllable_name,
|
||||
suggested_area=suggested_area,
|
||||
via_device=(DOMAIN, self.executor.get_gateway_id()),
|
||||
via_device=(DOMAIN, self._get_via_device_id(use_place_oid_grouping)),
|
||||
configuration_url=self.coordinator.client.server.configuration_url,
|
||||
)
|
||||
|
||||
|
||||
35
homeassistant/components/redgtech/__init__.py
Normal file
35
homeassistant/components/redgtech/__init__.py
Normal file
@@ -0,0 +1,35 @@
|
||||
"""Initialize the Redgtech integration for Home Assistant."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .coordinator import RedgtechConfigEntry, RedgtechDataUpdateCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
PLATFORMS = [Platform.SWITCH]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: RedgtechConfigEntry) -> bool:
|
||||
"""Set up Redgtech from a config entry."""
|
||||
_LOGGER.debug("Setting up Redgtech entry: %s", entry.entry_id)
|
||||
coordinator = RedgtechDataUpdateCoordinator(hass, entry)
|
||||
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
entry.runtime_data = coordinator
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
_LOGGER.debug("Successfully set up Redgtech entry: %s", entry.entry_id)
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: RedgtechConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
_LOGGER.debug("Unloading Redgtech entry: %s", entry.entry_id)
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
67
homeassistant/components/redgtech/config_flow.py
Normal file
67
homeassistant/components/redgtech/config_flow.py
Normal file
@@ -0,0 +1,67 @@
|
||||
"""Config flow for the Redgtech integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from redgtech_api.api import RedgtechAPI, RedgtechAuthError, RedgtechConnectionError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD
|
||||
|
||||
from .const import DOMAIN, INTEGRATION_NAME
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RedgtechConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Config Flow for Redgtech integration."""
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the initial user step for login."""
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
if user_input is not None:
|
||||
email = user_input[CONF_EMAIL]
|
||||
password = user_input[CONF_PASSWORD]
|
||||
|
||||
self._async_abort_entries_match({CONF_EMAIL: user_input[CONF_EMAIL]})
|
||||
|
||||
api = RedgtechAPI()
|
||||
try:
|
||||
await api.login(email, password)
|
||||
except RedgtechAuthError:
|
||||
errors["base"] = "invalid_auth"
|
||||
except RedgtechConnectionError:
|
||||
errors["base"] = "cannot_connect"
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected error during login")
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
_LOGGER.debug("Login successful, token received")
|
||||
return self.async_create_entry(
|
||||
title=email,
|
||||
data={
|
||||
CONF_EMAIL: email,
|
||||
CONF_PASSWORD: password,
|
||||
},
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=self.add_suggested_values_to_schema(
|
||||
vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_EMAIL): str,
|
||||
vol.Required(CONF_PASSWORD): str,
|
||||
}
|
||||
),
|
||||
user_input,
|
||||
),
|
||||
errors=errors,
|
||||
description_placeholders={"integration_name": INTEGRATION_NAME},
|
||||
)
|
||||
4
homeassistant/components/redgtech/const.py
Normal file
4
homeassistant/components/redgtech/const.py
Normal file
@@ -0,0 +1,4 @@
|
||||
"""Constants for the Redgtech integration."""
|
||||
|
||||
DOMAIN = "redgtech"
|
||||
INTEGRATION_NAME = "Redgtech"
|
||||
130
homeassistant/components/redgtech/coordinator.py
Normal file
130
homeassistant/components/redgtech/coordinator.py
Normal file
@@ -0,0 +1,130 @@
|
||||
"""Coordinator for Redgtech integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable, Coroutine
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from redgtech_api.api import RedgtechAPI, RedgtechAuthError, RedgtechConnectionError
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryError
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
UPDATE_INTERVAL = timedelta(seconds=15)
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class RedgtechDevice:
|
||||
"""Representation of a Redgtech device."""
|
||||
|
||||
unique_id: str
|
||||
name: str
|
||||
state: bool
|
||||
|
||||
|
||||
type RedgtechConfigEntry = ConfigEntry[RedgtechDataUpdateCoordinator]
|
||||
|
||||
|
||||
class RedgtechDataUpdateCoordinator(DataUpdateCoordinator[dict[str, RedgtechDevice]]):
|
||||
"""Coordinator to manage fetching data from the Redgtech API.
|
||||
|
||||
Uses a dictionary keyed by unique_id for O(1) device lookup instead of O(n) list iteration.
|
||||
"""
|
||||
|
||||
config_entry: RedgtechConfigEntry
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config_entry: RedgtechConfigEntry) -> None:
|
||||
"""Initialize the coordinator."""
|
||||
self.api = RedgtechAPI()
|
||||
self.access_token: str | None = None
|
||||
self.email = config_entry.data[CONF_EMAIL]
|
||||
self.password = config_entry.data[CONF_PASSWORD]
|
||||
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
name=DOMAIN,
|
||||
update_interval=UPDATE_INTERVAL,
|
||||
config_entry=config_entry,
|
||||
)
|
||||
|
||||
async def login(self, email: str, password: str) -> str | None:
|
||||
"""Login to the Redgtech API and return the access token."""
|
||||
try:
|
||||
self.access_token = await self.api.login(email, password)
|
||||
except RedgtechAuthError as e:
|
||||
raise ConfigEntryError("Authentication error during login") from e
|
||||
except RedgtechConnectionError as e:
|
||||
raise UpdateFailed("Connection error during login") from e
|
||||
else:
|
||||
_LOGGER.debug("Access token obtained successfully")
|
||||
return self.access_token
|
||||
|
||||
async def renew_token(self, email: str, password: str) -> None:
|
||||
"""Renew the access token."""
|
||||
self.access_token = await self.api.login(email, password)
|
||||
_LOGGER.debug("Access token renewed successfully")
|
||||
|
||||
async def call_api_with_valid_token[_R, *_Ts](
|
||||
self, api_call: Callable[[*_Ts], Coroutine[Any, Any, _R]], *args: *_Ts
|
||||
) -> _R:
|
||||
"""Make an API call with a valid token.
|
||||
|
||||
Ensure we have a valid access token, renewing it if necessary.
|
||||
"""
|
||||
if not self.access_token:
|
||||
_LOGGER.debug("No access token, logging in")
|
||||
self.access_token = await self.login(self.email, self.password)
|
||||
else:
|
||||
_LOGGER.debug("Using existing access token")
|
||||
try:
|
||||
return await api_call(*args)
|
||||
except RedgtechAuthError:
|
||||
_LOGGER.debug("Auth failed, trying to renew token")
|
||||
await self.renew_token(
|
||||
self.config_entry.data[CONF_EMAIL],
|
||||
self.config_entry.data[CONF_PASSWORD],
|
||||
)
|
||||
return await api_call(*args)
|
||||
|
||||
async def _async_update_data(self) -> dict[str, RedgtechDevice]:
|
||||
"""Fetch data from the API on demand.
|
||||
|
||||
Returns a dictionary keyed by unique_id for efficient device lookup.
|
||||
"""
|
||||
_LOGGER.debug("Fetching data from Redgtech API on demand")
|
||||
try:
|
||||
data = await self.call_api_with_valid_token(
|
||||
self.api.get_data, self.access_token
|
||||
)
|
||||
except RedgtechAuthError as e:
|
||||
raise ConfigEntryError("Authentication failed") from e
|
||||
except RedgtechConnectionError as e:
|
||||
raise UpdateFailed("Failed to connect to Redgtech API") from e
|
||||
|
||||
devices: dict[str, RedgtechDevice] = {}
|
||||
|
||||
for item in data["boards"]:
|
||||
display_categories = {cat.lower() for cat in item["displayCategories"]}
|
||||
|
||||
if "light" in display_categories or "switch" not in display_categories:
|
||||
continue
|
||||
|
||||
device = RedgtechDevice(
|
||||
unique_id=item["endpointId"],
|
||||
name=item["friendlyName"],
|
||||
state=item["value"],
|
||||
)
|
||||
_LOGGER.debug("Processing device: %s", device)
|
||||
devices[device.unique_id] = device
|
||||
|
||||
return devices
|
||||
11
homeassistant/components/redgtech/manifest.json
Normal file
11
homeassistant/components/redgtech/manifest.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"domain": "redgtech",
|
||||
"name": "Redgtech",
|
||||
"codeowners": ["@jonhsady", "@luan-nvg"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/redgtech",
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["redgtech-api==0.1.38"]
|
||||
}
|
||||
72
homeassistant/components/redgtech/quality_scale.yaml
Normal file
72
homeassistant/components/redgtech/quality_scale.yaml
Normal file
@@ -0,0 +1,72 @@
|
||||
rules:
|
||||
# Bronze
|
||||
action-setup:
|
||||
status: exempt
|
||||
comment: No custom actions
|
||||
appropriate-polling: done
|
||||
brands: done
|
||||
common-modules: done
|
||||
config-flow-test-coverage: done
|
||||
config-flow: done
|
||||
dependency-transparency: done
|
||||
docs-actions:
|
||||
status: exempt
|
||||
comment: No custom actions
|
||||
docs-high-level-description: done
|
||||
docs-installation-instructions: done
|
||||
docs-removal-instructions: done
|
||||
entity-event-setup:
|
||||
status: exempt
|
||||
comment: No explicit signature for events
|
||||
entity-unique-id: done
|
||||
has-entity-name: done
|
||||
runtime-data: done
|
||||
test-before-configure: done
|
||||
test-before-setup: done
|
||||
unique-config-entry: done
|
||||
|
||||
# Silver
|
||||
action-exceptions: done
|
||||
config-entry-unloading: done
|
||||
docs-configuration-parameters:
|
||||
status: exempt
|
||||
comment: No options flow
|
||||
docs-installation-parameters: done
|
||||
entity-unavailable: done
|
||||
integration-owner: done
|
||||
log-when-unavailable: done
|
||||
parallel-updates: done
|
||||
reauthentication-flow: todo
|
||||
test-coverage: done
|
||||
|
||||
# Gold
|
||||
devices: done
|
||||
diagnostics: todo
|
||||
discovery-update-info: todo
|
||||
discovery: todo
|
||||
docs-data-update: done
|
||||
docs-examples: done
|
||||
docs-known-limitations: done
|
||||
docs-supported-devices: done
|
||||
docs-supported-functions: done
|
||||
docs-troubleshooting: done
|
||||
docs-use-cases: done
|
||||
dynamic-devices: todo
|
||||
entity-category: done
|
||||
entity-device-class: done
|
||||
entity-disabled-by-default:
|
||||
status: exempt
|
||||
comment: Only essential entities
|
||||
entity-translations: done
|
||||
exception-translations: todo
|
||||
icon-translations: done
|
||||
reconfiguration-flow: todo
|
||||
repair-issues:
|
||||
status: exempt
|
||||
comment: No repair issues
|
||||
stale-devices: todo
|
||||
|
||||
# Platinum
|
||||
async-dependency: done
|
||||
inject-websession: todo
|
||||
strict-typing: done
|
||||
40
homeassistant/components/redgtech/strings.json
Normal file
40
homeassistant/components/redgtech/strings.json
Normal file
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"email": "[%key:common::config_flow::data::email%]",
|
||||
"password": "[%key:common::config_flow::data::password%]"
|
||||
},
|
||||
"data_description": {
|
||||
"email": "Enter the email address associated with your {integration_name} account.",
|
||||
"password": "Enter the password for your {integration_name} account."
|
||||
},
|
||||
"description": "Please enter your credentials to connect to the {integration_name} API.",
|
||||
"title": "Set up {integration_name}"
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"api_error": {
|
||||
"message": "Error while communicating with the {integration_name} API"
|
||||
},
|
||||
"authentication_failed": {
|
||||
"message": "Authentication failed. Please check your credentials."
|
||||
},
|
||||
"connection_error": {
|
||||
"message": "Connection error with {integration_name} API"
|
||||
},
|
||||
"switch_auth_error": {
|
||||
"message": "Authentication failed when controlling {integration_name} switch"
|
||||
}
|
||||
}
|
||||
}
|
||||
95
homeassistant/components/redgtech/switch.py
Normal file
95
homeassistant/components/redgtech/switch.py
Normal file
@@ -0,0 +1,95 @@
|
||||
"""Integration for Redgtech switches."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from redgtech_api.api import RedgtechAuthError, RedgtechConnectionError
|
||||
|
||||
from homeassistant.components.switch import SwitchEntity
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN, INTEGRATION_NAME
|
||||
from .coordinator import (
|
||||
RedgtechConfigEntry,
|
||||
RedgtechDataUpdateCoordinator,
|
||||
RedgtechDevice,
|
||||
)
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: RedgtechConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the switch platform."""
|
||||
coordinator = config_entry.runtime_data
|
||||
async_add_entities(
|
||||
RedgtechSwitch(coordinator, device) for device in coordinator.data.values()
|
||||
)
|
||||
|
||||
|
||||
class RedgtechSwitch(CoordinatorEntity[RedgtechDataUpdateCoordinator], SwitchEntity):
|
||||
"""Representation of a Redgtech switch."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
_attr_name = None
|
||||
|
||||
def __init__(
|
||||
self, coordinator: RedgtechDataUpdateCoordinator, device: RedgtechDevice
|
||||
) -> None:
|
||||
"""Initialize the switch."""
|
||||
super().__init__(coordinator)
|
||||
self.coordinator = coordinator
|
||||
self.device = device
|
||||
self._attr_unique_id = device.unique_id
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, device.unique_id)},
|
||||
name=device.name,
|
||||
manufacturer=INTEGRATION_NAME,
|
||||
)
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return true if the switch is on."""
|
||||
if device := self.coordinator.data.get(self.device.unique_id):
|
||||
return bool(device.state)
|
||||
return False
|
||||
|
||||
async def _set_state(self, new_state: bool) -> None:
|
||||
"""Set state of the switch."""
|
||||
try:
|
||||
await self.coordinator.call_api_with_valid_token(
|
||||
self.coordinator.api.set_switch_state,
|
||||
self.device.unique_id,
|
||||
new_state,
|
||||
self.coordinator.access_token,
|
||||
)
|
||||
except RedgtechAuthError as err:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="switch_auth_error",
|
||||
translation_placeholders={"integration_name": INTEGRATION_NAME},
|
||||
) from err
|
||||
except RedgtechConnectionError as err:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="connection_error",
|
||||
translation_placeholders={"integration_name": INTEGRATION_NAME},
|
||||
) from err
|
||||
|
||||
await self.coordinator.async_refresh()
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the switch on."""
|
||||
await self._set_state(True)
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn the switch off."""
|
||||
await self._set_state(False)
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import voluptuous as vol
|
||||
@@ -54,23 +53,11 @@ from .schemas import (
|
||||
from .template_entity import TemplateEntity
|
||||
from .trigger_entity import TriggerEntity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
OPEN_STATE = "open"
|
||||
OPENING_STATE = "opening"
|
||||
CLOSED_STATE = "closed"
|
||||
CLOSING_STATE = "closing"
|
||||
|
||||
_VALID_STATES = [
|
||||
OPEN_STATE,
|
||||
OPENING_STATE,
|
||||
CLOSED_STATE,
|
||||
CLOSING_STATE,
|
||||
"true",
|
||||
"false",
|
||||
"none",
|
||||
]
|
||||
|
||||
CONF_POSITION = "position"
|
||||
CONF_POSITION_TEMPLATE = "position_template"
|
||||
CONF_TILT = "tilt"
|
||||
|
||||
@@ -14,5 +14,6 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["pyvesync"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["pyvesync==3.4.1"]
|
||||
}
|
||||
|
||||
@@ -2865,53 +2865,3 @@ class ServiceRegistry:
|
||||
if TYPE_CHECKING:
|
||||
target = cast(Callable[..., ServiceResponse], target)
|
||||
return await self._hass.async_add_executor_job(target, service_call)
|
||||
|
||||
|
||||
# mypy: disable-error-code="attr-defined,no-untyped-def,unused-ignore,no-untyped-call"
|
||||
# fmt: off
|
||||
def _old_chain_future(source, destination):
|
||||
"""Revert of https://github.com/python/cpython/pull/142358."""
|
||||
from asyncio import futures # noqa: PLC0415
|
||||
|
||||
if not futures.isfuture(source) and not isinstance(
|
||||
source, concurrent.futures.Future
|
||||
):
|
||||
raise TypeError("A future is required for source argument")
|
||||
if not futures.isfuture(destination) and not isinstance(
|
||||
destination, concurrent.futures.Future
|
||||
):
|
||||
raise TypeError("A future is required for destination argument")
|
||||
source_loop = futures._get_loop(source) if futures.isfuture(source) else None # noqa: SLF001
|
||||
dest_loop = (
|
||||
futures._get_loop(destination) if futures.isfuture(destination) else None # noqa: SLF001
|
||||
)
|
||||
|
||||
def _set_state(future, other):
|
||||
if futures.isfuture(future):
|
||||
futures._copy_future_state(other, future) # noqa: SLF001
|
||||
else:
|
||||
futures._set_concurrent_future_state(future, other) # noqa: SLF001
|
||||
|
||||
def _call_check_cancel(destination):
|
||||
if destination.cancelled():
|
||||
if source_loop is None or source_loop is dest_loop:
|
||||
source.cancel()
|
||||
else:
|
||||
source_loop.call_soon_threadsafe(source.cancel)
|
||||
|
||||
def _call_set_state(source):
|
||||
if destination.cancelled() and dest_loop is not None and dest_loop.is_closed():
|
||||
return
|
||||
if dest_loop is None or dest_loop is source_loop:
|
||||
_set_state(destination, source)
|
||||
else:
|
||||
if dest_loop.is_closed():
|
||||
return
|
||||
dest_loop.call_soon_threadsafe(_set_state, destination, source)
|
||||
|
||||
destination.add_done_callback(_call_check_cancel)
|
||||
source.add_done_callback(_call_set_state)
|
||||
|
||||
|
||||
# monkey-patch asyncio to revert to 3.14.2 behavior
|
||||
asyncio.futures._chain_future = _old_chain_future # noqa: SLF001
|
||||
|
||||
1
homeassistant/generated/config_flows.py
generated
1
homeassistant/generated/config_flows.py
generated
@@ -570,6 +570,7 @@ FLOWS = {
|
||||
"rapt_ble",
|
||||
"rdw",
|
||||
"recollect_waste",
|
||||
"redgtech",
|
||||
"refoss",
|
||||
"rehlko",
|
||||
"remote_calendar",
|
||||
|
||||
@@ -5583,6 +5583,12 @@
|
||||
"config_flow": false,
|
||||
"iot_class": "cloud_polling"
|
||||
},
|
||||
"redgtech": {
|
||||
"name": "Redgtech",
|
||||
"integration_type": "service",
|
||||
"config_flow": true,
|
||||
"iot_class": "cloud_polling"
|
||||
},
|
||||
"refoss": {
|
||||
"name": "Refoss",
|
||||
"integration_type": "hub",
|
||||
|
||||
@@ -37,7 +37,7 @@ fnv-hash-fast==1.6.0
|
||||
go2rtc-client==0.4.0
|
||||
ha-ffmpeg==3.2.2
|
||||
habluetooth==5.8.0
|
||||
hass-nabucasa==1.12.0
|
||||
hass-nabucasa==1.13.0
|
||||
hassil==3.5.0
|
||||
home-assistant-bluetooth==1.13.1
|
||||
home-assistant-frontend==20260128.6
|
||||
|
||||
10
mypy.ini
generated
10
mypy.ini
generated
@@ -4106,6 +4106,16 @@ disallow_untyped_defs = true
|
||||
warn_return_any = true
|
||||
warn_unreachable = true
|
||||
|
||||
[mypy-homeassistant.components.redgtech.*]
|
||||
check_untyped_defs = true
|
||||
disallow_incomplete_defs = true
|
||||
disallow_subclassing_any = true
|
||||
disallow_untyped_calls = true
|
||||
disallow_untyped_decorators = true
|
||||
disallow_untyped_defs = true
|
||||
warn_return_any = true
|
||||
warn_unreachable = true
|
||||
|
||||
[mypy-homeassistant.components.remember_the_milk.*]
|
||||
check_untyped_defs = true
|
||||
disallow_incomplete_defs = true
|
||||
|
||||
@@ -51,7 +51,7 @@ dependencies = [
|
||||
"fnv-hash-fast==1.6.0",
|
||||
# hass-nabucasa is imported by helpers which don't depend on the cloud
|
||||
# integration
|
||||
"hass-nabucasa==1.12.0",
|
||||
"hass-nabucasa==1.13.0",
|
||||
# When bumping httpx, please check the version pins of
|
||||
# httpcore, anyio, and h11 in gen_requirements_all
|
||||
"httpx==0.28.1",
|
||||
|
||||
2
requirements.txt
generated
2
requirements.txt
generated
@@ -25,7 +25,7 @@ cronsim==2.7
|
||||
cryptography==46.0.2
|
||||
fnv-hash-fast==1.6.0
|
||||
ha-ffmpeg==3.2.2
|
||||
hass-nabucasa==1.12.0
|
||||
hass-nabucasa==1.13.0
|
||||
hassil==3.5.0
|
||||
home-assistant-bluetooth==1.13.1
|
||||
home-assistant-intents==2026.1.28
|
||||
|
||||
7
requirements_all.txt
generated
7
requirements_all.txt
generated
@@ -938,7 +938,7 @@ eufylife-ble-client==0.1.8
|
||||
# evdev==1.6.1
|
||||
|
||||
# homeassistant.components.evohome
|
||||
evohome-async==1.0.6
|
||||
evohome-async==1.1.3
|
||||
|
||||
# homeassistant.components.bryant_evolution
|
||||
evolutionhttp==0.0.18
|
||||
@@ -1175,7 +1175,7 @@ habluetooth==5.8.0
|
||||
hanna-cloud==0.0.7
|
||||
|
||||
# homeassistant.components.cloud
|
||||
hass-nabucasa==1.12.0
|
||||
hass-nabucasa==1.13.0
|
||||
|
||||
# homeassistant.components.splunk
|
||||
hass-splunk==0.1.1
|
||||
@@ -2744,6 +2744,9 @@ rapt-ble==0.1.2
|
||||
# homeassistant.components.raspyrfm
|
||||
raspyrfm-client==1.2.9
|
||||
|
||||
# homeassistant.components.redgtech
|
||||
redgtech-api==0.1.38
|
||||
|
||||
# homeassistant.components.refoss
|
||||
refoss-ha==1.2.5
|
||||
|
||||
|
||||
7
requirements_test_all.txt
generated
7
requirements_test_all.txt
generated
@@ -826,7 +826,7 @@ eternalegypt==0.0.18
|
||||
eufylife-ble-client==0.1.8
|
||||
|
||||
# homeassistant.components.evohome
|
||||
evohome-async==1.0.6
|
||||
evohome-async==1.1.3
|
||||
|
||||
# homeassistant.components.bryant_evolution
|
||||
evolutionhttp==0.0.18
|
||||
@@ -1045,7 +1045,7 @@ habluetooth==5.8.0
|
||||
hanna-cloud==0.0.7
|
||||
|
||||
# homeassistant.components.cloud
|
||||
hass-nabucasa==1.12.0
|
||||
hass-nabucasa==1.13.0
|
||||
|
||||
# homeassistant.components.assist_satellite
|
||||
# homeassistant.components.conversation
|
||||
@@ -2310,6 +2310,9 @@ radiotherm==2.1.0
|
||||
# homeassistant.components.rapt_ble
|
||||
rapt-ble==0.1.2
|
||||
|
||||
# homeassistant.components.redgtech
|
||||
redgtech-api==0.1.38
|
||||
|
||||
# homeassistant.components.refoss
|
||||
refoss-ha==1.2.5
|
||||
|
||||
|
||||
@@ -2039,7 +2039,6 @@ INTEGRATIONS_WITHOUT_SCALE = [
|
||||
"versasense",
|
||||
"version",
|
||||
"vicare",
|
||||
"vesync",
|
||||
"viaggiatreno",
|
||||
"vilfo",
|
||||
"vivotek",
|
||||
|
||||
@@ -814,7 +814,9 @@ async def test_put_light_state(
|
||||
|
||||
# mock light.turn_on call
|
||||
attributes = hass.states.get("light.ceiling_lights").attributes
|
||||
supported_features = attributes[ATTR_SUPPORTED_FEATURES] | light.SUPPORT_TRANSITION
|
||||
supported_features = (
|
||||
attributes[ATTR_SUPPORTED_FEATURES] | light.LightEntityFeature.TRANSITION
|
||||
)
|
||||
attributes = {**attributes, ATTR_SUPPORTED_FEATURES: supported_features}
|
||||
hass.states.async_set("light.ceiling_lights", STATE_ON, attributes)
|
||||
call_turn_on = async_mock_service(hass, "light", "turn_on")
|
||||
|
||||
@@ -168,7 +168,7 @@ async def setup_evohome(
|
||||
"evohomeasync2.auth.CredentialsManagerBase._post_request",
|
||||
mock_post_request(install),
|
||||
),
|
||||
patch("evohome.auth.AbstractAuth._make_request", mock_make_request(install)),
|
||||
patch("_evohome.auth.AbstractAuth._make_request", mock_make_request(install)),
|
||||
):
|
||||
evo: EvohomeClient | None = None
|
||||
|
||||
|
||||
@@ -31,13 +31,9 @@ _MSG_USR = (
|
||||
"special characters accepted via the vendor's website are not valid here."
|
||||
)
|
||||
|
||||
LOG_HINT_429_CREDS = ("evohome.credentials", logging.ERROR, _MSG_429)
|
||||
LOG_HINT_OTH_CREDS = ("evohome.credentials", logging.ERROR, _MSG_OTH)
|
||||
LOG_HINT_USR_CREDS = ("evohome.credentials", logging.ERROR, _MSG_USR)
|
||||
|
||||
LOG_HINT_429_AUTH = ("evohome.auth", logging.ERROR, _MSG_429)
|
||||
LOG_HINT_OTH_AUTH = ("evohome.auth", logging.ERROR, _MSG_OTH)
|
||||
LOG_HINT_USR_AUTH = ("evohome.auth", logging.ERROR, _MSG_USR)
|
||||
LOG_HINT_429_AUTH = ("evohomeasync2.auth", logging.ERROR, _MSG_429)
|
||||
LOG_HINT_OTH_AUTH = ("evohomeasync2.auth", logging.ERROR, _MSG_OTH)
|
||||
LOG_HINT_USR_AUTH = ("evohomeasync2.auth", logging.ERROR, _MSG_USR)
|
||||
|
||||
LOG_FAIL_CONNECTION = (
|
||||
"homeassistant.components.evohome",
|
||||
@@ -110,10 +106,10 @@ EXC_BAD_GATEWAY = aiohttp.ClientResponseError(
|
||||
)
|
||||
|
||||
AUTHENTICATION_TESTS: dict[Exception, list] = {
|
||||
EXC_BAD_CONNECTION: [LOG_HINT_OTH_CREDS, LOG_FAIL_CONNECTION, LOG_SETUP_FAILED],
|
||||
EXC_BAD_CREDENTIALS: [LOG_HINT_USR_CREDS, LOG_FAIL_CREDENTIALS, LOG_SETUP_FAILED],
|
||||
EXC_BAD_GATEWAY: [LOG_HINT_OTH_CREDS, LOG_FAIL_GATEWAY, LOG_SETUP_FAILED],
|
||||
EXC_TOO_MANY_REQUESTS: [LOG_HINT_429_CREDS, LOG_FAIL_TOO_MANY, LOG_SETUP_FAILED],
|
||||
EXC_BAD_CONNECTION: [LOG_HINT_OTH_AUTH, LOG_FAIL_CONNECTION, LOG_SETUP_FAILED],
|
||||
EXC_BAD_CREDENTIALS: [LOG_HINT_USR_AUTH, LOG_FAIL_CREDENTIALS, LOG_SETUP_FAILED],
|
||||
EXC_BAD_GATEWAY: [LOG_HINT_OTH_AUTH, LOG_FAIL_GATEWAY, LOG_SETUP_FAILED],
|
||||
EXC_TOO_MANY_REQUESTS: [LOG_HINT_429_AUTH, LOG_FAIL_TOO_MANY, LOG_SETUP_FAILED],
|
||||
}
|
||||
|
||||
CLIENT_REQUEST_TESTS: dict[Exception, list] = {
|
||||
@@ -137,7 +133,8 @@ async def test_authentication_failure_v2(
|
||||
|
||||
with (
|
||||
patch(
|
||||
"evohome.credentials.CredentialsManagerBase._request", side_effect=exception
|
||||
"_evohome.credentials.CredentialsManagerBase._request",
|
||||
side_effect=exception,
|
||||
),
|
||||
caplog.at_level(logging.WARNING),
|
||||
):
|
||||
@@ -165,7 +162,7 @@ async def test_client_request_failure_v2(
|
||||
"evohomeasync2.auth.CredentialsManagerBase._post_request",
|
||||
mock_post_request("default"),
|
||||
),
|
||||
patch("evohome.auth.AbstractAuth._request", side_effect=exception),
|
||||
patch("_evohome.auth.AbstractAuth._request", side_effect=exception),
|
||||
caplog.at_level(logging.WARNING),
|
||||
):
|
||||
result = await async_setup_component(hass, DOMAIN, {DOMAIN: config})
|
||||
|
||||
@@ -26,6 +26,7 @@ from homeassistant.components.light import (
|
||||
DOMAIN,
|
||||
ColorMode,
|
||||
LightEntity,
|
||||
LightEntityFeature,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
@@ -156,7 +157,7 @@ class MockLight(MockToggleEntity, LightEntity):
|
||||
|
||||
_attr_max_color_temp_kelvin = DEFAULT_MAX_KELVIN
|
||||
_attr_min_color_temp_kelvin = DEFAULT_MIN_KELVIN
|
||||
supported_features = 0
|
||||
supported_features = LightEntityFeature(0)
|
||||
|
||||
brightness = None
|
||||
color_temp_kelvin = None
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
"""The tests for the Light component."""
|
||||
|
||||
from types import ModuleType
|
||||
from typing import Literal
|
||||
from unittest.mock import MagicMock, mock_open, patch
|
||||
|
||||
import pytest
|
||||
@@ -30,9 +28,6 @@ from tests.common import (
|
||||
MockEntityPlatform,
|
||||
MockUser,
|
||||
async_mock_service,
|
||||
help_test_all,
|
||||
import_and_test_deprecated_constant,
|
||||
import_and_test_deprecated_constant_enum,
|
||||
setup_test_component_platform,
|
||||
)
|
||||
|
||||
@@ -137,13 +132,10 @@ async def test_services(
|
||||
ent3.supported_color_modes = [light.ColorMode.HS]
|
||||
ent1.supported_features = light.LightEntityFeature.TRANSITION
|
||||
ent2.supported_features = (
|
||||
light.SUPPORT_COLOR
|
||||
| light.LightEntityFeature.EFFECT
|
||||
| light.LightEntityFeature.TRANSITION
|
||||
light.LightEntityFeature.EFFECT | light.LightEntityFeature.TRANSITION
|
||||
)
|
||||
# Set color modes to none to trigger backwards compatibility in LightEntity
|
||||
ent2.supported_color_modes = None
|
||||
ent2.color_mode = None
|
||||
ent2.supported_color_modes = [light.ColorMode.HS]
|
||||
ent2.color_mode = light.ColorMode.HS
|
||||
ent3.supported_features = (
|
||||
light.LightEntityFeature.FLASH | light.LightEntityFeature.TRANSITION
|
||||
)
|
||||
@@ -903,16 +895,12 @@ async def test_light_brightness_step(hass: HomeAssistant) -> None:
|
||||
setup_test_component_platform(hass, light.DOMAIN, entities)
|
||||
|
||||
entity0 = entities[0]
|
||||
entity0.supported_features = light.SUPPORT_BRIGHTNESS
|
||||
# Set color modes to none to trigger backwards compatibility in LightEntity
|
||||
entity0.supported_color_modes = None
|
||||
entity0.color_mode = None
|
||||
entity0.supported_color_modes = {light.ColorMode.BRIGHTNESS}
|
||||
entity0.color_mode = light.ColorMode.BRIGHTNESS
|
||||
entity0.brightness = 100
|
||||
entity1 = entities[1]
|
||||
entity1.supported_features = light.SUPPORT_BRIGHTNESS
|
||||
# Set color modes to none to trigger backwards compatibility in LightEntity
|
||||
entity1.supported_color_modes = None
|
||||
entity1.color_mode = None
|
||||
entity1.supported_color_modes = {light.ColorMode.BRIGHTNESS}
|
||||
entity1.color_mode = light.ColorMode.BRIGHTNESS
|
||||
entity1.brightness = 50
|
||||
assert await async_setup_component(hass, "light", {"light": {"platform": "test"}})
|
||||
await hass.async_block_till_done()
|
||||
@@ -955,10 +943,8 @@ async def test_light_brightness_step_pct(hass: HomeAssistant) -> None:
|
||||
|
||||
setup_test_component_platform(hass, light.DOMAIN, [entity])
|
||||
|
||||
entity.supported_features = light.SUPPORT_BRIGHTNESS
|
||||
# Set color modes to none to trigger backwards compatibility in LightEntity
|
||||
entity.supported_color_modes = None
|
||||
entity.color_mode = None
|
||||
entity.supported_color_modes = {light.ColorMode.BRIGHTNESS}
|
||||
entity.color_mode = light.ColorMode.BRIGHTNESS
|
||||
entity.brightness = 255
|
||||
assert await async_setup_component(hass, "light", {"light": {"platform": "test"}})
|
||||
await hass.async_block_till_done()
|
||||
@@ -1000,10 +986,8 @@ async def test_light_brightness_pct_conversion(
|
||||
setup_test_component_platform(hass, light.DOMAIN, mock_light_entities)
|
||||
|
||||
entity = mock_light_entities[0]
|
||||
entity.supported_features = light.SUPPORT_BRIGHTNESS
|
||||
# Set color modes to none to trigger backwards compatibility in LightEntity
|
||||
entity.supported_color_modes = None
|
||||
entity.color_mode = None
|
||||
entity.supported_color_modes = {light.ColorMode.BRIGHTNESS}
|
||||
entity.color_mode = light.ColorMode.BRIGHTNESS
|
||||
entity.brightness = 100
|
||||
assert await async_setup_component(hass, "light", {"light": {"platform": "test"}})
|
||||
await hass.async_block_till_done()
|
||||
@@ -1152,167 +1136,6 @@ invalid_no_brightness_no_color_no_transition,,,
|
||||
assert invalid_profile_name not in profiles.data
|
||||
|
||||
|
||||
@pytest.mark.parametrize("light_state", [STATE_ON, STATE_OFF])
|
||||
async def test_light_backwards_compatibility_supported_color_modes(
|
||||
hass: HomeAssistant, light_state: Literal["on", "off"]
|
||||
) -> None:
|
||||
"""Test supported_color_modes if not implemented by the entity."""
|
||||
entities = [
|
||||
MockLight("Test_0", light_state),
|
||||
MockLight("Test_1", light_state),
|
||||
MockLight("Test_2", light_state),
|
||||
MockLight("Test_3", light_state),
|
||||
MockLight("Test_4", light_state),
|
||||
]
|
||||
|
||||
entity0 = entities[0]
|
||||
|
||||
entity1 = entities[1]
|
||||
entity1.supported_features = light.SUPPORT_BRIGHTNESS
|
||||
# Set color modes to none to trigger backwards compatibility in LightEntity
|
||||
entity1.supported_color_modes = None
|
||||
entity1.color_mode = None
|
||||
|
||||
entity2 = entities[2]
|
||||
entity2.supported_features = light.SUPPORT_BRIGHTNESS | light.SUPPORT_COLOR_TEMP
|
||||
# Set color modes to none to trigger backwards compatibility in LightEntity
|
||||
entity2.supported_color_modes = None
|
||||
entity2.color_mode = None
|
||||
|
||||
entity3 = entities[3]
|
||||
entity3.supported_features = light.SUPPORT_BRIGHTNESS | light.SUPPORT_COLOR
|
||||
# Set color modes to none to trigger backwards compatibility in LightEntity
|
||||
entity3.supported_color_modes = None
|
||||
entity3.color_mode = None
|
||||
|
||||
entity4 = entities[4]
|
||||
entity4.supported_features = (
|
||||
light.SUPPORT_BRIGHTNESS | light.SUPPORT_COLOR | light.SUPPORT_COLOR_TEMP
|
||||
)
|
||||
# Set color modes to none to trigger backwards compatibility in LightEntity
|
||||
entity4.supported_color_modes = None
|
||||
entity4.color_mode = None
|
||||
|
||||
setup_test_component_platform(hass, light.DOMAIN, entities)
|
||||
|
||||
assert await async_setup_component(hass, "light", {"light": {"platform": "test"}})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(entity0.entity_id)
|
||||
assert state.attributes["supported_color_modes"] == [light.ColorMode.ONOFF]
|
||||
if light_state == STATE_OFF:
|
||||
assert state.attributes["color_mode"] is None
|
||||
else:
|
||||
assert state.attributes["color_mode"] == light.ColorMode.ONOFF
|
||||
|
||||
state = hass.states.get(entity1.entity_id)
|
||||
assert state.attributes["supported_color_modes"] == [light.ColorMode.BRIGHTNESS]
|
||||
if light_state == STATE_OFF:
|
||||
assert state.attributes["color_mode"] is None
|
||||
else:
|
||||
assert state.attributes["color_mode"] == light.ColorMode.UNKNOWN
|
||||
|
||||
state = hass.states.get(entity2.entity_id)
|
||||
assert state.attributes["supported_color_modes"] == [light.ColorMode.COLOR_TEMP]
|
||||
if light_state == STATE_OFF:
|
||||
assert state.attributes["color_mode"] is None
|
||||
else:
|
||||
assert state.attributes["color_mode"] == light.ColorMode.UNKNOWN
|
||||
|
||||
state = hass.states.get(entity3.entity_id)
|
||||
assert state.attributes["supported_color_modes"] == [light.ColorMode.HS]
|
||||
if light_state == STATE_OFF:
|
||||
assert state.attributes["color_mode"] is None
|
||||
else:
|
||||
assert state.attributes["color_mode"] == light.ColorMode.UNKNOWN
|
||||
|
||||
state = hass.states.get(entity4.entity_id)
|
||||
assert state.attributes["supported_color_modes"] == [
|
||||
light.ColorMode.COLOR_TEMP,
|
||||
light.ColorMode.HS,
|
||||
]
|
||||
if light_state == STATE_OFF:
|
||||
assert state.attributes["color_mode"] is None
|
||||
else:
|
||||
assert state.attributes["color_mode"] == light.ColorMode.UNKNOWN
|
||||
|
||||
|
||||
async def test_light_backwards_compatibility_color_mode(hass: HomeAssistant) -> None:
|
||||
"""Test color_mode if not implemented by the entity."""
|
||||
entities = [
|
||||
MockLight("Test_0", STATE_ON),
|
||||
MockLight("Test_1", STATE_ON),
|
||||
MockLight("Test_2", STATE_ON),
|
||||
MockLight("Test_3", STATE_ON),
|
||||
MockLight("Test_4", STATE_ON),
|
||||
]
|
||||
|
||||
entity0 = entities[0]
|
||||
|
||||
entity1 = entities[1]
|
||||
entity1.supported_features = light.SUPPORT_BRIGHTNESS
|
||||
# Set color modes to none to trigger backwards compatibility in LightEntity
|
||||
entity1.supported_color_modes = None
|
||||
entity1.color_mode = None
|
||||
entity1.brightness = 100
|
||||
|
||||
entity2 = entities[2]
|
||||
entity2.supported_features = light.SUPPORT_BRIGHTNESS | light.SUPPORT_COLOR_TEMP
|
||||
# Set color modes to none to trigger backwards compatibility in LightEntity
|
||||
entity2.supported_color_modes = None
|
||||
entity2.color_mode = None
|
||||
entity2.color_temp_kelvin = 10000
|
||||
|
||||
entity3 = entities[3]
|
||||
entity3.supported_features = light.SUPPORT_BRIGHTNESS | light.SUPPORT_COLOR
|
||||
# Set color modes to none to trigger backwards compatibility in LightEntity
|
||||
entity3.supported_color_modes = None
|
||||
entity3.color_mode = None
|
||||
entity3.hs_color = (240, 100)
|
||||
|
||||
entity4 = entities[4]
|
||||
entity4.supported_features = (
|
||||
light.SUPPORT_BRIGHTNESS | light.SUPPORT_COLOR | light.SUPPORT_COLOR_TEMP
|
||||
)
|
||||
# Set color modes to none to trigger backwards compatibility in LightEntity
|
||||
entity4.supported_color_modes = None
|
||||
entity4.color_mode = None
|
||||
entity4.hs_color = (240, 100)
|
||||
entity4.color_temp_kelvin = 10000
|
||||
|
||||
setup_test_component_platform(hass, light.DOMAIN, entities)
|
||||
|
||||
assert await async_setup_component(hass, "light", {"light": {"platform": "test"}})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(entity0.entity_id)
|
||||
assert state.attributes["supported_color_modes"] == [light.ColorMode.ONOFF]
|
||||
assert state.attributes["color_mode"] == light.ColorMode.ONOFF
|
||||
|
||||
state = hass.states.get(entity1.entity_id)
|
||||
assert state.attributes["supported_color_modes"] == [light.ColorMode.BRIGHTNESS]
|
||||
assert state.attributes["color_mode"] == light.ColorMode.BRIGHTNESS
|
||||
|
||||
state = hass.states.get(entity2.entity_id)
|
||||
assert state.attributes["supported_color_modes"] == [light.ColorMode.COLOR_TEMP]
|
||||
assert state.attributes["color_mode"] == light.ColorMode.COLOR_TEMP
|
||||
assert state.attributes["rgb_color"] == (202, 218, 255)
|
||||
assert state.attributes["hs_color"] == (221.575, 20.9)
|
||||
assert state.attributes["xy_color"] == (0.278, 0.287)
|
||||
|
||||
state = hass.states.get(entity3.entity_id)
|
||||
assert state.attributes["supported_color_modes"] == [light.ColorMode.HS]
|
||||
assert state.attributes["color_mode"] == light.ColorMode.HS
|
||||
|
||||
state = hass.states.get(entity4.entity_id)
|
||||
assert state.attributes["supported_color_modes"] == [
|
||||
light.ColorMode.COLOR_TEMP,
|
||||
light.ColorMode.HS,
|
||||
]
|
||||
# hs color prioritized over color_temp, light should report mode ColorMode.HS
|
||||
assert state.attributes["color_mode"] == light.ColorMode.HS
|
||||
|
||||
|
||||
async def test_light_service_call_rgbw(hass: HomeAssistant) -> None:
|
||||
"""Test rgbw functionality in service calls."""
|
||||
entity0 = MockLight("Test_rgbw", STATE_ON)
|
||||
@@ -1478,7 +1301,6 @@ async def test_light_service_call_color_conversion(hass: HomeAssistant) -> None:
|
||||
MockLight("Test_rgb", STATE_ON),
|
||||
MockLight("Test_xy", STATE_ON),
|
||||
MockLight("Test_all", STATE_ON),
|
||||
MockLight("Test_legacy", STATE_ON),
|
||||
MockLight("Test_rgbw", STATE_ON),
|
||||
MockLight("Test_rgbww", STATE_ON),
|
||||
MockLight("Test_temperature", STATE_ON),
|
||||
@@ -1502,19 +1324,13 @@ async def test_light_service_call_color_conversion(hass: HomeAssistant) -> None:
|
||||
}
|
||||
|
||||
entity4 = entities[4]
|
||||
entity4.supported_features = light.SUPPORT_COLOR
|
||||
# Set color modes to none to trigger backwards compatibility in LightEntity
|
||||
entity4.supported_color_modes = None
|
||||
entity4.color_mode = None
|
||||
entity4.supported_color_modes = {light.ColorMode.RGBW}
|
||||
|
||||
entity5 = entities[5]
|
||||
entity5.supported_color_modes = {light.ColorMode.RGBW}
|
||||
entity5.supported_color_modes = {light.ColorMode.RGBWW}
|
||||
|
||||
entity6 = entities[6]
|
||||
entity6.supported_color_modes = {light.ColorMode.RGBWW}
|
||||
|
||||
entity7 = entities[7]
|
||||
entity7.supported_color_modes = {light.ColorMode.COLOR_TEMP}
|
||||
entity6.supported_color_modes = {light.ColorMode.COLOR_TEMP}
|
||||
|
||||
assert await async_setup_component(hass, "light", {"light": {"platform": "test"}})
|
||||
await hass.async_block_till_done()
|
||||
@@ -1536,15 +1352,12 @@ async def test_light_service_call_color_conversion(hass: HomeAssistant) -> None:
|
||||
]
|
||||
|
||||
state = hass.states.get(entity4.entity_id)
|
||||
assert state.attributes["supported_color_modes"] == [light.ColorMode.HS]
|
||||
|
||||
state = hass.states.get(entity5.entity_id)
|
||||
assert state.attributes["supported_color_modes"] == [light.ColorMode.RGBW]
|
||||
|
||||
state = hass.states.get(entity6.entity_id)
|
||||
state = hass.states.get(entity5.entity_id)
|
||||
assert state.attributes["supported_color_modes"] == [light.ColorMode.RGBWW]
|
||||
|
||||
state = hass.states.get(entity7.entity_id)
|
||||
state = hass.states.get(entity6.entity_id)
|
||||
assert state.attributes["supported_color_modes"] == [light.ColorMode.COLOR_TEMP]
|
||||
|
||||
await hass.services.async_call(
|
||||
@@ -1559,7 +1372,6 @@ async def test_light_service_call_color_conversion(hass: HomeAssistant) -> None:
|
||||
entity4.entity_id,
|
||||
entity5.entity_id,
|
||||
entity6.entity_id,
|
||||
entity7.entity_id,
|
||||
],
|
||||
"brightness_pct": 100,
|
||||
"hs_color": (240, 100),
|
||||
@@ -1575,12 +1387,10 @@ async def test_light_service_call_color_conversion(hass: HomeAssistant) -> None:
|
||||
_, data = entity3.last_call("turn_on")
|
||||
assert data == {"brightness": 255, "hs_color": (240.0, 100.0)}
|
||||
_, data = entity4.last_call("turn_on")
|
||||
assert data == {"brightness": 255, "hs_color": (240.0, 100.0)}
|
||||
_, data = entity5.last_call("turn_on")
|
||||
assert data == {"brightness": 255, "rgbw_color": (0, 0, 255, 0)}
|
||||
_, data = entity6.last_call("turn_on")
|
||||
_, data = entity5.last_call("turn_on")
|
||||
assert data == {"brightness": 255, "rgbww_color": (0, 0, 255, 0, 0)}
|
||||
_, data = entity7.last_call("turn_on")
|
||||
_, data = entity6.last_call("turn_on")
|
||||
assert data == {"brightness": 255, "color_temp_kelvin": 1739}
|
||||
|
||||
await hass.services.async_call(
|
||||
@@ -1595,7 +1405,6 @@ async def test_light_service_call_color_conversion(hass: HomeAssistant) -> None:
|
||||
entity4.entity_id,
|
||||
entity5.entity_id,
|
||||
entity6.entity_id,
|
||||
entity7.entity_id,
|
||||
],
|
||||
"brightness_pct": 100,
|
||||
"hs_color": (240, 0),
|
||||
@@ -1611,13 +1420,11 @@ async def test_light_service_call_color_conversion(hass: HomeAssistant) -> None:
|
||||
_, data = entity3.last_call("turn_on")
|
||||
assert data == {"brightness": 255, "hs_color": (240.0, 0.0)}
|
||||
_, data = entity4.last_call("turn_on")
|
||||
assert data == {"brightness": 255, "hs_color": (240.0, 0.0)}
|
||||
_, data = entity5.last_call("turn_on")
|
||||
assert data == {"brightness": 255, "rgbw_color": (0, 0, 0, 255)}
|
||||
_, data = entity6.last_call("turn_on")
|
||||
_, data = entity5.last_call("turn_on")
|
||||
# The midpoint of the white channels is warm, compensated by adding green + blue
|
||||
assert data == {"brightness": 255, "rgbww_color": (0, 76, 141, 255, 255)}
|
||||
_, data = entity7.last_call("turn_on")
|
||||
_, data = entity6.last_call("turn_on")
|
||||
assert data == {"brightness": 255, "color_temp_kelvin": 5962}
|
||||
|
||||
await hass.services.async_call(
|
||||
@@ -1632,7 +1439,6 @@ async def test_light_service_call_color_conversion(hass: HomeAssistant) -> None:
|
||||
entity4.entity_id,
|
||||
entity5.entity_id,
|
||||
entity6.entity_id,
|
||||
entity7.entity_id,
|
||||
],
|
||||
"brightness_pct": 50,
|
||||
"rgb_color": (128, 0, 0),
|
||||
@@ -1648,12 +1454,10 @@ async def test_light_service_call_color_conversion(hass: HomeAssistant) -> None:
|
||||
_, data = entity3.last_call("turn_on")
|
||||
assert data == {"brightness": 128, "rgb_color": (128, 0, 0)}
|
||||
_, data = entity4.last_call("turn_on")
|
||||
assert data == {"brightness": 128, "hs_color": (0.0, 100.0)}
|
||||
_, data = entity5.last_call("turn_on")
|
||||
assert data == {"brightness": 128, "rgbw_color": (128, 0, 0, 0)}
|
||||
_, data = entity6.last_call("turn_on")
|
||||
_, data = entity5.last_call("turn_on")
|
||||
assert data == {"brightness": 128, "rgbww_color": (128, 0, 0, 0, 0)}
|
||||
_, data = entity7.last_call("turn_on")
|
||||
_, data = entity6.last_call("turn_on")
|
||||
assert data == {"brightness": 128, "color_temp_kelvin": 6279}
|
||||
|
||||
await hass.services.async_call(
|
||||
@@ -1668,7 +1472,6 @@ async def test_light_service_call_color_conversion(hass: HomeAssistant) -> None:
|
||||
entity4.entity_id,
|
||||
entity5.entity_id,
|
||||
entity6.entity_id,
|
||||
entity7.entity_id,
|
||||
],
|
||||
"brightness_pct": 50,
|
||||
"rgb_color": (255, 255, 255),
|
||||
@@ -1684,13 +1487,11 @@ async def test_light_service_call_color_conversion(hass: HomeAssistant) -> None:
|
||||
_, data = entity3.last_call("turn_on")
|
||||
assert data == {"brightness": 128, "rgb_color": (255, 255, 255)}
|
||||
_, data = entity4.last_call("turn_on")
|
||||
assert data == {"brightness": 128, "hs_color": (0.0, 0.0)}
|
||||
_, data = entity5.last_call("turn_on")
|
||||
assert data == {"brightness": 128, "rgbw_color": (0, 0, 0, 255)}
|
||||
_, data = entity6.last_call("turn_on")
|
||||
_, data = entity5.last_call("turn_on")
|
||||
# The midpoint the white channels is warm, compensated by adding green + blue
|
||||
assert data == {"brightness": 128, "rgbww_color": (0, 76, 141, 255, 255)}
|
||||
_, data = entity7.last_call("turn_on")
|
||||
_, data = entity6.last_call("turn_on")
|
||||
assert data == {"brightness": 128, "color_temp_kelvin": 5962}
|
||||
|
||||
await hass.services.async_call(
|
||||
@@ -1705,7 +1506,6 @@ async def test_light_service_call_color_conversion(hass: HomeAssistant) -> None:
|
||||
entity4.entity_id,
|
||||
entity5.entity_id,
|
||||
entity6.entity_id,
|
||||
entity7.entity_id,
|
||||
],
|
||||
"brightness_pct": 50,
|
||||
"xy_color": (0.1, 0.8),
|
||||
@@ -1721,12 +1521,10 @@ async def test_light_service_call_color_conversion(hass: HomeAssistant) -> None:
|
||||
_, data = entity3.last_call("turn_on")
|
||||
assert data == {"brightness": 128, "xy_color": (0.1, 0.8)}
|
||||
_, data = entity4.last_call("turn_on")
|
||||
assert data == {"brightness": 128, "hs_color": (125.176, 100.0)}
|
||||
_, data = entity5.last_call("turn_on")
|
||||
assert data == {"brightness": 128, "rgbw_color": (0, 255, 22, 0)}
|
||||
_, data = entity6.last_call("turn_on")
|
||||
_, data = entity5.last_call("turn_on")
|
||||
assert data == {"brightness": 128, "rgbww_color": (0, 255, 22, 0, 0)}
|
||||
_, data = entity7.last_call("turn_on")
|
||||
_, data = entity6.last_call("turn_on")
|
||||
assert data == {"brightness": 128, "color_temp_kelvin": 8645}
|
||||
|
||||
await hass.services.async_call(
|
||||
@@ -1741,7 +1539,6 @@ async def test_light_service_call_color_conversion(hass: HomeAssistant) -> None:
|
||||
entity4.entity_id,
|
||||
entity5.entity_id,
|
||||
entity6.entity_id,
|
||||
entity7.entity_id,
|
||||
],
|
||||
"brightness_pct": 50,
|
||||
"xy_color": (0.323, 0.329),
|
||||
@@ -1757,13 +1554,11 @@ async def test_light_service_call_color_conversion(hass: HomeAssistant) -> None:
|
||||
_, data = entity3.last_call("turn_on")
|
||||
assert data == {"brightness": 128, "xy_color": (0.323, 0.329)}
|
||||
_, data = entity4.last_call("turn_on")
|
||||
assert data == {"brightness": 128, "hs_color": (0.0, 0.392)}
|
||||
_, data = entity5.last_call("turn_on")
|
||||
assert data == {"brightness": 128, "rgbw_color": (1, 0, 0, 255)}
|
||||
_, data = entity6.last_call("turn_on")
|
||||
_, data = entity5.last_call("turn_on")
|
||||
# The midpoint the white channels is warm, compensated by adding green + blue
|
||||
assert data == {"brightness": 128, "rgbww_color": (0, 75, 140, 255, 255)}
|
||||
_, data = entity7.last_call("turn_on")
|
||||
_, data = entity6.last_call("turn_on")
|
||||
assert data == {"brightness": 128, "color_temp_kelvin": 5962}
|
||||
|
||||
await hass.services.async_call(
|
||||
@@ -1778,7 +1573,6 @@ async def test_light_service_call_color_conversion(hass: HomeAssistant) -> None:
|
||||
entity4.entity_id,
|
||||
entity5.entity_id,
|
||||
entity6.entity_id,
|
||||
entity7.entity_id,
|
||||
],
|
||||
"brightness_pct": 50,
|
||||
"rgbw_color": (128, 0, 0, 64),
|
||||
@@ -1794,13 +1588,11 @@ async def test_light_service_call_color_conversion(hass: HomeAssistant) -> None:
|
||||
_, data = entity3.last_call("turn_on")
|
||||
assert data == {"brightness": 128, "rgb_color": (128, 43, 43)}
|
||||
_, data = entity4.last_call("turn_on")
|
||||
assert data == {"brightness": 128, "hs_color": (0.0, 66.406)}
|
||||
_, data = entity5.last_call("turn_on")
|
||||
assert data == {"brightness": 128, "rgbw_color": (128, 0, 0, 64)}
|
||||
_, data = entity6.last_call("turn_on")
|
||||
_, data = entity5.last_call("turn_on")
|
||||
# The midpoint the white channels is warm, compensated by adding green + blue
|
||||
assert data == {"brightness": 128, "rgbww_color": (128, 0, 30, 117, 117)}
|
||||
_, data = entity7.last_call("turn_on")
|
||||
_, data = entity6.last_call("turn_on")
|
||||
assert data == {"brightness": 128, "color_temp_kelvin": 3011}
|
||||
|
||||
await hass.services.async_call(
|
||||
@@ -1815,7 +1607,6 @@ async def test_light_service_call_color_conversion(hass: HomeAssistant) -> None:
|
||||
entity4.entity_id,
|
||||
entity5.entity_id,
|
||||
entity6.entity_id,
|
||||
entity7.entity_id,
|
||||
],
|
||||
"brightness_pct": 50,
|
||||
"rgbw_color": (255, 255, 255, 255),
|
||||
@@ -1831,13 +1622,11 @@ async def test_light_service_call_color_conversion(hass: HomeAssistant) -> None:
|
||||
_, data = entity3.last_call("turn_on")
|
||||
assert data == {"brightness": 128, "rgb_color": (255, 255, 255)}
|
||||
_, data = entity4.last_call("turn_on")
|
||||
assert data == {"brightness": 128, "hs_color": (0.0, 0.0)}
|
||||
_, data = entity5.last_call("turn_on")
|
||||
assert data == {"brightness": 128, "rgbw_color": (255, 255, 255, 255)}
|
||||
_, data = entity6.last_call("turn_on")
|
||||
_, data = entity5.last_call("turn_on")
|
||||
# The midpoint the white channels is warm, compensated by adding green + blue
|
||||
assert data == {"brightness": 128, "rgbww_color": (0, 76, 141, 255, 255)}
|
||||
_, data = entity7.last_call("turn_on")
|
||||
_, data = entity6.last_call("turn_on")
|
||||
assert data == {"brightness": 128, "color_temp_kelvin": 5962}
|
||||
|
||||
await hass.services.async_call(
|
||||
@@ -1852,7 +1641,6 @@ async def test_light_service_call_color_conversion(hass: HomeAssistant) -> None:
|
||||
entity4.entity_id,
|
||||
entity5.entity_id,
|
||||
entity6.entity_id,
|
||||
entity7.entity_id,
|
||||
],
|
||||
"brightness_pct": 50,
|
||||
"rgbww_color": (128, 0, 0, 64, 32),
|
||||
@@ -1868,12 +1656,10 @@ async def test_light_service_call_color_conversion(hass: HomeAssistant) -> None:
|
||||
_, data = entity3.last_call("turn_on")
|
||||
assert data == {"brightness": 128, "rgb_color": (128, 33, 26)}
|
||||
_, data = entity4.last_call("turn_on")
|
||||
assert data == {"brightness": 128, "hs_color": (4.118, 79.688)}
|
||||
_, data = entity5.last_call("turn_on")
|
||||
assert data == {"brightness": 128, "rgbw_color": (128, 9, 0, 33)}
|
||||
_, data = entity6.last_call("turn_on")
|
||||
_, data = entity5.last_call("turn_on")
|
||||
assert data == {"brightness": 128, "rgbww_color": (128, 0, 0, 64, 32)}
|
||||
_, data = entity7.last_call("turn_on")
|
||||
_, data = entity6.last_call("turn_on")
|
||||
assert data == {"brightness": 128, "color_temp_kelvin": 3845}
|
||||
|
||||
await hass.services.async_call(
|
||||
@@ -1888,7 +1674,6 @@ async def test_light_service_call_color_conversion(hass: HomeAssistant) -> None:
|
||||
entity4.entity_id,
|
||||
entity5.entity_id,
|
||||
entity6.entity_id,
|
||||
entity7.entity_id,
|
||||
],
|
||||
"brightness_pct": 50,
|
||||
"rgbww_color": (255, 255, 255, 255, 255),
|
||||
@@ -1904,13 +1689,11 @@ async def test_light_service_call_color_conversion(hass: HomeAssistant) -> None:
|
||||
_, data = entity3.last_call("turn_on")
|
||||
assert data == {"brightness": 128, "rgb_color": (255, 217, 185)}
|
||||
_, data = entity4.last_call("turn_on")
|
||||
assert data == {"brightness": 128, "hs_color": (27.429, 27.451)}
|
||||
_, data = entity5.last_call("turn_on")
|
||||
# The midpoint the white channels is warm, compensated by decreasing green + blue
|
||||
assert data == {"brightness": 128, "rgbw_color": (96, 44, 0, 255)}
|
||||
_, data = entity6.last_call("turn_on")
|
||||
_, data = entity5.last_call("turn_on")
|
||||
assert data == {"brightness": 128, "rgbww_color": (255, 255, 255, 255, 255)}
|
||||
_, data = entity7.last_call("turn_on")
|
||||
_, data = entity6.last_call("turn_on")
|
||||
assert data == {"brightness": 128, "color_temp_kelvin": 3451}
|
||||
|
||||
|
||||
@@ -1923,7 +1706,6 @@ async def test_light_service_call_color_conversion_named_tuple(
|
||||
MockLight("Test_rgb", STATE_ON),
|
||||
MockLight("Test_xy", STATE_ON),
|
||||
MockLight("Test_all", STATE_ON),
|
||||
MockLight("Test_legacy", STATE_ON),
|
||||
MockLight("Test_rgbw", STATE_ON),
|
||||
MockLight("Test_rgbww", STATE_ON),
|
||||
]
|
||||
@@ -1946,16 +1728,10 @@ async def test_light_service_call_color_conversion_named_tuple(
|
||||
}
|
||||
|
||||
entity4 = entities[4]
|
||||
entity4.supported_features = light.SUPPORT_COLOR
|
||||
# Set color modes to none to trigger backwards compatibility in LightEntity
|
||||
entity4.supported_color_modes = None
|
||||
entity4.color_mode = None
|
||||
entity4.supported_color_modes = {light.ColorMode.RGBW}
|
||||
|
||||
entity5 = entities[5]
|
||||
entity5.supported_color_modes = {light.ColorMode.RGBW}
|
||||
|
||||
entity6 = entities[6]
|
||||
entity6.supported_color_modes = {light.ColorMode.RGBWW}
|
||||
entity5.supported_color_modes = {light.ColorMode.RGBWW}
|
||||
|
||||
assert await async_setup_component(hass, "light", {"light": {"platform": "test"}})
|
||||
await hass.async_block_till_done()
|
||||
@@ -1971,7 +1747,6 @@ async def test_light_service_call_color_conversion_named_tuple(
|
||||
entity3.entity_id,
|
||||
entity4.entity_id,
|
||||
entity5.entity_id,
|
||||
entity6.entity_id,
|
||||
],
|
||||
"brightness_pct": 25,
|
||||
"rgb_color": color_util.RGBColor(128, 0, 0),
|
||||
@@ -1987,10 +1762,8 @@ async def test_light_service_call_color_conversion_named_tuple(
|
||||
_, data = entity3.last_call("turn_on")
|
||||
assert data == {"brightness": 64, "rgb_color": (128, 0, 0)}
|
||||
_, data = entity4.last_call("turn_on")
|
||||
assert data == {"brightness": 64, "hs_color": (0.0, 100.0)}
|
||||
_, data = entity5.last_call("turn_on")
|
||||
assert data == {"brightness": 64, "rgbw_color": (128, 0, 0, 0)}
|
||||
_, data = entity6.last_call("turn_on")
|
||||
_, data = entity5.last_call("turn_on")
|
||||
assert data == {"brightness": 64, "rgbww_color": (128, 0, 0, 0, 0)}
|
||||
|
||||
|
||||
@@ -2327,7 +2100,6 @@ async def test_light_state_color_conversion(hass: HomeAssistant) -> None:
|
||||
MockLight("Test_hs", STATE_ON),
|
||||
MockLight("Test_rgb", STATE_ON),
|
||||
MockLight("Test_xy", STATE_ON),
|
||||
MockLight("Test_legacy", STATE_ON),
|
||||
]
|
||||
setup_test_component_platform(hass, light.DOMAIN, entities)
|
||||
|
||||
@@ -2352,13 +2124,6 @@ async def test_light_state_color_conversion(hass: HomeAssistant) -> None:
|
||||
entity2.rgb_color = "Invalid" # Should be ignored
|
||||
entity2.xy_color = (0.1, 0.8)
|
||||
|
||||
entity3 = entities[3]
|
||||
entity3.hs_color = (240, 100)
|
||||
entity3.supported_features = light.SUPPORT_COLOR
|
||||
# Set color modes to none to trigger backwards compatibility in LightEntity
|
||||
entity3.supported_color_modes = None
|
||||
entity3.color_mode = None
|
||||
|
||||
assert await async_setup_component(hass, "light", {"light": {"platform": "test"}})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
@@ -2380,12 +2145,6 @@ async def test_light_state_color_conversion(hass: HomeAssistant) -> None:
|
||||
assert state.attributes["rgb_color"] == (0, 255, 22)
|
||||
assert state.attributes["xy_color"] == (0.1, 0.8)
|
||||
|
||||
state = hass.states.get(entity3.entity_id)
|
||||
assert state.attributes["color_mode"] == light.ColorMode.HS
|
||||
assert state.attributes["hs_color"] == (240, 100)
|
||||
assert state.attributes["rgb_color"] == (0, 0, 255)
|
||||
assert state.attributes["xy_color"] == (0.136, 0.04)
|
||||
|
||||
|
||||
async def test_services_filter_parameters(
|
||||
hass: HomeAssistant,
|
||||
@@ -2620,31 +2379,6 @@ def test_filter_supported_color_modes() -> None:
|
||||
assert light.filter_supported_color_modes(supported) == {light.ColorMode.BRIGHTNESS}
|
||||
|
||||
|
||||
def test_deprecated_supported_features_ints(
|
||||
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
"""Test deprecated supported features ints."""
|
||||
|
||||
class MockLightEntityEntity(light.LightEntity):
|
||||
@property
|
||||
def supported_features(self) -> int:
|
||||
"""Return supported features."""
|
||||
return 1
|
||||
|
||||
entity = MockLightEntityEntity()
|
||||
entity.hass = hass
|
||||
entity.platform = MockEntityPlatform(hass, domain="test", platform_name="test")
|
||||
assert entity.supported_features_compat is light.LightEntityFeature(1)
|
||||
assert "MockLightEntityEntity" in caplog.text
|
||||
assert "is using deprecated supported features values" in caplog.text
|
||||
assert "Instead it should use" in caplog.text
|
||||
assert "LightEntityFeature" in caplog.text
|
||||
assert "and color modes" in caplog.text
|
||||
caplog.clear()
|
||||
assert entity.supported_features_compat is light.LightEntityFeature(1)
|
||||
assert "is using deprecated supported features values" not in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("color_mode", "supported_color_modes", "warning_expected"),
|
||||
[
|
||||
@@ -2871,46 +2605,3 @@ def test_missing_kelvin_property_warnings(
|
||||
|
||||
assert state.attributes[light.ATTR_MIN_COLOR_TEMP_KELVIN] == expected_values[0]
|
||||
assert state.attributes[light.ATTR_MAX_COLOR_TEMP_KELVIN] == expected_values[1]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"module",
|
||||
[light],
|
||||
)
|
||||
def test_all(module: ModuleType) -> None:
|
||||
"""Test module.__all__ is correctly set."""
|
||||
help_test_all(module)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("constant_name", "constant_value", "constant_replacement"),
|
||||
[
|
||||
("SUPPORT_BRIGHTNESS", 1, "supported_color_modes"),
|
||||
("SUPPORT_COLOR_TEMP", 2, "supported_color_modes"),
|
||||
("SUPPORT_COLOR", 16, "supported_color_modes"),
|
||||
],
|
||||
)
|
||||
def test_deprecated_light_constants(
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
constant_name: str,
|
||||
constant_value: int | str,
|
||||
constant_replacement: str,
|
||||
) -> None:
|
||||
"""Test deprecated light constants."""
|
||||
import_and_test_deprecated_constant(
|
||||
caplog, light, constant_name, constant_replacement, constant_value, "2026.1"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"entity_feature",
|
||||
list(light.LightEntityFeature),
|
||||
)
|
||||
def test_deprecated_support_light_constants_enums(
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
entity_feature: light.LightEntityFeature,
|
||||
) -> None:
|
||||
"""Test deprecated support light constants."""
|
||||
import_and_test_deprecated_constant_enum(
|
||||
caplog, light, entity_feature, "SUPPORT_", "2026.1"
|
||||
)
|
||||
|
||||
@@ -174,7 +174,9 @@ async def test_rgb_light(
|
||||
assert state.state == STATE_UNKNOWN
|
||||
color_modes = [light.ColorMode.HS]
|
||||
assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes
|
||||
expected_features = light.SUPPORT_FLASH | light.SUPPORT_TRANSITION
|
||||
expected_features = (
|
||||
light.LightEntityFeature.FLASH | light.LightEntityFeature.TRANSITION
|
||||
)
|
||||
assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == expected_features
|
||||
|
||||
|
||||
|
||||
177
tests/components/overkiz/test_entity.py
Normal file
177
tests/components/overkiz/test_entity.py
Normal file
@@ -0,0 +1,177 @@
|
||||
"""Tests for Overkiz entity."""
|
||||
|
||||
from unittest.mock import Mock
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.overkiz.entity import OverkizEntity
|
||||
|
||||
|
||||
def _create_mock_device(
|
||||
device_url: str, place_oid: str | None, label: str = "Device"
|
||||
) -> Mock:
|
||||
"""Create a mock device with the given properties."""
|
||||
device = Mock()
|
||||
device.device_url = device_url
|
||||
device.place_oid = place_oid
|
||||
device.label = label
|
||||
device.available = True
|
||||
device.states = []
|
||||
device.widget = Mock(value="TestWidget")
|
||||
device.controllable_name = "test:Component"
|
||||
return device
|
||||
|
||||
|
||||
def _create_mock_entity(device: Mock, all_devices: list[Mock]) -> Mock:
|
||||
"""Create a mock entity with the given device and coordinator data."""
|
||||
entity = Mock(spec=OverkizEntity)
|
||||
entity.device = device
|
||||
entity.device_url = device.device_url
|
||||
entity.base_device_url = device.device_url.split("#")[0]
|
||||
entity.coordinator = Mock()
|
||||
entity.coordinator.data = {d.device_url: d for d in all_devices}
|
||||
|
||||
prefix = f"{entity.base_device_url}#"
|
||||
entity._get_sibling_devices = lambda: [
|
||||
d
|
||||
for d in all_devices
|
||||
if d.device_url != device.device_url and d.device_url.startswith(prefix)
|
||||
]
|
||||
entity._get_device_index = lambda url: (
|
||||
int(url.split("#")[-1]) if url.split("#")[-1].isdigit() else None
|
||||
)
|
||||
return entity
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("place_oids", "expected"),
|
||||
[
|
||||
(["place-a", "place-b"], True),
|
||||
(["place-a", "place-a"], False),
|
||||
],
|
||||
ids=["different_place_oids", "same_place_oids"],
|
||||
)
|
||||
def test_has_siblings_with_different_place_oid(
|
||||
place_oids: list[str], expected: bool
|
||||
) -> None:
|
||||
"""Test detection of siblings with different placeOIDs."""
|
||||
devices = [
|
||||
_create_mock_device("io://gateway/123#1", place_oids[0], "Device 1"),
|
||||
_create_mock_device("io://gateway/123#2", place_oids[1], "Device 2"),
|
||||
]
|
||||
entity = _create_mock_entity(devices[0], devices)
|
||||
|
||||
result = OverkizEntity._has_siblings_with_different_place_oid(entity)
|
||||
|
||||
assert result is expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("device_index", "expected"),
|
||||
[
|
||||
(0, True),
|
||||
(1, False),
|
||||
],
|
||||
ids=["lowest_index_is_main", "higher_index_not_main"],
|
||||
)
|
||||
def test_is_main_device_for_place_oid(device_index: int, expected: bool) -> None:
|
||||
"""Test main device detection for placeOID group."""
|
||||
devices = [
|
||||
_create_mock_device("io://gateway/123#1", "place-a", "Device 1"),
|
||||
_create_mock_device("io://gateway/123#4", "place-a", "Device 4"),
|
||||
]
|
||||
entity = _create_mock_entity(devices[device_index], devices)
|
||||
|
||||
result = OverkizEntity._is_main_device_for_place_oid(entity)
|
||||
|
||||
assert result is expected
|
||||
|
||||
|
||||
def test_get_via_device_id_sub_device_links_to_main() -> None:
|
||||
"""Test sub-device links to main actuator with placeOID grouping."""
|
||||
devices = [
|
||||
_create_mock_device("io://gateway/123#1", "place-a", "Actuator"),
|
||||
_create_mock_device("io://gateway/123#2", "place-b", "Zone"),
|
||||
]
|
||||
entity = _create_mock_entity(devices[1], devices)
|
||||
entity.executor = Mock()
|
||||
entity.executor.get_gateway_id = Mock(return_value="gateway-id")
|
||||
|
||||
result = OverkizEntity._get_via_device_id(entity, use_place_oid_grouping=True)
|
||||
|
||||
assert result == "io://gateway/123#place-a"
|
||||
|
||||
|
||||
def test_get_via_device_id_main_device_links_to_gateway() -> None:
|
||||
"""Test main device (#1) links to gateway."""
|
||||
devices = [
|
||||
_create_mock_device("io://gateway/123#1", "place-a", "Actuator"),
|
||||
]
|
||||
entity = _create_mock_entity(devices[0], devices)
|
||||
entity.executor = Mock()
|
||||
entity.executor.get_gateway_id = Mock(return_value="gateway-id")
|
||||
|
||||
result = OverkizEntity._get_via_device_id(entity, use_place_oid_grouping=True)
|
||||
|
||||
assert result == "gateway-id"
|
||||
|
||||
|
||||
def test_has_siblings_with_no_place_oid() -> None:
|
||||
"""Test device with no placeOID returns False."""
|
||||
devices = [
|
||||
_create_mock_device("io://gateway/123#1", None, "Device 1"),
|
||||
_create_mock_device("io://gateway/123#2", "place-b", "Device 2"),
|
||||
]
|
||||
entity = _create_mock_entity(devices[0], devices)
|
||||
|
||||
result = OverkizEntity._has_siblings_with_different_place_oid(entity)
|
||||
|
||||
assert result is False
|
||||
|
||||
|
||||
def test_is_main_device_with_no_place_oid() -> None:
|
||||
"""Test device with no placeOID is always considered main."""
|
||||
devices = [
|
||||
_create_mock_device("io://gateway/123#2", None, "Device 2"),
|
||||
_create_mock_device("io://gateway/123#1", "place-a", "Device 1"),
|
||||
]
|
||||
entity = _create_mock_entity(devices[0], devices)
|
||||
|
||||
result = OverkizEntity._is_main_device_for_place_oid(entity)
|
||||
|
||||
assert result is True
|
||||
|
||||
|
||||
def test_get_via_device_id_main_device_without_place_oid() -> None:
|
||||
"""Test fallback to gateway when #1 device has no placeOID."""
|
||||
devices = [
|
||||
_create_mock_device("io://gateway/123#1", None, "Actuator"),
|
||||
_create_mock_device("io://gateway/123#2", "place-b", "Zone"),
|
||||
]
|
||||
entity = _create_mock_entity(devices[1], devices)
|
||||
entity.executor = Mock()
|
||||
entity.executor.get_gateway_id = Mock(return_value="gateway-id")
|
||||
|
||||
result = OverkizEntity._get_via_device_id(entity, use_place_oid_grouping=True)
|
||||
|
||||
assert result == "gateway-id"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("device_url", "expected"),
|
||||
[
|
||||
("io://gateway/123#4", 4),
|
||||
("io://gateway/123#10", 10),
|
||||
("io://gateway/123#abc", None),
|
||||
("io://gateway/123#", None),
|
||||
],
|
||||
ids=["single_digit", "multi_digit", "non_numeric", "empty_suffix"],
|
||||
)
|
||||
def test_get_device_index(device_url: str, expected: int | None) -> None:
|
||||
"""Test extracting numeric index from device URL."""
|
||||
device = _create_mock_device(device_url, "place-a")
|
||||
entity = _create_mock_entity(device, [device])
|
||||
|
||||
result = OverkizEntity._get_device_index(entity, device_url)
|
||||
|
||||
assert result == expected
|
||||
1
tests/components/redgtech/__init__.py
Normal file
1
tests/components/redgtech/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Tests for the Redgtech component."""
|
||||
70
tests/components/redgtech/conftest.py
Normal file
70
tests/components/redgtech/conftest.py
Normal file
@@ -0,0 +1,70 @@
|
||||
"""Test fixtures for Redgtech integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Generator
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.redgtech.const import DOMAIN
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
TEST_EMAIL = "test@example.com"
|
||||
TEST_PASSWORD = "test_password"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_redgtech_api() -> Generator[MagicMock]:
|
||||
"""Return a mocked Redgtech API client."""
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.redgtech.coordinator.RedgtechAPI", autospec=True
|
||||
) as api_mock,
|
||||
patch(
|
||||
"homeassistant.components.redgtech.config_flow.RedgtechAPI",
|
||||
new=api_mock,
|
||||
),
|
||||
):
|
||||
api = api_mock.return_value
|
||||
|
||||
api.login = AsyncMock(return_value="mock_access_token")
|
||||
api.get_data = AsyncMock(
|
||||
return_value={
|
||||
"boards": [
|
||||
{
|
||||
"endpointId": "switch_001",
|
||||
"friendlyName": "Living Room Switch",
|
||||
"value": False,
|
||||
"displayCategories": ["SWITCH"],
|
||||
},
|
||||
{
|
||||
"endpointId": "switch_002",
|
||||
"friendlyName": "Kitchen Switch",
|
||||
"value": True,
|
||||
"displayCategories": ["SWITCH"],
|
||||
},
|
||||
{
|
||||
"endpointId": "light_switch_001",
|
||||
"friendlyName": "Bedroom Light Switch",
|
||||
"value": False,
|
||||
"displayCategories": ["LIGHT", "SWITCH"],
|
||||
},
|
||||
]
|
||||
}
|
||||
)
|
||||
api.set_switch_state = AsyncMock()
|
||||
|
||||
yield api
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_config_entry() -> MockConfigEntry:
|
||||
"""Mock config entry."""
|
||||
return MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data={"email": TEST_EMAIL, "password": TEST_PASSWORD},
|
||||
title="Mock Title",
|
||||
entry_id="test_entry",
|
||||
)
|
||||
99
tests/components/redgtech/snapshots/test_switch.ambr
Normal file
99
tests/components/redgtech/snapshots/test_switch.ambr
Normal file
@@ -0,0 +1,99 @@
|
||||
# serializer version: 1
|
||||
# name: test_entities[switch.kitchen_switch-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'switch',
|
||||
'entity_category': None,
|
||||
'entity_id': 'switch.kitchen_switch',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': None,
|
||||
'platform': 'redgtech',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': None,
|
||||
'unique_id': 'switch_002',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_entities[switch.kitchen_switch-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Kitchen Switch',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'switch.kitchen_switch',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'on',
|
||||
})
|
||||
# ---
|
||||
# name: test_entities[switch.living_room_switch-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'switch',
|
||||
'entity_category': None,
|
||||
'entity_id': 'switch.living_room_switch',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': None,
|
||||
'platform': 'redgtech',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': None,
|
||||
'unique_id': 'switch_001',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_entities[switch.living_room_switch-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Living Room Switch',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'switch.living_room_switch',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'off',
|
||||
})
|
||||
# ---
|
||||
138
tests/components/redgtech/test_config_flow.py
Normal file
138
tests/components/redgtech/test_config_flow.py
Normal file
@@ -0,0 +1,138 @@
|
||||
"""Tests Config flow for the Redgtech integration."""
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
from redgtech_api.api import RedgtechAuthError, RedgtechConnectionError
|
||||
|
||||
from homeassistant.components.redgtech.const import DOMAIN
|
||||
from homeassistant.config_entries import SOURCE_USER
|
||||
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
TEST_EMAIL = "test@example.com"
|
||||
TEST_PASSWORD = "123456"
|
||||
FAKE_TOKEN = "fake_token"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("side_effect", "expected_error"),
|
||||
[
|
||||
(RedgtechAuthError, "invalid_auth"),
|
||||
(RedgtechConnectionError, "cannot_connect"),
|
||||
(Exception("Generic error"), "unknown"),
|
||||
],
|
||||
)
|
||||
async def test_user_step_errors(
|
||||
hass: HomeAssistant,
|
||||
mock_redgtech_api: MagicMock,
|
||||
side_effect: type[Exception],
|
||||
expected_error: str,
|
||||
) -> None:
|
||||
"""Test user step with various errors."""
|
||||
user_input = {CONF_EMAIL: TEST_EMAIL, CONF_PASSWORD: TEST_PASSWORD}
|
||||
mock_redgtech_api.login.side_effect = side_effect
|
||||
mock_redgtech_api.login.return_value = None
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}, data=user_input
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"]["base"] == expected_error
|
||||
mock_redgtech_api.login.assert_called_once_with(TEST_EMAIL, TEST_PASSWORD)
|
||||
|
||||
|
||||
async def test_user_step_creates_entry(
|
||||
hass: HomeAssistant,
|
||||
mock_redgtech_api: MagicMock,
|
||||
) -> None:
|
||||
"""Tests the correct creation of the entry in the configuration."""
|
||||
user_input = {CONF_EMAIL: TEST_EMAIL, CONF_PASSWORD: TEST_PASSWORD}
|
||||
mock_redgtech_api.login.reset_mock()
|
||||
mock_redgtech_api.login.return_value = FAKE_TOKEN
|
||||
mock_redgtech_api.login.side_effect = None
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}, data=user_input
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == TEST_EMAIL
|
||||
assert result["data"] == user_input
|
||||
# Verify login was called at least once with correct parameters
|
||||
mock_redgtech_api.login.assert_any_call(TEST_EMAIL, TEST_PASSWORD)
|
||||
|
||||
|
||||
async def test_user_step_duplicate_entry(
|
||||
hass: HomeAssistant,
|
||||
mock_redgtech_api: MagicMock,
|
||||
) -> None:
|
||||
"""Test attempt to add duplicate entry."""
|
||||
existing_entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
unique_id=TEST_EMAIL,
|
||||
data={CONF_EMAIL: TEST_EMAIL},
|
||||
)
|
||||
existing_entry.add_to_hass(hass)
|
||||
|
||||
user_input = {CONF_EMAIL: TEST_EMAIL, CONF_PASSWORD: TEST_PASSWORD}
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}, data=user_input
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
mock_redgtech_api.login.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("side_effect", "expected_error"),
|
||||
[
|
||||
(RedgtechAuthError, "invalid_auth"),
|
||||
(RedgtechConnectionError, "cannot_connect"),
|
||||
(Exception("Generic error"), "unknown"),
|
||||
],
|
||||
)
|
||||
async def test_user_step_error_recovery(
|
||||
hass: HomeAssistant,
|
||||
mock_redgtech_api: MagicMock,
|
||||
side_effect: Exception,
|
||||
expected_error: str,
|
||||
) -> None:
|
||||
"""Test that the flow can recover from errors and complete successfully."""
|
||||
user_input = {CONF_EMAIL: TEST_EMAIL, CONF_PASSWORD: TEST_PASSWORD}
|
||||
|
||||
# Reset mock to start fresh
|
||||
mock_redgtech_api.login.reset_mock()
|
||||
mock_redgtech_api.login.return_value = None
|
||||
mock_redgtech_api.login.side_effect = None
|
||||
|
||||
# First attempt fails with error
|
||||
mock_redgtech_api.login.side_effect = side_effect
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}, data=user_input
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"]["base"] == expected_error
|
||||
# Verify login was called at least once for the first attempt
|
||||
assert mock_redgtech_api.login.call_count >= 1
|
||||
first_call_count = mock_redgtech_api.login.call_count
|
||||
|
||||
# Second attempt succeeds - flow recovers
|
||||
mock_redgtech_api.login.side_effect = None
|
||||
mock_redgtech_api.login.return_value = FAKE_TOKEN
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input=user_input
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == TEST_EMAIL
|
||||
assert result["data"] == user_input
|
||||
# Verify login was called again for the second attempt (recovery)
|
||||
assert mock_redgtech_api.login.call_count > first_call_count
|
||||
255
tests/components/redgtech/test_switch.py
Normal file
255
tests/components/redgtech/test_switch.py
Normal file
@@ -0,0 +1,255 @@
|
||||
"""Tests for the Redgtech switch platform."""
|
||||
|
||||
from datetime import timedelta
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from freezegun import freeze_time
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
import pytest
|
||||
from redgtech_api.api import RedgtechAuthError, RedgtechConnectionError
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
SERVICE_TOGGLE,
|
||||
SERVICE_TURN_OFF,
|
||||
SERVICE_TURN_ON,
|
||||
STATE_UNAVAILABLE,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from tests.common import MockConfigEntry, snapshot_platform
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def freezer():
|
||||
"""Provide a freezer fixture that works with freeze_time decorator."""
|
||||
with freeze_time() as frozen_time:
|
||||
yield frozen_time
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def setup_redgtech_integration(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_redgtech_api: MagicMock,
|
||||
) -> MagicMock:
|
||||
"""Set up the Redgtech integration with mocked API."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
return mock_redgtech_api
|
||||
|
||||
|
||||
async def test_entities(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
snapshot: SnapshotAssertion,
|
||||
setup_redgtech_integration,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test entity setup."""
|
||||
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
|
||||
|
||||
|
||||
async def test_switch_turn_on(
|
||||
hass: HomeAssistant,
|
||||
setup_redgtech_integration: MagicMock,
|
||||
) -> None:
|
||||
"""Test turning a switch on."""
|
||||
mock_api = setup_redgtech_integration
|
||||
await hass.services.async_call(
|
||||
SWITCH_DOMAIN,
|
||||
SERVICE_TURN_ON,
|
||||
{ATTR_ENTITY_ID: "switch.living_room_switch"},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
mock_api.set_switch_state.assert_called_once_with(
|
||||
"switch_001", True, "mock_access_token"
|
||||
)
|
||||
|
||||
|
||||
async def test_switch_turn_off(
|
||||
hass: HomeAssistant,
|
||||
setup_redgtech_integration: MagicMock,
|
||||
) -> None:
|
||||
"""Test turning a switch off."""
|
||||
mock_api = setup_redgtech_integration
|
||||
await hass.services.async_call(
|
||||
SWITCH_DOMAIN,
|
||||
SERVICE_TURN_OFF,
|
||||
{ATTR_ENTITY_ID: "switch.kitchen_switch"},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
mock_api.set_switch_state.assert_called_once_with(
|
||||
"switch_002", False, "mock_access_token"
|
||||
)
|
||||
|
||||
|
||||
async def test_switch_toggle(
|
||||
hass: HomeAssistant,
|
||||
setup_redgtech_integration: MagicMock,
|
||||
) -> None:
|
||||
"""Test toggling a switch."""
|
||||
mock_api = setup_redgtech_integration
|
||||
await hass.services.async_call(
|
||||
SWITCH_DOMAIN,
|
||||
SERVICE_TOGGLE,
|
||||
{ATTR_ENTITY_ID: "switch.living_room_switch"},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
mock_api.set_switch_state.assert_called_once_with(
|
||||
"switch_001", True, "mock_access_token"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("exception", "error_message"),
|
||||
[
|
||||
(
|
||||
RedgtechConnectionError("Connection failed"),
|
||||
"Connection error with Redgtech API",
|
||||
),
|
||||
(
|
||||
RedgtechAuthError("Auth failed"),
|
||||
"Authentication failed when controlling Redgtech switch",
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_exception_handling(
|
||||
hass: HomeAssistant,
|
||||
setup_redgtech_integration: MagicMock,
|
||||
exception: Exception,
|
||||
error_message: str,
|
||||
) -> None:
|
||||
"""Test exception handling when controlling switches."""
|
||||
mock_api = setup_redgtech_integration
|
||||
mock_api.set_switch_state.side_effect = exception
|
||||
|
||||
with pytest.raises(HomeAssistantError, match=error_message):
|
||||
await hass.services.async_call(
|
||||
SWITCH_DOMAIN,
|
||||
SERVICE_TURN_ON,
|
||||
{ATTR_ENTITY_ID: "switch.living_room_switch"},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
|
||||
async def test_switch_auth_error_with_retry(
|
||||
hass: HomeAssistant,
|
||||
setup_redgtech_integration: MagicMock,
|
||||
) -> None:
|
||||
"""Test handling auth errors with token renewal."""
|
||||
mock_api = setup_redgtech_integration
|
||||
# Mock fails with auth error
|
||||
mock_api.set_switch_state.side_effect = RedgtechAuthError("Auth failed")
|
||||
|
||||
# Expect HomeAssistantError to be raised
|
||||
with pytest.raises(
|
||||
HomeAssistantError,
|
||||
match="Authentication failed when controlling Redgtech switch",
|
||||
):
|
||||
await hass.services.async_call(
|
||||
SWITCH_DOMAIN,
|
||||
SERVICE_TURN_ON,
|
||||
{ATTR_ENTITY_ID: "switch.living_room_switch"},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
|
||||
@freeze_time("2023-01-01 12:00:00")
|
||||
async def test_coordinator_data_update_success(
|
||||
hass: HomeAssistant,
|
||||
setup_redgtech_integration: MagicMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
) -> None:
|
||||
"""Test successful data update through coordinator."""
|
||||
mock_api = setup_redgtech_integration
|
||||
|
||||
# Update mock data
|
||||
mock_api.get_data.return_value = {
|
||||
"boards": [
|
||||
{
|
||||
"endpointId": "switch_001",
|
||||
"friendlyName": "Living Room Switch",
|
||||
"value": True, # Changed to True
|
||||
"displayCategories": ["SWITCH"],
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
# Use freezer to advance time and trigger update
|
||||
freezer.tick(delta=timedelta(minutes=2))
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Verify the entity state was updated successfully
|
||||
living_room_state = hass.states.get("switch.living_room_switch")
|
||||
assert living_room_state is not None
|
||||
assert living_room_state.state == "on"
|
||||
|
||||
|
||||
@freeze_time("2023-01-01 12:00:00")
|
||||
async def test_coordinator_connection_error_during_update(
|
||||
hass: HomeAssistant,
|
||||
setup_redgtech_integration: MagicMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
) -> None:
|
||||
"""Test coordinator handling connection errors during data updates."""
|
||||
mock_api = setup_redgtech_integration
|
||||
mock_api.get_data.side_effect = RedgtechConnectionError("Connection failed")
|
||||
|
||||
# Use freezer to advance time and trigger update
|
||||
freezer.tick(delta=timedelta(minutes=2))
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Verify entities become unavailable due to coordinator error
|
||||
living_room_state = hass.states.get("switch.living_room_switch")
|
||||
kitchen_state = hass.states.get("switch.kitchen_switch")
|
||||
|
||||
assert living_room_state.state == STATE_UNAVAILABLE
|
||||
assert kitchen_state.state == STATE_UNAVAILABLE
|
||||
|
||||
|
||||
@freeze_time("2023-01-01 12:00:00")
|
||||
async def test_coordinator_auth_error_with_token_renewal(
|
||||
hass: HomeAssistant,
|
||||
setup_redgtech_integration: MagicMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
) -> None:
|
||||
"""Test coordinator handling auth errors with token renewal."""
|
||||
mock_api = setup_redgtech_integration
|
||||
|
||||
# First call fails with auth error, second succeeds after token renewal
|
||||
mock_api.get_data.side_effect = [
|
||||
RedgtechAuthError("Auth failed"),
|
||||
{
|
||||
"boards": [
|
||||
{
|
||||
"endpointId": "switch_001",
|
||||
"friendlyName": "Living Room Switch",
|
||||
"value": True,
|
||||
"displayCategories": ["SWITCH"],
|
||||
}
|
||||
]
|
||||
},
|
||||
]
|
||||
|
||||
# Use freezer to advance time and trigger update
|
||||
freezer.tick(delta=timedelta(minutes=2))
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Verify token renewal was attempted
|
||||
assert mock_api.login.call_count >= 2
|
||||
# Verify entity is available after successful token renewal
|
||||
living_room_state = hass.states.get("switch.living_room_switch")
|
||||
assert living_room_state is not None
|
||||
assert living_room_state.state != STATE_UNAVAILABLE
|
||||
Reference in New Issue
Block a user