mirror of
https://github.com/home-assistant/core.git
synced 2026-02-05 14:55:35 +01:00
Compare commits
113 Commits
edenhaus-b
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d37e958a0b | ||
|
|
0498ac7364 | ||
|
|
67bdeb9945 | ||
|
|
a227307387 | ||
|
|
0e0309cabf | ||
|
|
fd2dfc83c6 | ||
|
|
9e736891c4 | ||
|
|
fbabf0dcb8 | ||
|
|
7128791152 | ||
|
|
94456b5bc3 | ||
|
|
2105c6b177 | ||
|
|
34156f79e8 | ||
|
|
bb1a2530f5 | ||
|
|
06613746f9 | ||
|
|
98ca948afe | ||
|
|
fa58fe5f4e | ||
|
|
46f230c487 | ||
|
|
13a987aba3 | ||
|
|
9cef323581 | ||
|
|
7ea7576188 | ||
|
|
f8abbfd42b | ||
|
|
5cd1821bc9 | ||
|
|
2ef7f26ffb | ||
|
|
184bea49e2 | ||
|
|
c853fb2068 | ||
|
|
79e0a93e48 | ||
|
|
3867c1d7d1 | ||
|
|
b9b6b050cc | ||
|
|
d960736b3d | ||
|
|
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 |
@@ -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
|
||||
|
||||
@@ -99,7 +99,7 @@ class AbodeLight(AbodeDevice, LightEntity):
|
||||
return _hs
|
||||
|
||||
@property
|
||||
def color_mode(self) -> ColorMode | None:
|
||||
def color_mode(self) -> ColorMode:
|
||||
"""Return the color mode of the light."""
|
||||
if self._device.is_dimmable and self._device.is_color_capable:
|
||||
if self.hs_color is not None:
|
||||
@@ -110,7 +110,7 @@ class AbodeLight(AbodeDevice, LightEntity):
|
||||
return ColorMode.ONOFF
|
||||
|
||||
@property
|
||||
def supported_color_modes(self) -> set[ColorMode] | None:
|
||||
def supported_color_modes(self) -> set[ColorMode]:
|
||||
"""Flag supported color modes."""
|
||||
if self._device.is_dimmable and self._device.is_color_capable:
|
||||
return {ColorMode.COLOR_TEMP, ColorMode.HS}
|
||||
|
||||
@@ -90,6 +90,9 @@
|
||||
"cannot_retrieve_data_with_error": {
|
||||
"message": "Error retrieving data: {error}"
|
||||
},
|
||||
"config_entry_not_found": {
|
||||
"message": "Config entry not found: {device_id}"
|
||||
},
|
||||
"device_serial_number_missing": {
|
||||
"message": "Device serial number missing: {device_id}"
|
||||
},
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -196,44 +196,46 @@ class R2BackupAgent(BackupAgent):
|
||||
)
|
||||
upload_id = multipart_upload["UploadId"]
|
||||
try:
|
||||
parts = []
|
||||
parts: list[dict[str, Any]] = []
|
||||
part_number = 1
|
||||
buffer_size = 0 # bytes
|
||||
buffer: list[bytes] = []
|
||||
buffer = bytearray() # bytes buffer to store the data
|
||||
|
||||
stream = await open_stream()
|
||||
async for chunk in stream:
|
||||
buffer_size += len(chunk)
|
||||
buffer.append(chunk)
|
||||
buffer.extend(chunk)
|
||||
|
||||
# upload parts of exactly MULTIPART_MIN_PART_SIZE_BYTES to ensure
|
||||
# all non-trailing parts have the same size (required by S3/R2)
|
||||
while len(buffer) >= MULTIPART_MIN_PART_SIZE_BYTES:
|
||||
part_data = bytes(buffer[:MULTIPART_MIN_PART_SIZE_BYTES])
|
||||
del buffer[:MULTIPART_MIN_PART_SIZE_BYTES]
|
||||
|
||||
# If buffer size meets minimum part size, upload it as a part
|
||||
if buffer_size >= MULTIPART_MIN_PART_SIZE_BYTES:
|
||||
_LOGGER.debug(
|
||||
"Uploading part number %d, size %d", part_number, buffer_size
|
||||
"Uploading part number %d, size %d",
|
||||
part_number,
|
||||
len(part_data),
|
||||
)
|
||||
part = await self._client.upload_part(
|
||||
Bucket=self._bucket,
|
||||
Key=self._with_prefix(tar_filename),
|
||||
PartNumber=part_number,
|
||||
UploadId=upload_id,
|
||||
Body=b"".join(buffer),
|
||||
Body=part_data,
|
||||
)
|
||||
parts.append({"PartNumber": part_number, "ETag": part["ETag"]})
|
||||
part_number += 1
|
||||
buffer_size = 0
|
||||
buffer = []
|
||||
|
||||
# Upload the final buffer as the last part (no minimum size requirement)
|
||||
if buffer:
|
||||
_LOGGER.debug(
|
||||
"Uploading final part number %d, size %d", part_number, buffer_size
|
||||
"Uploading final part number %d, size %d", part_number, len(buffer)
|
||||
)
|
||||
part = await self._client.upload_part(
|
||||
Bucket=self._bucket,
|
||||
Key=self._with_prefix(tar_filename),
|
||||
PartNumber=part_number,
|
||||
UploadId=upload_id,
|
||||
Body=b"".join(buffer),
|
||||
Body=bytes(buffer),
|
||||
)
|
||||
parts.append({"PartNumber": part_number, "ETag": part["ETag"]})
|
||||
|
||||
|
||||
@@ -46,7 +46,6 @@ async def async_setup_entry(
|
||||
async_add_entities(
|
||||
[
|
||||
DemoLight(
|
||||
available=True,
|
||||
effect_list=LIGHT_EFFECT_LIST,
|
||||
effect=LIGHT_EFFECT_LIST[0],
|
||||
translation_key="bed_light",
|
||||
@@ -55,21 +54,18 @@ async def async_setup_entry(
|
||||
unique_id="light_1",
|
||||
),
|
||||
DemoLight(
|
||||
available=True,
|
||||
ct=LIGHT_TEMPS[1],
|
||||
device_name="Ceiling Lights",
|
||||
state=True,
|
||||
unique_id="light_2",
|
||||
),
|
||||
DemoLight(
|
||||
available=True,
|
||||
hs_color=LIGHT_COLORS[1],
|
||||
device_name="Kitchen Lights",
|
||||
state=True,
|
||||
unique_id="light_3",
|
||||
),
|
||||
DemoLight(
|
||||
available=True,
|
||||
ct=LIGHT_TEMPS[1],
|
||||
device_name="Office RGBW Lights",
|
||||
rgbw_color=(255, 0, 0, 255),
|
||||
@@ -78,7 +74,6 @@ async def async_setup_entry(
|
||||
unique_id="light_4",
|
||||
),
|
||||
DemoLight(
|
||||
available=True,
|
||||
device_name="Living Room RGBWW Lights",
|
||||
rgbww_color=(255, 0, 0, 255, 0),
|
||||
state=True,
|
||||
@@ -86,7 +81,6 @@ async def async_setup_entry(
|
||||
unique_id="light_5",
|
||||
),
|
||||
DemoLight(
|
||||
available=True,
|
||||
device_name="Entrance Color + White Lights",
|
||||
hs_color=LIGHT_COLORS[1],
|
||||
state=True,
|
||||
@@ -112,7 +106,6 @@ class DemoLight(LightEntity):
|
||||
unique_id: str,
|
||||
device_name: str,
|
||||
state: bool,
|
||||
available: bool = False,
|
||||
brightness: int = 180,
|
||||
ct: int | None = None,
|
||||
effect_list: list[str] | None = None,
|
||||
@@ -125,128 +118,72 @@ class DemoLight(LightEntity):
|
||||
) -> None:
|
||||
"""Initialize the light."""
|
||||
self._attr_translation_key = translation_key
|
||||
self._available = True
|
||||
self._brightness = brightness
|
||||
self._ct = ct or random.choice(LIGHT_TEMPS)
|
||||
self._effect = effect
|
||||
self._effect_list = effect_list
|
||||
self._hs_color = hs_color
|
||||
self._rgbw_color = rgbw_color
|
||||
self._rgbww_color = rgbww_color
|
||||
self._state = state
|
||||
self._unique_id = unique_id
|
||||
self._attr_brightness = brightness
|
||||
self._attr_color_temp_kelvin = ct or random.choice(LIGHT_TEMPS)
|
||||
self._attr_effect = effect
|
||||
self._attr_effect_list = effect_list
|
||||
self._attr_hs_color = hs_color
|
||||
self._attr_rgbw_color = rgbw_color
|
||||
self._attr_rgbww_color = rgbww_color
|
||||
self._attr_is_on = state
|
||||
self._attr_unique_id = unique_id
|
||||
if hs_color:
|
||||
self._color_mode = ColorMode.HS
|
||||
self._attr_color_mode = ColorMode.HS
|
||||
elif rgbw_color:
|
||||
self._color_mode = ColorMode.RGBW
|
||||
self._attr_color_mode = ColorMode.RGBW
|
||||
elif rgbww_color:
|
||||
self._color_mode = ColorMode.RGBWW
|
||||
self._attr_color_mode = ColorMode.RGBWW
|
||||
else:
|
||||
self._color_mode = ColorMode.COLOR_TEMP
|
||||
self._attr_color_mode = ColorMode.COLOR_TEMP
|
||||
if not supported_color_modes:
|
||||
supported_color_modes = SUPPORT_DEMO
|
||||
self._color_modes = supported_color_modes
|
||||
if self._effect_list is not None:
|
||||
self._attr_supported_color_modes = supported_color_modes
|
||||
if self._attr_effect_list is not None:
|
||||
self._attr_supported_features |= LightEntityFeature.EFFECT
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={
|
||||
# Serial numbers are unique identifiers within a specific domain
|
||||
(DOMAIN, self.unique_id)
|
||||
(DOMAIN, unique_id)
|
||||
},
|
||||
name=device_name,
|
||||
)
|
||||
|
||||
@property
|
||||
def unique_id(self) -> str:
|
||||
"""Return unique ID for light."""
|
||||
return self._unique_id
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return availability."""
|
||||
# This demo light is always available, but well-behaving components
|
||||
# should implement this to inform Home Assistant accordingly.
|
||||
return self._available
|
||||
|
||||
@property
|
||||
def brightness(self) -> int:
|
||||
"""Return the brightness of this light between 0..255."""
|
||||
return self._brightness
|
||||
|
||||
@property
|
||||
def color_mode(self) -> ColorMode | None:
|
||||
"""Return the color mode of the light."""
|
||||
return self._color_mode
|
||||
|
||||
@property
|
||||
def hs_color(self) -> tuple[int, int] | None:
|
||||
"""Return the hs color value."""
|
||||
return self._hs_color
|
||||
|
||||
@property
|
||||
def rgbw_color(self) -> tuple[int, int, int, int] | None:
|
||||
"""Return the rgbw color value."""
|
||||
return self._rgbw_color
|
||||
|
||||
@property
|
||||
def rgbww_color(self) -> tuple[int, int, int, int, int] | None:
|
||||
"""Return the rgbww color value."""
|
||||
return self._rgbww_color
|
||||
|
||||
@property
|
||||
def color_temp_kelvin(self) -> int | None:
|
||||
"""Return the color temperature value in Kelvin."""
|
||||
return self._ct
|
||||
|
||||
@property
|
||||
def effect_list(self) -> list[str] | None:
|
||||
"""Return the list of supported effects."""
|
||||
return self._effect_list
|
||||
|
||||
@property
|
||||
def effect(self) -> str | None:
|
||||
"""Return the current effect."""
|
||||
return self._effect
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return true if light is on."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def supported_color_modes(self) -> set[ColorMode]:
|
||||
"""Flag supported color modes."""
|
||||
return self._color_modes
|
||||
return True
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the light on."""
|
||||
self._state = True
|
||||
self._attr_is_on = True
|
||||
|
||||
if ATTR_BRIGHTNESS in kwargs:
|
||||
self._brightness = kwargs[ATTR_BRIGHTNESS]
|
||||
self._attr_brightness = kwargs[ATTR_BRIGHTNESS]
|
||||
|
||||
if ATTR_COLOR_TEMP_KELVIN in kwargs:
|
||||
self._color_mode = ColorMode.COLOR_TEMP
|
||||
self._ct = kwargs[ATTR_COLOR_TEMP_KELVIN]
|
||||
self._attr_color_mode = ColorMode.COLOR_TEMP
|
||||
self._attr_color_temp_kelvin = kwargs[ATTR_COLOR_TEMP_KELVIN]
|
||||
|
||||
if ATTR_EFFECT in kwargs:
|
||||
self._effect = kwargs[ATTR_EFFECT]
|
||||
self._attr_effect = kwargs[ATTR_EFFECT]
|
||||
|
||||
if ATTR_HS_COLOR in kwargs:
|
||||
self._color_mode = ColorMode.HS
|
||||
self._hs_color = kwargs[ATTR_HS_COLOR]
|
||||
self._attr_color_mode = ColorMode.HS
|
||||
self._attr_hs_color = kwargs[ATTR_HS_COLOR]
|
||||
|
||||
if ATTR_RGBW_COLOR in kwargs:
|
||||
self._color_mode = ColorMode.RGBW
|
||||
self._rgbw_color = kwargs[ATTR_RGBW_COLOR]
|
||||
self._attr_color_mode = ColorMode.RGBW
|
||||
self._attr_rgbw_color = kwargs[ATTR_RGBW_COLOR]
|
||||
|
||||
if ATTR_RGBWW_COLOR in kwargs:
|
||||
self._color_mode = ColorMode.RGBWW
|
||||
self._rgbww_color = kwargs[ATTR_RGBWW_COLOR]
|
||||
self._attr_color_mode = ColorMode.RGBWW
|
||||
self._attr_rgbww_color = kwargs[ATTR_RGBWW_COLOR]
|
||||
|
||||
if ATTR_WHITE in kwargs:
|
||||
self._color_mode = ColorMode.WHITE
|
||||
self._brightness = kwargs[ATTR_WHITE]
|
||||
self._attr_color_mode = ColorMode.WHITE
|
||||
self._attr_brightness = kwargs[ATTR_WHITE]
|
||||
|
||||
# As we have disabled polling, we need to inform
|
||||
# Home Assistant about updates in our state ourselves.
|
||||
@@ -254,7 +191,7 @@ class DemoLight(LightEntity):
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn the light off."""
|
||||
self._state = False
|
||||
self._attr_is_on = False
|
||||
|
||||
# As we have disabled polling, we need to inform
|
||||
# Home Assistant about updates in our state ourselves.
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["denonavr"],
|
||||
"requirements": ["denonavr==1.3.1"],
|
||||
"requirements": ["denonavr==1.3.2"],
|
||||
"ssdp": [
|
||||
{
|
||||
"deviceType": "urn:schemas-upnp-org:device:MediaRenderer:1",
|
||||
|
||||
@@ -17,6 +17,7 @@ from denonavr.const import (
|
||||
STATE_ON,
|
||||
STATE_PAUSED,
|
||||
STATE_PLAYING,
|
||||
STATE_STOPPED,
|
||||
)
|
||||
from denonavr.exceptions import (
|
||||
AvrCommandError,
|
||||
@@ -69,6 +70,7 @@ SUPPORT_MEDIA_MODES = (
|
||||
| MediaPlayerEntityFeature.NEXT_TRACK
|
||||
| MediaPlayerEntityFeature.VOLUME_SET
|
||||
| MediaPlayerEntityFeature.PLAY
|
||||
| MediaPlayerEntityFeature.STOP
|
||||
)
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=10)
|
||||
@@ -96,6 +98,7 @@ DENON_STATE_MAPPING = {
|
||||
STATE_OFF: MediaPlayerState.OFF,
|
||||
STATE_PLAYING: MediaPlayerState.PLAYING,
|
||||
STATE_PAUSED: MediaPlayerState.PAUSED,
|
||||
STATE_STOPPED: MediaPlayerState.IDLE,
|
||||
}
|
||||
|
||||
|
||||
@@ -404,6 +407,11 @@ class DenonDevice(MediaPlayerEntity):
|
||||
"""Send pause command."""
|
||||
await self._receiver.async_pause()
|
||||
|
||||
@async_log_errors
|
||||
async def async_media_stop(self) -> None:
|
||||
"""Send stop command."""
|
||||
await self._receiver.async_stop()
|
||||
|
||||
@async_log_errors
|
||||
async def async_media_previous_track(self) -> None:
|
||||
"""Send previous track command."""
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["google_air_quality_api"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["google_air_quality_api==3.0.0"]
|
||||
"requirements": ["google_air_quality_api==3.0.1"]
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["pyliebherrhomeapi"],
|
||||
"quality_scale": "bronze",
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["pyliebherrhomeapi==0.2.1"],
|
||||
"zeroconf": [
|
||||
{
|
||||
|
||||
@@ -34,7 +34,7 @@ rules:
|
||||
docs-installation-parameters: done
|
||||
entity-unavailable: done
|
||||
integration-owner: done
|
||||
log-when-unavailable: todo
|
||||
log-when-unavailable: done
|
||||
parallel-updates: done
|
||||
reauthentication-flow: done
|
||||
test-coverage: done
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -85,7 +85,7 @@ class NanoleafLight(NanoleafEntity, LightEntity):
|
||||
return self._nanoleaf.hue, self._nanoleaf.saturation
|
||||
|
||||
@property
|
||||
def color_mode(self) -> ColorMode | None:
|
||||
def color_mode(self) -> ColorMode:
|
||||
"""Return the color mode of the light."""
|
||||
# According to API docs, color mode is "ct", "effect" or "hs"
|
||||
# https://forum.nanoleaf.me/docs/openapi#_4qgqrz96f44d
|
||||
|
||||
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)
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["pysmarlaapi", "pysignalr"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["pysmarlaapi==0.9.3"]
|
||||
"requirements": ["pysmarlaapi==0.13.0"]
|
||||
}
|
||||
|
||||
@@ -74,7 +74,7 @@ class SwitchbotLightEntity(SwitchbotEntity, LightEntity):
|
||||
return max(0, min(255, round(self._device.brightness * 2.55)))
|
||||
|
||||
@property
|
||||
def color_mode(self) -> ColorMode | None:
|
||||
def color_mode(self) -> ColorMode:
|
||||
"""Return the color mode of the light."""
|
||||
return SWITCHBOT_COLOR_MODE_TO_HASS.get(
|
||||
self._device.color_mode, ColorMode.UNKNOWN
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""The syncthing integration."""
|
||||
|
||||
import asyncio
|
||||
from asyncio import Task
|
||||
import logging
|
||||
|
||||
import aiosyncthing
|
||||
@@ -13,7 +14,7 @@ from homeassistant.const import (
|
||||
EVENT_HOMEASSISTANT_STOP,
|
||||
Platform,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.core import Event, HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
|
||||
@@ -57,7 +58,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
async def cancel_listen_task(_):
|
||||
async def cancel_listen_task(event: Event) -> None:
|
||||
"""Cancel the listen task on Home Assistant stop."""
|
||||
await syncthing.unsubscribe()
|
||||
|
||||
entry.async_on_unload(
|
||||
@@ -80,44 +82,46 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
class SyncthingClient:
|
||||
"""A Syncthing client."""
|
||||
|
||||
def __init__(self, hass, client, server_id):
|
||||
def __init__(
|
||||
self, hass: HomeAssistant, client: aiosyncthing.Syncthing, server_id: str
|
||||
) -> None:
|
||||
"""Initialize the client."""
|
||||
self._hass = hass
|
||||
self._client = client
|
||||
self._server_id = server_id
|
||||
self._listen_task = None
|
||||
self._listen_task: Task[None] | None = None
|
||||
|
||||
@property
|
||||
def server_id(self):
|
||||
def server_id(self) -> str:
|
||||
"""Get server id."""
|
||||
return self._server_id
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
def url(self) -> str:
|
||||
"""Get server URL."""
|
||||
return self._client.url
|
||||
|
||||
@property
|
||||
def database(self):
|
||||
def database(self) -> aiosyncthing.Database:
|
||||
"""Get database namespace client."""
|
||||
return self._client.database
|
||||
|
||||
@property
|
||||
def system(self):
|
||||
def system(self) -> aiosyncthing.System:
|
||||
"""Get system namespace client."""
|
||||
return self._client.system
|
||||
|
||||
def subscribe(self):
|
||||
def subscribe(self) -> None:
|
||||
"""Start event listener coroutine."""
|
||||
self._listen_task = asyncio.create_task(self._listen())
|
||||
|
||||
async def unsubscribe(self):
|
||||
async def unsubscribe(self) -> None:
|
||||
"""Stop event listener coroutine."""
|
||||
if self._listen_task:
|
||||
self._listen_task.cancel()
|
||||
await self._client.close()
|
||||
|
||||
async def _listen(self):
|
||||
async def _listen(self) -> None:
|
||||
"""Listen to Syncthing events."""
|
||||
events = self._client.events
|
||||
server_was_unavailable = False
|
||||
@@ -142,11 +146,7 @@ class SyncthingClient:
|
||||
continue
|
||||
|
||||
signal_name = EVENTS[event["type"]]
|
||||
folder = None
|
||||
if "folder" in event["data"]:
|
||||
folder = event["data"]["folder"]
|
||||
else: # A workaround, some events store folder id under `id` key
|
||||
folder = event["data"]["id"]
|
||||
folder = event["data"].get("folder") or event["data"]["id"]
|
||||
async_dispatcher_send(
|
||||
self._hass,
|
||||
f"{signal_name}-{self._server_id}-{folder}",
|
||||
@@ -168,7 +168,8 @@ class SyncthingClient:
|
||||
server_was_unavailable = True
|
||||
continue
|
||||
|
||||
async def _server_available(self):
|
||||
async def _server_available(self) -> bool:
|
||||
"""Check if the Syncthing server is available."""
|
||||
try:
|
||||
await self._client.system.ping()
|
||||
except aiosyncthing.exceptions.SyncthingError:
|
||||
|
||||
@@ -21,7 +21,7 @@ DATA_SCHEMA = vol.Schema(
|
||||
)
|
||||
|
||||
|
||||
async def validate_input(hass: HomeAssistant, data):
|
||||
async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, str]:
|
||||
"""Validate the user input allows us to connect."""
|
||||
|
||||
try:
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
"""Support for monitoring the Syncthing instance."""
|
||||
"""Support for Syncthing sensors."""
|
||||
|
||||
from collections.abc import Mapping
|
||||
from typing import Any
|
||||
|
||||
import aiosyncthing
|
||||
|
||||
from homeassistant.components.sensor import SensorEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
|
||||
from homeassistant.exceptions import PlatformNotReady
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.event import async_track_time_interval
|
||||
|
||||
from . import SyncthingClient
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
FOLDER_PAUSED_RECEIVED,
|
||||
@@ -86,14 +90,21 @@ class FolderSensor(SensorEntity):
|
||||
"stateChanged": "state_changed",
|
||||
}
|
||||
|
||||
def __init__(self, syncthing, server_id, folder_id, folder_label, version):
|
||||
def __init__(
|
||||
self,
|
||||
syncthing: SyncthingClient,
|
||||
server_id: str,
|
||||
folder_id: str,
|
||||
folder_label: str,
|
||||
version: str,
|
||||
) -> None:
|
||||
"""Initialize the sensor."""
|
||||
self._syncthing = syncthing
|
||||
self._server_id = server_id
|
||||
self._folder_id = folder_id
|
||||
self._folder_label = folder_label
|
||||
self._state = None
|
||||
self._unsub_timer = None
|
||||
self._state: dict[str, Any] | None = None
|
||||
self._unsub_timer: CALLBACK_TYPE | None = None
|
||||
|
||||
self._short_server_id = server_id.split("-")[0]
|
||||
self._attr_name = f"{self._short_server_id} {folder_id} {folder_label}"
|
||||
@@ -107,9 +118,9 @@ class FolderSensor(SensorEntity):
|
||||
)
|
||||
|
||||
@property
|
||||
def native_value(self):
|
||||
def native_value(self) -> str | None:
|
||||
"""Return the state of the sensor."""
|
||||
return self._state["state"]
|
||||
return self._state["state"] if self._state else None
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
@@ -117,11 +128,11 @@ class FolderSensor(SensorEntity):
|
||||
return self._state is not None
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self):
|
||||
def extra_state_attributes(self) -> Mapping[str, Any] | None:
|
||||
"""Return the state attributes."""
|
||||
return self._state
|
||||
|
||||
async def async_update_status(self):
|
||||
async def async_update_status(self) -> None:
|
||||
"""Request folder status and update state."""
|
||||
try:
|
||||
state = await self._syncthing.database.status(self._folder_id)
|
||||
@@ -131,11 +142,11 @@ class FolderSensor(SensorEntity):
|
||||
self._state = self._filter_state(state)
|
||||
self.async_write_ha_state()
|
||||
|
||||
def subscribe(self):
|
||||
def subscribe(self) -> None:
|
||||
"""Start polling syncthing folder status."""
|
||||
if self._unsub_timer is None:
|
||||
|
||||
async def refresh(event_time):
|
||||
async def refresh(event_time) -> None:
|
||||
"""Get the latest data from Syncthing."""
|
||||
await self.async_update_status()
|
||||
|
||||
@@ -144,7 +155,7 @@ class FolderSensor(SensorEntity):
|
||||
)
|
||||
|
||||
@callback
|
||||
def unsubscribe(self):
|
||||
def unsubscribe(self) -> None:
|
||||
"""Stop polling syncthing folder status."""
|
||||
if self._unsub_timer is not None:
|
||||
self._unsub_timer()
|
||||
@@ -154,8 +165,9 @@ class FolderSensor(SensorEntity):
|
||||
"""Handle entity which will be added."""
|
||||
|
||||
@callback
|
||||
def handle_folder_summary(event):
|
||||
if self._state is not None:
|
||||
def handle_folder_summary(event: dict[str, Any]) -> None:
|
||||
"""Handle folder summary event."""
|
||||
if self._state:
|
||||
self._state = self._filter_state(event["data"]["summary"])
|
||||
self.async_write_ha_state()
|
||||
|
||||
@@ -168,8 +180,9 @@ class FolderSensor(SensorEntity):
|
||||
)
|
||||
|
||||
@callback
|
||||
def handle_state_changed(event):
|
||||
if self._state is not None:
|
||||
def handle_state_changed(event: dict[str, Any]) -> None:
|
||||
"""Handle folder state changed event."""
|
||||
if self._state:
|
||||
self._state["state"] = event["data"]["to"]
|
||||
self.async_write_ha_state()
|
||||
|
||||
@@ -182,8 +195,9 @@ class FolderSensor(SensorEntity):
|
||||
)
|
||||
|
||||
@callback
|
||||
def handle_folder_paused(event):
|
||||
if self._state is not None:
|
||||
def handle_folder_paused(event: dict[str, Any]) -> None:
|
||||
"""Handle folder paused event."""
|
||||
if self._state:
|
||||
self._state["state"] = "paused"
|
||||
self.async_write_ha_state()
|
||||
|
||||
@@ -196,7 +210,8 @@ class FolderSensor(SensorEntity):
|
||||
)
|
||||
|
||||
@callback
|
||||
def handle_server_unavailable():
|
||||
def handle_server_unavailable() -> None:
|
||||
"""Handle server becoming unavailable."""
|
||||
self._state = None
|
||||
self.unsubscribe()
|
||||
self.async_write_ha_state()
|
||||
@@ -209,7 +224,8 @@ class FolderSensor(SensorEntity):
|
||||
)
|
||||
)
|
||||
|
||||
async def handle_server_available():
|
||||
async def handle_server_available() -> None:
|
||||
"""Handle server becoming available."""
|
||||
self.subscribe()
|
||||
await self.async_update_status()
|
||||
|
||||
@@ -226,20 +242,20 @@ class FolderSensor(SensorEntity):
|
||||
|
||||
await self.async_update_status()
|
||||
|
||||
def _filter_state(self, state):
|
||||
# Select only needed state attributes and map their names
|
||||
state = {
|
||||
def _filter_state(self, state: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Filter and map state attributes."""
|
||||
filtered_state: dict[str, Any] = {
|
||||
self.STATE_ATTRIBUTES[key]: value
|
||||
for key, value in state.items()
|
||||
if key in self.STATE_ATTRIBUTES
|
||||
}
|
||||
|
||||
# A workaround, for some reason, state of paused folders is an empty string
|
||||
if state["state"] == "":
|
||||
state["state"] = "paused"
|
||||
if filtered_state["state"] == "":
|
||||
filtered_state["state"] = "paused"
|
||||
|
||||
# Add some useful attributes
|
||||
state["id"] = self._folder_id
|
||||
state["label"] = self._folder_label
|
||||
filtered_state["id"] = self._folder_id
|
||||
filtered_state["label"] = self._folder_label
|
||||
|
||||
return state
|
||||
return filtered_state
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -293,7 +293,6 @@ class AbstractTemplateLight(AbstractTemplateEntity, LightEntity):
|
||||
# Stored values for template attributes
|
||||
self._attr_is_on = initial_state
|
||||
self._supports_transition = False
|
||||
self._attr_color_mode: ColorMode | None = None
|
||||
|
||||
def _setup_light_features(self, config: ConfigType, name: str) -> None:
|
||||
"""Setup light scripts, supported color modes, and supported features."""
|
||||
|
||||
@@ -110,7 +110,7 @@ class TradfriLight(TradfriBaseEntity, LightEntity):
|
||||
return cast(bool, self._device_data.state)
|
||||
|
||||
@property
|
||||
def color_mode(self) -> ColorMode | None:
|
||||
def color_mode(self) -> ColorMode:
|
||||
"""Return the color mode of the light."""
|
||||
if self._fixed_color_mode:
|
||||
return self._fixed_color_mode
|
||||
|
||||
@@ -14,5 +14,6 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["pyvesync"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["pyvesync==3.4.1"]
|
||||
}
|
||||
|
||||
15
homeassistant/components/waterfurnace/icons.json
Normal file
15
homeassistant/components/waterfurnace/icons.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"actual_compressor_speed": {
|
||||
"default": "mdi:speedometer"
|
||||
},
|
||||
"airflow_current_speed": {
|
||||
"default": "mdi:fan"
|
||||
},
|
||||
"mode": {
|
||||
"default": "mdi:gauge"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,89 +18,93 @@ from homeassistant.util import slugify
|
||||
from . import UPDATE_TOPIC, WaterFurnaceConfigEntry, WaterFurnaceData
|
||||
|
||||
SENSORS = [
|
||||
SensorEntityDescription(name="Furnace Mode", key="mode", icon="mdi:gauge"),
|
||||
SensorEntityDescription(
|
||||
name="Total Power",
|
||||
key="mode",
|
||||
translation_key="mode",
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="totalunitpower",
|
||||
translation_key="total_unit_power",
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
name="Active Setpoint",
|
||||
key="tstatactivesetpoint",
|
||||
translation_key="tstat_active_setpoint",
|
||||
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
name="Leaving Air",
|
||||
key="leavingairtemp",
|
||||
translation_key="leaving_air_temp",
|
||||
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
name="Room Temp",
|
||||
key="tstatroomtemp",
|
||||
translation_key="room_temp",
|
||||
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
name="Loop Temp",
|
||||
key="enteringwatertemp",
|
||||
translation_key="entering_water_temp",
|
||||
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
name="Humidity Set Point",
|
||||
key="tstathumidsetpoint",
|
||||
icon="mdi:water-percent",
|
||||
translation_key="tstat_humid_setpoint",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
device_class=SensorDeviceClass.HUMIDITY,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
name="Humidity",
|
||||
key="tstatrelativehumidity",
|
||||
icon="mdi:water-percent",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
device_class=SensorDeviceClass.HUMIDITY,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
name="Compressor Power",
|
||||
key="compressorpower",
|
||||
translation_key="compressor_power",
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
name="Fan Power",
|
||||
key="fanpower",
|
||||
translation_key="fan_power",
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
name="Aux Power",
|
||||
key="auxpower",
|
||||
translation_key="aux_power",
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
name="Loop Pump Power",
|
||||
key="looppumppower",
|
||||
translation_key="loop_pump_power",
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
name="Compressor Speed", key="actualcompressorspeed", icon="mdi:speedometer"
|
||||
key="actualcompressorspeed",
|
||||
translation_key="actual_compressor_speed",
|
||||
),
|
||||
SensorEntityDescription(
|
||||
name="Fan Speed", key="airflowcurrentspeed", icon="mdi:fan"
|
||||
key="airflowcurrentspeed",
|
||||
translation_key="airflow_current_speed",
|
||||
),
|
||||
]
|
||||
|
||||
@@ -124,6 +128,7 @@ class WaterFurnaceSensor(SensorEntity):
|
||||
"""Implementing the Waterfurnace sensor."""
|
||||
|
||||
_attr_should_poll = False
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self, client: WaterFurnaceData, description: SensorEntityDescription
|
||||
|
||||
@@ -26,6 +26,49 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"actual_compressor_speed": {
|
||||
"name": "Compressor speed"
|
||||
},
|
||||
"airflow_current_speed": {
|
||||
"name": "Fan speed"
|
||||
},
|
||||
"aux_power": {
|
||||
"name": "Aux power"
|
||||
},
|
||||
"compressor_power": {
|
||||
"name": "Compressor power"
|
||||
},
|
||||
"entering_water_temp": {
|
||||
"name": "Loop temperature"
|
||||
},
|
||||
"fan_power": {
|
||||
"name": "Fan power"
|
||||
},
|
||||
"leaving_air_temp": {
|
||||
"name": "Leaving air temperature"
|
||||
},
|
||||
"loop_pump_power": {
|
||||
"name": "Loop pump power"
|
||||
},
|
||||
"mode": {
|
||||
"name": "Furnace mode"
|
||||
},
|
||||
"room_temp": {
|
||||
"name": "Room temperature"
|
||||
},
|
||||
"total_unit_power": {
|
||||
"name": "Total power"
|
||||
},
|
||||
"tstat_active_setpoint": {
|
||||
"name": "Active setpoint"
|
||||
},
|
||||
"tstat_humid_setpoint": {
|
||||
"name": "Humidity setpoint"
|
||||
}
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"deprecated_yaml_import_issue_cannot_connect": {
|
||||
"description": "Configuring {integration_title} via YAML is deprecated and will be removed in a future release. While importing your configuration, we could not connect to {integration_title}. Please check your YAML configuration and restart Home Assistant, or remove the {domain} key from your configuration and configure the integration via the UI.",
|
||||
|
||||
@@ -157,10 +157,10 @@ class Light(LightEntity, ZHAEntity):
|
||||
)
|
||||
|
||||
@property
|
||||
def color_mode(self) -> ColorMode | None:
|
||||
def color_mode(self) -> ColorMode:
|
||||
"""Return the color mode."""
|
||||
if self.entity_data.entity.color_mode is None:
|
||||
return None
|
||||
return ColorMode.UNKNOWN
|
||||
return ZHA_TO_HA_COLOR_MODE[self.entity_data.entity.color_mode]
|
||||
|
||||
@property
|
||||
|
||||
@@ -120,10 +120,6 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity):
|
||||
self._supports_rgbw = False
|
||||
self._supports_color_temp = False
|
||||
self._supports_dimming = False
|
||||
self._color_mode: ColorMode | None = None
|
||||
self._hs_color: tuple[float, float] | None = None
|
||||
self._rgbw_color: tuple[int, int, int, int] | None = None
|
||||
self._color_temp: int | None = None
|
||||
self._warm_white = self.get_zwave_value(
|
||||
TARGET_COLOR_PROPERTY,
|
||||
CommandClass.SWITCH_COLOR,
|
||||
@@ -134,7 +130,6 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity):
|
||||
CommandClass.SWITCH_COLOR,
|
||||
value_property_key=ColorComponent.COLD_WHITE,
|
||||
)
|
||||
self._supported_color_modes: set[ColorMode] = set()
|
||||
|
||||
self._target_brightness: Value | None = None
|
||||
|
||||
@@ -180,17 +175,18 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity):
|
||||
)
|
||||
|
||||
self._calculate_color_support()
|
||||
self._attr_supported_color_modes = set()
|
||||
if self._supports_rgbw:
|
||||
self._supported_color_modes.add(ColorMode.RGBW)
|
||||
self._attr_supported_color_modes.add(ColorMode.RGBW)
|
||||
elif self._supports_color:
|
||||
self._supported_color_modes.add(ColorMode.HS)
|
||||
self._attr_supported_color_modes.add(ColorMode.HS)
|
||||
if self._supports_color_temp:
|
||||
self._supported_color_modes.add(ColorMode.COLOR_TEMP)
|
||||
if not self._supported_color_modes:
|
||||
self._attr_supported_color_modes.add(ColorMode.COLOR_TEMP)
|
||||
if not self._attr_supported_color_modes:
|
||||
if self.info.primary_value.command_class == CommandClass.SWITCH_BINARY:
|
||||
self._supported_color_modes.add(ColorMode.ONOFF)
|
||||
self._attr_supported_color_modes.add(ColorMode.ONOFF)
|
||||
else:
|
||||
self._supported_color_modes.add(ColorMode.BRIGHTNESS)
|
||||
self._attr_supported_color_modes.add(ColorMode.BRIGHTNESS)
|
||||
self._calculate_color_values()
|
||||
|
||||
# Entity class attributes
|
||||
@@ -225,11 +221,6 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity):
|
||||
return None
|
||||
return round((cast(int, self.info.primary_value.value) / 99) * 255)
|
||||
|
||||
@property
|
||||
def color_mode(self) -> ColorMode | None:
|
||||
"""Return the color mode of the light."""
|
||||
return self._color_mode
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool | None:
|
||||
"""Return true if device is on (brightness above 0)."""
|
||||
@@ -239,26 +230,6 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity):
|
||||
brightness = self.brightness
|
||||
return brightness > 0 if brightness is not None else None
|
||||
|
||||
@property
|
||||
def hs_color(self) -> tuple[float, float] | None:
|
||||
"""Return the hs color."""
|
||||
return self._hs_color
|
||||
|
||||
@property
|
||||
def rgbw_color(self) -> tuple[int, int, int, int] | None:
|
||||
"""Return the RGBW color."""
|
||||
return self._rgbw_color
|
||||
|
||||
@property
|
||||
def color_temp_kelvin(self) -> int | None:
|
||||
"""Return the color temperature value in Kelvin."""
|
||||
return self._color_temp
|
||||
|
||||
@property
|
||||
def supported_color_modes(self) -> set[ColorMode] | None:
|
||||
"""Flag supported features."""
|
||||
return self._supported_color_modes
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the device on."""
|
||||
|
||||
@@ -479,9 +450,9 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity):
|
||||
|
||||
# Default: Brightness (no color) or Unknown
|
||||
if self.supported_color_modes == {ColorMode.BRIGHTNESS}:
|
||||
self._color_mode = ColorMode.BRIGHTNESS
|
||||
self._attr_color_mode = ColorMode.BRIGHTNESS
|
||||
else:
|
||||
self._color_mode = ColorMode.UNKNOWN
|
||||
self._attr_color_mode = ColorMode.UNKNOWN
|
||||
|
||||
# RGB support
|
||||
if red_val and green_val and blue_val:
|
||||
@@ -491,9 +462,9 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity):
|
||||
blue = multi_color.get(COLOR_SWITCH_COMBINED_BLUE, blue_val.value)
|
||||
if red is not None and green is not None and blue is not None:
|
||||
# convert to HS
|
||||
self._hs_color = color_util.color_RGB_to_hs(red, green, blue)
|
||||
self._attr_hs_color = color_util.color_RGB_to_hs(red, green, blue)
|
||||
# Light supports color, set color mode to hs
|
||||
self._color_mode = ColorMode.HS
|
||||
self._attr_color_mode = ColorMode.HS
|
||||
|
||||
# color temperature support
|
||||
if ww_val and cw_val:
|
||||
@@ -501,14 +472,16 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity):
|
||||
cold_white = multi_color.get(COLOR_SWITCH_COMBINED_COLD_WHITE, cw_val.value)
|
||||
# Calculate color temps based on whites
|
||||
if cold_white or warm_white:
|
||||
self._color_temp = color_util.color_temperature_mired_to_kelvin(
|
||||
MAX_MIREDS
|
||||
- ((cast(int, cold_white) / 255) * (MAX_MIREDS - MIN_MIREDS))
|
||||
self._attr_color_temp_kelvin = (
|
||||
color_util.color_temperature_mired_to_kelvin(
|
||||
MAX_MIREDS
|
||||
- ((cast(int, cold_white) / 255) * (MAX_MIREDS - MIN_MIREDS))
|
||||
)
|
||||
)
|
||||
# White channels turned on, set color mode to color_temp
|
||||
self._color_mode = ColorMode.COLOR_TEMP
|
||||
self._attr_color_mode = ColorMode.COLOR_TEMP
|
||||
else:
|
||||
self._color_temp = None
|
||||
self._attr_color_temp_kelvin = None
|
||||
# only one white channel (warm white) = rgbw support
|
||||
elif red_val and green_val and blue_val and ww_val:
|
||||
white = multi_color.get(COLOR_SWITCH_COMBINED_WARM_WHITE, ww_val.value)
|
||||
@@ -519,9 +492,9 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity):
|
||||
and blue is not None
|
||||
and white is not None
|
||||
)
|
||||
self._rgbw_color = (red, green, blue, white)
|
||||
self._attr_rgbw_color = (red, green, blue, white)
|
||||
# Light supports rgbw, set color mode to rgbw
|
||||
self._color_mode = ColorMode.RGBW
|
||||
self._attr_color_mode = ColorMode.RGBW
|
||||
# only one white channel (cool white) = rgbw support
|
||||
elif cw_val:
|
||||
self._supports_rgbw = True
|
||||
@@ -533,9 +506,9 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity):
|
||||
and blue is not None
|
||||
and white is not None
|
||||
)
|
||||
self._rgbw_color = (red, green, blue, white)
|
||||
self._attr_rgbw_color = (red, green, blue, white)
|
||||
# Light supports rgbw, set color mode to rgbw
|
||||
self._color_mode = ColorMode.RGBW
|
||||
self._attr_color_mode = ColorMode.RGBW
|
||||
|
||||
|
||||
class ZwaveColorOnOffLight(ZwaveLight):
|
||||
@@ -620,8 +593,8 @@ class ZwaveColorOnOffLight(ZwaveLight):
|
||||
new_colors = {}
|
||||
for color, value in self._last_on_color.items():
|
||||
new_colors[color] = round(value * new_scale)
|
||||
elif hs_color is None and self._color_mode == ColorMode.HS:
|
||||
hs_color = self._hs_color
|
||||
elif hs_color is None and self._attr_color_mode == ColorMode.HS:
|
||||
hs_color = self._attr_hs_color
|
||||
elif hs_color is not None and brightness is None:
|
||||
# Turned on by using the color controls
|
||||
current_brightness = self.brightness
|
||||
|
||||
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
|
||||
|
||||
13
requirements_all.txt
generated
13
requirements_all.txt
generated
@@ -797,7 +797,7 @@ deluge-client==1.10.2
|
||||
demetriek==1.3.0
|
||||
|
||||
# homeassistant.components.denonavr
|
||||
denonavr==1.3.1
|
||||
denonavr==1.3.2
|
||||
|
||||
# homeassistant.components.devialet
|
||||
devialet==1.5.7
|
||||
@@ -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
|
||||
@@ -1105,7 +1105,7 @@ google-nest-sdm==9.1.2
|
||||
google-photos-library-api==0.12.1
|
||||
|
||||
# homeassistant.components.google_air_quality
|
||||
google_air_quality_api==3.0.0
|
||||
google_air_quality_api==3.0.1
|
||||
|
||||
# homeassistant.components.slide
|
||||
# homeassistant.components.slide_local
|
||||
@@ -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
|
||||
@@ -2434,7 +2434,7 @@ pysma==1.1.0
|
||||
pysmappee==0.2.29
|
||||
|
||||
# homeassistant.components.smarla
|
||||
pysmarlaapi==0.9.3
|
||||
pysmarlaapi==0.13.0
|
||||
|
||||
# homeassistant.components.smartthings
|
||||
pysmartthings==3.5.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
|
||||
|
||||
|
||||
13
requirements_test_all.txt
generated
13
requirements_test_all.txt
generated
@@ -706,7 +706,7 @@ deluge-client==1.10.2
|
||||
demetriek==1.3.0
|
||||
|
||||
# homeassistant.components.denonavr
|
||||
denonavr==1.3.1
|
||||
denonavr==1.3.2
|
||||
|
||||
# homeassistant.components.devialet
|
||||
devialet==1.5.7
|
||||
@@ -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
|
||||
@@ -981,7 +981,7 @@ google-nest-sdm==9.1.2
|
||||
google-photos-library-api==0.12.1
|
||||
|
||||
# homeassistant.components.google_air_quality
|
||||
google_air_quality_api==3.0.0
|
||||
google_air_quality_api==3.0.1
|
||||
|
||||
# homeassistant.components.slide
|
||||
# homeassistant.components.slide_local
|
||||
@@ -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
|
||||
@@ -2060,7 +2060,7 @@ pysma==1.1.0
|
||||
pysmappee==0.2.29
|
||||
|
||||
# homeassistant.components.smarla
|
||||
pysmarlaapi==0.9.3
|
||||
pysmarlaapi==0.13.0
|
||||
|
||||
# homeassistant.components.smartthings
|
||||
pysmartthings==3.5.1
|
||||
@@ -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",
|
||||
|
||||
@@ -188,3 +188,69 @@ async def test_config_entry_not_loaded(
|
||||
assert exc_info.value.translation_domain == DOMAIN
|
||||
assert exc_info.value.translation_key == "entry_not_loaded"
|
||||
assert exc_info.value.translation_placeholders == {"entry": mock_config_entry.title}
|
||||
|
||||
|
||||
async def test_invalid_config_entry(
|
||||
hass: HomeAssistant,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
mock_amazon_devices_client: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test invalid config entry."""
|
||||
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
device_entry = device_registry.async_get_device(
|
||||
identifiers={(DOMAIN, TEST_DEVICE_1_SN)}
|
||||
)
|
||||
assert device_entry
|
||||
|
||||
device_entry.config_entries.add("non_existing_entry_id")
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Call Service
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_SOUND_NOTIFICATION,
|
||||
{
|
||||
ATTR_SOUND: "bell_02",
|
||||
ATTR_DEVICE_ID: device_entry.id,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
# No exception should be raised
|
||||
assert mock_amazon_devices_client.call_alexa_sound.call_count == 1
|
||||
|
||||
|
||||
async def test_missing_config_entry(
|
||||
hass: HomeAssistant,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
mock_amazon_devices_client: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test missing config entry."""
|
||||
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
device_entry = device_registry.async_get_device(
|
||||
identifiers={(DOMAIN, TEST_DEVICE_1_SN)}
|
||||
)
|
||||
assert device_entry
|
||||
|
||||
device_entry.config_entries.clear()
|
||||
|
||||
# Call Service
|
||||
with pytest.raises(ServiceValidationError) as exc_info:
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_SOUND_NOTIFICATION,
|
||||
{
|
||||
ATTR_SOUND: "bell_02",
|
||||
ATTR_DEVICE_ID: device_entry.id,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert exc_info.value.translation_domain == DOMAIN
|
||||
assert exc_info.value.translation_key == "config_entry_not_found"
|
||||
assert exc_info.value.translation_placeholders == {"device_id": device_entry.id}
|
||||
|
||||
@@ -367,6 +367,57 @@ async def test_agents_upload_network_failure(
|
||||
assert "Upload failed for cloudflare_r2" in caplog.text
|
||||
|
||||
|
||||
async def test_multipart_upload_consistent_part_sizes(
|
||||
hass: HomeAssistant,
|
||||
mock_client: MagicMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test that multipart upload uses consistent part sizes.
|
||||
|
||||
S3/R2 requires all non-trailing parts to have the same size. This test
|
||||
verifies that varying chunk sizes still result in consistent part sizes.
|
||||
"""
|
||||
agent = R2BackupAgent(hass, mock_config_entry)
|
||||
|
||||
# simulate varying chunk data sizes
|
||||
# total data: 12 + 12 + 10 + 12 + 5 = 51 MiB
|
||||
chunk_sizes = [12, 12, 10, 12, 5] # in units of 1 MiB
|
||||
mib = 2**20
|
||||
|
||||
async def mock_stream():
|
||||
for size in chunk_sizes:
|
||||
yield b"x" * (size * mib)
|
||||
|
||||
async def open_stream():
|
||||
return mock_stream()
|
||||
|
||||
# Record the sizes of each uploaded part
|
||||
uploaded_part_sizes: list[int] = []
|
||||
|
||||
async def record_upload_part(**kwargs):
|
||||
body = kwargs.get("Body", b"")
|
||||
uploaded_part_sizes.append(len(body))
|
||||
return {"ETag": f"etag-{len(uploaded_part_sizes)}"}
|
||||
|
||||
mock_client.upload_part.side_effect = record_upload_part
|
||||
|
||||
await agent._upload_multipart("test.tar", open_stream)
|
||||
|
||||
# Verify that all non-trailing parts have the same size
|
||||
assert len(uploaded_part_sizes) >= 2, "Expected at least 2 parts"
|
||||
non_trailing_parts = uploaded_part_sizes[:-1]
|
||||
assert all(size == MULTIPART_MIN_PART_SIZE_BYTES for size in non_trailing_parts), (
|
||||
f"All non-trailing parts should be {MULTIPART_MIN_PART_SIZE_BYTES} bytes, got {non_trailing_parts}"
|
||||
)
|
||||
|
||||
# Verify the trailing part contains the remainder
|
||||
total_data = sum(chunk_sizes) * mib
|
||||
expected_trailing = total_data % MULTIPART_MIN_PART_SIZE_BYTES
|
||||
if expected_trailing == 0:
|
||||
expected_trailing = MULTIPART_MIN_PART_SIZE_BYTES
|
||||
assert uploaded_part_sizes[-1] == expected_trailing
|
||||
|
||||
|
||||
async def test_agents_download(
|
||||
hass_client: ClientSessionGenerator,
|
||||
mock_client: MagicMock,
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
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