mirror of
https://github.com/home-assistant/core.git
synced 2025-07-22 20:57:21 +00:00
2025.1.3 (#136092)
This commit is contained in:
commit
3e1d13b6ad
@ -11,5 +11,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/airzone",
|
"documentation": "https://www.home-assistant.io/integrations/airzone",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["aioairzone"],
|
"loggers": ["aioairzone"],
|
||||||
"requirements": ["aioairzone==0.9.7"]
|
"requirements": ["aioairzone==0.9.9"]
|
||||||
}
|
}
|
||||||
|
@ -50,7 +50,7 @@ async def async_setup_entry(
|
|||||||
|
|
||||||
descriptions: list[AprilaireHumidifierDescription] = []
|
descriptions: list[AprilaireHumidifierDescription] = []
|
||||||
|
|
||||||
if coordinator.data.get(Attribute.HUMIDIFICATION_AVAILABLE) in (0, 1, 2):
|
if coordinator.data.get(Attribute.HUMIDIFICATION_AVAILABLE) in (1, 2):
|
||||||
descriptions.append(
|
descriptions.append(
|
||||||
AprilaireHumidifierDescription(
|
AprilaireHumidifierDescription(
|
||||||
key="humidifier",
|
key="humidifier",
|
||||||
@ -67,7 +67,7 @@ async def async_setup_entry(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if coordinator.data.get(Attribute.DEHUMIDIFICATION_AVAILABLE) in (0, 1):
|
if coordinator.data.get(Attribute.DEHUMIDIFICATION_AVAILABLE) == 1:
|
||||||
descriptions.append(
|
descriptions.append(
|
||||||
AprilaireHumidifierDescription(
|
AprilaireHumidifierDescription(
|
||||||
key="dehumidifier",
|
key="dehumidifier",
|
||||||
|
@ -1017,9 +1017,18 @@ class PipelineRun:
|
|||||||
raise RuntimeError("Recognize intent was not prepared")
|
raise RuntimeError("Recognize intent was not prepared")
|
||||||
|
|
||||||
if self.pipeline.conversation_language == MATCH_ALL:
|
if self.pipeline.conversation_language == MATCH_ALL:
|
||||||
# LLMs support all languages ('*') so use pipeline language for
|
# LLMs support all languages ('*') so use languages from the
|
||||||
# intent fallback.
|
# pipeline for intent fallback.
|
||||||
input_language = self.pipeline.language
|
#
|
||||||
|
# We prioritize the STT and TTS languages because they may be more
|
||||||
|
# specific, such as "zh-CN" instead of just "zh". This is necessary
|
||||||
|
# for languages whose intents are split out by region when
|
||||||
|
# preferring local intent matching.
|
||||||
|
input_language = (
|
||||||
|
self.pipeline.stt_language
|
||||||
|
or self.pipeline.tts_language
|
||||||
|
or self.pipeline.language
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
input_language = self.pipeline.conversation_language
|
input_language = self.pipeline.conversation_language
|
||||||
|
|
||||||
|
@ -6,5 +6,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/aussie_broadband",
|
"documentation": "https://www.home-assistant.io/integrations/aussie_broadband",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["aussiebb"],
|
"loggers": ["aussiebb"],
|
||||||
"requirements": ["pyaussiebb==0.1.4"]
|
"requirements": ["pyaussiebb==0.1.5"]
|
||||||
}
|
}
|
||||||
|
@ -111,7 +111,7 @@
|
|||||||
"services": {
|
"services": {
|
||||||
"send_message": {
|
"send_message": {
|
||||||
"name": "[%key:component::notify::services::notify::name%]",
|
"name": "[%key:component::notify::services::notify::name%]",
|
||||||
"description": "Send a mobile push notification to members of a shared Bring! list.",
|
"description": "Sends a mobile push notification to members of a shared Bring! list.",
|
||||||
"fields": {
|
"fields": {
|
||||||
"entity_id": {
|
"entity_id": {
|
||||||
"name": "List",
|
"name": "List",
|
||||||
@ -122,8 +122,8 @@
|
|||||||
"description": "Type of push notification to send to list members."
|
"description": "Type of push notification to send to list members."
|
||||||
},
|
},
|
||||||
"item": {
|
"item": {
|
||||||
"name": "Article (Required if message type `Urgent Message` selected)",
|
"name": "Article (Required if notification type `Urgent message` is selected)",
|
||||||
"description": "Article name to include in an urgent message e.g. `Urgent Message - Please buy Cilantro urgently`"
|
"description": "Article name to include in an urgent message e.g. `Urgent message - Please buy Cilantro urgently`"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -134,7 +134,7 @@
|
|||||||
"going_shopping": "I'm going shopping! - Last chance to make changes",
|
"going_shopping": "I'm going shopping! - Last chance to make changes",
|
||||||
"changed_list": "List updated - Take a look at the articles",
|
"changed_list": "List updated - Take a look at the articles",
|
||||||
"shopping_done": "Shopping done - The fridge is well stocked",
|
"shopping_done": "Shopping done - The fridge is well stocked",
|
||||||
"urgent_message": "Urgent Message - Please buy `Article name` urgently"
|
"urgent_message": "Urgent message - Please buy `Article` urgently"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,5 +6,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/ecovacs",
|
"documentation": "https://www.home-assistant.io/integrations/ecovacs",
|
||||||
"iot_class": "cloud_push",
|
"iot_class": "cloud_push",
|
||||||
"loggers": ["sleekxmppfs", "sucks", "deebot_client"],
|
"loggers": ["sleekxmppfs", "sucks", "deebot_client"],
|
||||||
"requirements": ["py-sucks==0.9.10", "deebot-client==10.1.0"]
|
"requirements": ["py-sucks==0.9.10", "deebot-client==11.0.0"]
|
||||||
}
|
}
|
||||||
|
@ -15,5 +15,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/elkm1",
|
"documentation": "https://www.home-assistant.io/integrations/elkm1",
|
||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"loggers": ["elkm1_lib"],
|
"loggers": ["elkm1_lib"],
|
||||||
"requirements": ["elkm1-lib==2.2.10"]
|
"requirements": ["elkm1-lib==2.2.11"]
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,7 @@ from pyezviz.exceptions import PyEzvizError
|
|||||||
from pyezviz.utils import decrypt_image
|
from pyezviz.utils import decrypt_image
|
||||||
|
|
||||||
from homeassistant.components.image import Image, ImageEntity, ImageEntityDescription
|
from homeassistant.components.image import Image, ImageEntity, ImageEntityDescription
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import SOURCE_IGNORE, ConfigEntry
|
||||||
from homeassistant.const import CONF_PASSWORD
|
from homeassistant.const import CONF_PASSWORD
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
@ -57,7 +57,9 @@ class EzvizLastMotion(EzvizEntity, ImageEntity):
|
|||||||
)
|
)
|
||||||
camera = hass.config_entries.async_entry_for_domain_unique_id(DOMAIN, serial)
|
camera = hass.config_entries.async_entry_for_domain_unique_id(DOMAIN, serial)
|
||||||
self.alarm_image_password = (
|
self.alarm_image_password = (
|
||||||
camera.data[CONF_PASSWORD] if camera is not None else None
|
camera.data[CONF_PASSWORD]
|
||||||
|
if camera and camera.source != SOURCE_IGNORE
|
||||||
|
else None
|
||||||
)
|
)
|
||||||
|
|
||||||
async def _async_load_image_from_url(self, url: str) -> Image | None:
|
async def _async_load_image_from_url(self, url: str) -> Image | None:
|
||||||
|
@ -7,6 +7,6 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/freebox",
|
"documentation": "https://www.home-assistant.io/integrations/freebox",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["freebox_api"],
|
"loggers": ["freebox_api"],
|
||||||
"requirements": ["freebox-api==1.2.1"],
|
"requirements": ["freebox-api==1.2.2"],
|
||||||
"zeroconf": ["_fbx-api._tcp.local."]
|
"zeroconf": ["_fbx-api._tcp.local."]
|
||||||
}
|
}
|
||||||
|
@ -227,11 +227,12 @@ class SupervisorBackupReaderWriter(BackupReaderWriter):
|
|||||||
include_addons_set = supervisor_backups.AddonSet.ALL
|
include_addons_set = supervisor_backups.AddonSet.ALL
|
||||||
elif include_addons:
|
elif include_addons:
|
||||||
include_addons_set = set(include_addons)
|
include_addons_set = set(include_addons)
|
||||||
include_folders_set = (
|
include_folders_set = {
|
||||||
{supervisor_backups.Folder(folder) for folder in include_folders}
|
supervisor_backups.Folder(folder) for folder in include_folders or []
|
||||||
if include_folders
|
}
|
||||||
else None
|
# Always include SSL if Home Assistant is included
|
||||||
)
|
if include_homeassistant:
|
||||||
|
include_folders_set.add(supervisor_backups.Folder.SSL)
|
||||||
|
|
||||||
hassio_agents: list[SupervisorBackupAgent] = [
|
hassio_agents: list[SupervisorBackupAgent] = [
|
||||||
cast(SupervisorBackupAgent, manager.backup_agents[agent_id])
|
cast(SupervisorBackupAgent, manager.backup_agents[agent_id])
|
||||||
|
@ -9,5 +9,5 @@
|
|||||||
},
|
},
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["apyhiveapi"],
|
"loggers": ["apyhiveapi"],
|
||||||
"requirements": ["pyhiveapi==0.5.16"]
|
"requirements": ["pyhive-integration==1.0.1"]
|
||||||
}
|
}
|
||||||
|
@ -52,6 +52,7 @@ from .const import (
|
|||||||
PROP_MIN_VALUE,
|
PROP_MIN_VALUE,
|
||||||
SERV_LIGHTBULB,
|
SERV_LIGHTBULB,
|
||||||
)
|
)
|
||||||
|
from .util import get_min_max
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -120,12 +121,14 @@ class Light(HomeAccessory):
|
|||||||
self.char_brightness = serv_light.configure_char(CHAR_BRIGHTNESS, value=100)
|
self.char_brightness = serv_light.configure_char(CHAR_BRIGHTNESS, value=100)
|
||||||
|
|
||||||
if CHAR_COLOR_TEMPERATURE in self.chars:
|
if CHAR_COLOR_TEMPERATURE in self.chars:
|
||||||
self.min_mireds = color_temperature_kelvin_to_mired(
|
min_mireds = color_temperature_kelvin_to_mired(
|
||||||
attributes.get(ATTR_MAX_COLOR_TEMP_KELVIN, DEFAULT_MAX_COLOR_TEMP)
|
attributes.get(ATTR_MAX_COLOR_TEMP_KELVIN, DEFAULT_MAX_COLOR_TEMP)
|
||||||
)
|
)
|
||||||
self.max_mireds = color_temperature_kelvin_to_mired(
|
max_mireds = color_temperature_kelvin_to_mired(
|
||||||
attributes.get(ATTR_MIN_COLOR_TEMP_KELVIN, DEFAULT_MIN_COLOR_TEMP)
|
attributes.get(ATTR_MIN_COLOR_TEMP_KELVIN, DEFAULT_MIN_COLOR_TEMP)
|
||||||
)
|
)
|
||||||
|
# Ensure min is less than max
|
||||||
|
self.min_mireds, self.max_mireds = get_min_max(min_mireds, max_mireds)
|
||||||
if not self.color_temp_supported and not self.rgbww_supported:
|
if not self.color_temp_supported and not self.rgbww_supported:
|
||||||
self.max_mireds = self.min_mireds
|
self.max_mireds = self.min_mireds
|
||||||
self.char_color_temp = serv_light.configure_char(
|
self.char_color_temp = serv_light.configure_char(
|
||||||
@ -282,7 +285,11 @@ class Light(HomeAccessory):
|
|||||||
hue, saturation = color_temperature_to_hs(color_temp)
|
hue, saturation = color_temperature_to_hs(color_temp)
|
||||||
elif color_mode == ColorMode.WHITE:
|
elif color_mode == ColorMode.WHITE:
|
||||||
hue, saturation = 0, 0
|
hue, saturation = 0, 0
|
||||||
elif hue_sat := attributes.get(ATTR_HS_COLOR):
|
elif (
|
||||||
|
(hue_sat := attributes.get(ATTR_HS_COLOR))
|
||||||
|
and isinstance(hue_sat, (list, tuple))
|
||||||
|
and len(hue_sat) == 2
|
||||||
|
):
|
||||||
hue, saturation = hue_sat
|
hue, saturation = hue_sat
|
||||||
else:
|
else:
|
||||||
hue = None
|
hue = None
|
||||||
|
@ -14,6 +14,7 @@ from homeassistant.components.climate import (
|
|||||||
ATTR_HVAC_ACTION,
|
ATTR_HVAC_ACTION,
|
||||||
ATTR_HVAC_MODE,
|
ATTR_HVAC_MODE,
|
||||||
ATTR_HVAC_MODES,
|
ATTR_HVAC_MODES,
|
||||||
|
ATTR_MAX_HUMIDITY,
|
||||||
ATTR_MAX_TEMP,
|
ATTR_MAX_TEMP,
|
||||||
ATTR_MIN_HUMIDITY,
|
ATTR_MIN_HUMIDITY,
|
||||||
ATTR_MIN_TEMP,
|
ATTR_MIN_TEMP,
|
||||||
@ -21,6 +22,7 @@ from homeassistant.components.climate import (
|
|||||||
ATTR_SWING_MODES,
|
ATTR_SWING_MODES,
|
||||||
ATTR_TARGET_TEMP_HIGH,
|
ATTR_TARGET_TEMP_HIGH,
|
||||||
ATTR_TARGET_TEMP_LOW,
|
ATTR_TARGET_TEMP_LOW,
|
||||||
|
DEFAULT_MAX_HUMIDITY,
|
||||||
DEFAULT_MAX_TEMP,
|
DEFAULT_MAX_TEMP,
|
||||||
DEFAULT_MIN_HUMIDITY,
|
DEFAULT_MIN_HUMIDITY,
|
||||||
DEFAULT_MIN_TEMP,
|
DEFAULT_MIN_TEMP,
|
||||||
@ -90,7 +92,7 @@ from .const import (
|
|||||||
SERV_FANV2,
|
SERV_FANV2,
|
||||||
SERV_THERMOSTAT,
|
SERV_THERMOSTAT,
|
||||||
)
|
)
|
||||||
from .util import temperature_to_homekit, temperature_to_states
|
from .util import get_min_max, temperature_to_homekit, temperature_to_states
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -208,7 +210,10 @@ class Thermostat(HomeAccessory):
|
|||||||
self.fan_chars: list[str] = []
|
self.fan_chars: list[str] = []
|
||||||
|
|
||||||
attributes = state.attributes
|
attributes = state.attributes
|
||||||
min_humidity = attributes.get(ATTR_MIN_HUMIDITY, DEFAULT_MIN_HUMIDITY)
|
min_humidity, _ = get_min_max(
|
||||||
|
attributes.get(ATTR_MIN_HUMIDITY, DEFAULT_MIN_HUMIDITY),
|
||||||
|
attributes.get(ATTR_MAX_HUMIDITY, DEFAULT_MAX_HUMIDITY),
|
||||||
|
)
|
||||||
features = attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
features = attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||||
|
|
||||||
if features & ClimateEntityFeature.TARGET_TEMPERATURE_RANGE:
|
if features & ClimateEntityFeature.TARGET_TEMPERATURE_RANGE:
|
||||||
@ -839,6 +844,9 @@ def _get_temperature_range_from_state(
|
|||||||
else:
|
else:
|
||||||
max_temp = default_max
|
max_temp = default_max
|
||||||
|
|
||||||
|
# Handle reversed temperature range
|
||||||
|
min_temp, max_temp = get_min_max(min_temp, max_temp)
|
||||||
|
|
||||||
# Homekit only supports 10-38, overwriting
|
# Homekit only supports 10-38, overwriting
|
||||||
# the max to appears to work, but less than 0 causes
|
# the max to appears to work, but less than 0 causes
|
||||||
# a crash on the home app
|
# a crash on the home app
|
||||||
|
@ -655,3 +655,14 @@ def state_changed_event_is_same_state(event: Event[EventStateChangedData]) -> bo
|
|||||||
old_state = event_data["old_state"]
|
old_state = event_data["old_state"]
|
||||||
new_state = event_data["new_state"]
|
new_state = event_data["new_state"]
|
||||||
return bool(new_state and old_state and new_state.state == old_state.state)
|
return bool(new_state and old_state and new_state.state == old_state.state)
|
||||||
|
|
||||||
|
|
||||||
|
def get_min_max(value1: float, value2: float) -> tuple[float, float]:
|
||||||
|
"""Return the minimum and maximum of two values.
|
||||||
|
|
||||||
|
HomeKit will go unavailable if the min and max are reversed
|
||||||
|
so we make sure the min is always the min and the max is always the max
|
||||||
|
as any mistakes made in integrations will cause the entire
|
||||||
|
bridge to go unavailable.
|
||||||
|
"""
|
||||||
|
return min(value1, value2), max(value1, value2)
|
||||||
|
@ -12,7 +12,7 @@
|
|||||||
"requirements": [
|
"requirements": [
|
||||||
"xknx==3.4.0",
|
"xknx==3.4.0",
|
||||||
"xknxproject==3.8.1",
|
"xknxproject==3.8.1",
|
||||||
"knx-frontend==2024.12.26.233449"
|
"knx-frontend==2025.1.18.164225"
|
||||||
],
|
],
|
||||||
"single_config_entry": true
|
"single_config_entry": true
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,7 @@
|
|||||||
"integration_type": "device",
|
"integration_type": "device",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["demetriek"],
|
"loggers": ["demetriek"],
|
||||||
"requirements": ["demetriek==1.1.1"],
|
"requirements": ["demetriek==1.2.0"],
|
||||||
"ssdp": [
|
"ssdp": [
|
||||||
{
|
{
|
||||||
"deviceType": "urn:schemas-upnp-org:device:LaMetric:1"
|
"deviceType": "urn:schemas-upnp-org:device:LaMetric:1"
|
||||||
|
@ -6,7 +6,7 @@ from collections.abc import Awaitable, Callable
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from demetriek import Device, LaMetricDevice
|
from demetriek import Device, LaMetricDevice, Range
|
||||||
|
|
||||||
from homeassistant.components.number import NumberEntity, NumberEntityDescription
|
from homeassistant.components.number import NumberEntity, NumberEntityDescription
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
@ -25,6 +25,7 @@ class LaMetricNumberEntityDescription(NumberEntityDescription):
|
|||||||
"""Class describing LaMetric number entities."""
|
"""Class describing LaMetric number entities."""
|
||||||
|
|
||||||
value_fn: Callable[[Device], int | None]
|
value_fn: Callable[[Device], int | None]
|
||||||
|
range_fn: Callable[[Device], Range | None]
|
||||||
has_fn: Callable[[Device], bool] = lambda device: True
|
has_fn: Callable[[Device], bool] = lambda device: True
|
||||||
set_value_fn: Callable[[LaMetricDevice, float], Awaitable[Any]]
|
set_value_fn: Callable[[LaMetricDevice, float], Awaitable[Any]]
|
||||||
|
|
||||||
@ -33,11 +34,9 @@ NUMBERS = [
|
|||||||
LaMetricNumberEntityDescription(
|
LaMetricNumberEntityDescription(
|
||||||
key="brightness",
|
key="brightness",
|
||||||
translation_key="brightness",
|
translation_key="brightness",
|
||||||
name="Brightness",
|
|
||||||
entity_category=EntityCategory.CONFIG,
|
entity_category=EntityCategory.CONFIG,
|
||||||
native_step=1,
|
native_step=1,
|
||||||
native_min_value=0,
|
range_fn=lambda device: device.display.brightness_limit,
|
||||||
native_max_value=100,
|
|
||||||
native_unit_of_measurement=PERCENTAGE,
|
native_unit_of_measurement=PERCENTAGE,
|
||||||
value_fn=lambda device: device.display.brightness,
|
value_fn=lambda device: device.display.brightness,
|
||||||
set_value_fn=lambda device, bri: device.display(brightness=int(bri)),
|
set_value_fn=lambda device, bri: device.display(brightness=int(bri)),
|
||||||
@ -45,11 +44,10 @@ NUMBERS = [
|
|||||||
LaMetricNumberEntityDescription(
|
LaMetricNumberEntityDescription(
|
||||||
key="volume",
|
key="volume",
|
||||||
translation_key="volume",
|
translation_key="volume",
|
||||||
name="Volume",
|
|
||||||
entity_category=EntityCategory.CONFIG,
|
entity_category=EntityCategory.CONFIG,
|
||||||
native_step=1,
|
native_step=1,
|
||||||
native_min_value=0,
|
range_fn=lambda device: device.audio.volume_range if device.audio else None,
|
||||||
native_max_value=100,
|
native_unit_of_measurement=PERCENTAGE,
|
||||||
has_fn=lambda device: bool(device.audio and device.audio.available),
|
has_fn=lambda device: bool(device.audio and device.audio.available),
|
||||||
value_fn=lambda device: device.audio.volume if device.audio else 0,
|
value_fn=lambda device: device.audio.volume if device.audio else 0,
|
||||||
set_value_fn=lambda api, volume: api.audio(volume=int(volume)),
|
set_value_fn=lambda api, volume: api.audio(volume=int(volume)),
|
||||||
@ -93,6 +91,20 @@ class LaMetricNumberEntity(LaMetricEntity, NumberEntity):
|
|||||||
"""Return the number value."""
|
"""Return the number value."""
|
||||||
return self.entity_description.value_fn(self.coordinator.data)
|
return self.entity_description.value_fn(self.coordinator.data)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def native_min_value(self) -> int:
|
||||||
|
"""Return the min range."""
|
||||||
|
if limits := self.entity_description.range_fn(self.coordinator.data):
|
||||||
|
return limits.range_min
|
||||||
|
return 0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def native_max_value(self) -> int:
|
||||||
|
"""Return the max range."""
|
||||||
|
if limits := self.entity_description.range_fn(self.coordinator.data):
|
||||||
|
return limits.range_max
|
||||||
|
return 100
|
||||||
|
|
||||||
@lametric_exception_handler
|
@lametric_exception_handler
|
||||||
async def async_set_native_value(self, value: float) -> None:
|
async def async_set_native_value(self, value: float) -> None:
|
||||||
"""Change to new number value."""
|
"""Change to new number value."""
|
||||||
|
@ -66,6 +66,14 @@
|
|||||||
"name": "Dismiss all notifications"
|
"name": "Dismiss all notifications"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"number": {
|
||||||
|
"brightness": {
|
||||||
|
"name": "Brightness"
|
||||||
|
},
|
||||||
|
"volume": {
|
||||||
|
"name": "Volume"
|
||||||
|
}
|
||||||
|
},
|
||||||
"sensor": {
|
"sensor": {
|
||||||
"rssi": {
|
"rssi": {
|
||||||
"name": "Wi-Fi signal"
|
"name": "Wi-Fi signal"
|
||||||
|
@ -8,6 +8,6 @@
|
|||||||
"iot_class": "calculated",
|
"iot_class": "calculated",
|
||||||
"loggers": ["yt_dlp"],
|
"loggers": ["yt_dlp"],
|
||||||
"quality_scale": "internal",
|
"quality_scale": "internal",
|
||||||
"requirements": ["yt-dlp[default]==2024.12.23"],
|
"requirements": ["yt-dlp[default]==2025.01.15"],
|
||||||
"single_config_entry": true
|
"single_config_entry": true
|
||||||
}
|
}
|
||||||
|
@ -179,14 +179,14 @@ class MqttNumber(MqttEntity, RestoreNumber):
|
|||||||
return
|
return
|
||||||
|
|
||||||
if num_value is not None and (
|
if num_value is not None and (
|
||||||
num_value < self.min_value or num_value > self.max_value
|
num_value < self.native_min_value or num_value > self.native_max_value
|
||||||
):
|
):
|
||||||
_LOGGER.error(
|
_LOGGER.error(
|
||||||
"Invalid value for %s: %s (range %s - %s)",
|
"Invalid value for %s: %s (range %s - %s)",
|
||||||
self.entity_id,
|
self.entity_id,
|
||||||
num_value,
|
num_value,
|
||||||
self.min_value,
|
self.native_min_value,
|
||||||
self.max_value,
|
self.native_max_value,
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -112,7 +112,7 @@ class NikoHomeControlLight(NikoHomeControlEntity, LightEntity):
|
|||||||
|
|
||||||
def turn_on(self, **kwargs: Any) -> None:
|
def turn_on(self, **kwargs: Any) -> None:
|
||||||
"""Instruct the light to turn on."""
|
"""Instruct the light to turn on."""
|
||||||
self._action.turn_on(kwargs.get(ATTR_BRIGHTNESS, 255) / 2.55)
|
self._action.turn_on(round(kwargs.get(ATTR_BRIGHTNESS, 255) / 2.55))
|
||||||
|
|
||||||
def turn_off(self, **kwargs: Any) -> None:
|
def turn_off(self, **kwargs: Any) -> None:
|
||||||
"""Instruct the light to turn off."""
|
"""Instruct the light to turn off."""
|
||||||
|
@ -6,5 +6,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/niko_home_control",
|
"documentation": "https://www.home-assistant.io/integrations/niko_home_control",
|
||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"loggers": ["nikohomecontrol"],
|
"loggers": ["nikohomecontrol"],
|
||||||
"requirements": ["nhc==0.3.2"]
|
"requirements": ["nhc==0.3.4"]
|
||||||
}
|
}
|
||||||
|
@ -7,5 +7,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/nmap_tracker",
|
"documentation": "https://www.home-assistant.io/integrations/nmap_tracker",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["nmap"],
|
"loggers": ["nmap"],
|
||||||
"requirements": ["netmap==0.7.0.2", "getmac==0.9.4", "aiooui==0.1.7"]
|
"requirements": ["netmap==0.7.0.2", "getmac==0.9.4", "aiooui==0.1.9"]
|
||||||
}
|
}
|
||||||
|
@ -61,7 +61,8 @@ MODEL_NAMES = [ # https://ollama.com/library
|
|||||||
"goliath",
|
"goliath",
|
||||||
"granite-code",
|
"granite-code",
|
||||||
"granite3-dense",
|
"granite3-dense",
|
||||||
"granite3-guardian" "granite3-moe",
|
"granite3-guardian",
|
||||||
|
"granite3-moe",
|
||||||
"hermes3",
|
"hermes3",
|
||||||
"internlm2",
|
"internlm2",
|
||||||
"llama-guard3",
|
"llama-guard3",
|
||||||
|
@ -263,16 +263,22 @@ class ONVIFDevice:
|
|||||||
LOGGER.warning("%s: Could not retrieve date/time on this camera", self.name)
|
LOGGER.warning("%s: Could not retrieve date/time on this camera", self.name)
|
||||||
return
|
return
|
||||||
|
|
||||||
cam_date = dt.datetime(
|
try:
|
||||||
cdate.Date.Year,
|
cam_date = dt.datetime(
|
||||||
cdate.Date.Month,
|
cdate.Date.Year,
|
||||||
cdate.Date.Day,
|
cdate.Date.Month,
|
||||||
cdate.Time.Hour,
|
cdate.Date.Day,
|
||||||
cdate.Time.Minute,
|
cdate.Time.Hour,
|
||||||
cdate.Time.Second,
|
cdate.Time.Minute,
|
||||||
0,
|
cdate.Time.Second,
|
||||||
tzone,
|
0,
|
||||||
)
|
tzone,
|
||||||
|
)
|
||||||
|
except ValueError as err:
|
||||||
|
LOGGER.warning(
|
||||||
|
"%s: Could not parse date/time from camera: %s", self.name, err
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
cam_date_utc = cam_date.astimezone(dt_util.UTC)
|
cam_date_utc = cam_date.astimezone(dt_util.UTC)
|
||||||
|
|
||||||
|
@ -8,5 +8,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/onvif",
|
"documentation": "https://www.home-assistant.io/integrations/onvif",
|
||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"loggers": ["onvif", "wsdiscovery", "zeep"],
|
"loggers": ["onvif", "wsdiscovery", "zeep"],
|
||||||
"requirements": ["onvif-zeep-async==3.1.13", "WSDiscovery==2.0.0"]
|
"requirements": ["onvif-zeep-async==3.2.3", "WSDiscovery==2.0.0"]
|
||||||
}
|
}
|
||||||
|
@ -119,5 +119,5 @@ class HitachiAirToWaterHeatingZone(OverkizEntity, ClimateEntity):
|
|||||||
temperature = cast(float, kwargs.get(ATTR_TEMPERATURE))
|
temperature = cast(float, kwargs.get(ATTR_TEMPERATURE))
|
||||||
|
|
||||||
await self.executor.async_execute_command(
|
await self.executor.async_execute_command(
|
||||||
OverkizCommand.SET_THERMOSTAT_SETTING_CONTROL_ZONE_1, int(temperature)
|
OverkizCommand.SET_THERMOSTAT_SETTING_CONTROL_ZONE_1, float(temperature)
|
||||||
)
|
)
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
"dependencies": ["usb"],
|
"dependencies": ["usb"],
|
||||||
"documentation": "https://www.home-assistant.io/integrations/rainforest_raven",
|
"documentation": "https://www.home-assistant.io/integrations/rainforest_raven",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"requirements": ["aioraven==0.7.0"],
|
"requirements": ["aioraven==0.7.1"],
|
||||||
"usb": [
|
"usb": [
|
||||||
{
|
{
|
||||||
"vid": "0403",
|
"vid": "0403",
|
||||||
|
@ -209,10 +209,7 @@ class RfxtrxOptionsFlow(OptionsFlow):
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
errors[CONF_COMMAND_OFF] = "invalid_input_2262_off"
|
errors[CONF_COMMAND_OFF] = "invalid_input_2262_off"
|
||||||
|
|
||||||
try:
|
off_delay = user_input.get(CONF_OFF_DELAY)
|
||||||
off_delay = none_or_int(user_input.get(CONF_OFF_DELAY), 10)
|
|
||||||
except ValueError:
|
|
||||||
errors[CONF_OFF_DELAY] = "invalid_input_off_delay"
|
|
||||||
|
|
||||||
if not errors:
|
if not errors:
|
||||||
devices = {}
|
devices = {}
|
||||||
@ -252,11 +249,11 @@ class RfxtrxOptionsFlow(OptionsFlow):
|
|||||||
vol.Optional(
|
vol.Optional(
|
||||||
CONF_OFF_DELAY,
|
CONF_OFF_DELAY,
|
||||||
description={"suggested_value": device_data[CONF_OFF_DELAY]},
|
description={"suggested_value": device_data[CONF_OFF_DELAY]},
|
||||||
): str,
|
): int,
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
off_delay_schema = {
|
off_delay_schema = {
|
||||||
vol.Optional(CONF_OFF_DELAY): str,
|
vol.Optional(CONF_OFF_DELAY): int,
|
||||||
}
|
}
|
||||||
data_schema.update(off_delay_schema)
|
data_schema.update(off_delay_schema)
|
||||||
|
|
||||||
|
@ -68,7 +68,6 @@
|
|||||||
"invalid_event_code": "Invalid event code",
|
"invalid_event_code": "Invalid event code",
|
||||||
"invalid_input_2262_on": "Invalid input for command on",
|
"invalid_input_2262_on": "Invalid input for command on",
|
||||||
"invalid_input_2262_off": "Invalid input for command off",
|
"invalid_input_2262_off": "Invalid input for command off",
|
||||||
"invalid_input_off_delay": "Invalid input for off delay",
|
|
||||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -10,12 +10,16 @@ import logging
|
|||||||
|
|
||||||
from aiohttp.client_exceptions import ClientConnectionError, ClientResponseError
|
from aiohttp.client_exceptions import ClientConnectionError, ClientResponseError
|
||||||
from pysmartapp.event import EVENT_TYPE_DEVICE
|
from pysmartapp.event import EVENT_TYPE_DEVICE
|
||||||
from pysmartthings import Attribute, Capability, SmartThings
|
from pysmartthings import APIInvalidGrant, Attribute, Capability, SmartThings
|
||||||
|
|
||||||
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
||||||
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_CLIENT_ID, CONF_CLIENT_SECRET
|
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_CLIENT_ID, CONF_CLIENT_SECRET
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import ConfigEntryNotReady
|
from homeassistant.exceptions import (
|
||||||
|
ConfigEntryAuthFailed,
|
||||||
|
ConfigEntryError,
|
||||||
|
ConfigEntryNotReady,
|
||||||
|
)
|
||||||
from homeassistant.helpers import config_validation as cv
|
from homeassistant.helpers import config_validation as cv
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||||
@ -106,7 +110,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
# to import the modules.
|
# to import the modules.
|
||||||
await async_get_loaded_integration(hass, DOMAIN).async_get_platforms(PLATFORMS)
|
await async_get_loaded_integration(hass, DOMAIN).async_get_platforms(PLATFORMS)
|
||||||
|
|
||||||
remove_entry = False
|
|
||||||
try:
|
try:
|
||||||
# See if the app is already setup. This occurs when there are
|
# See if the app is already setup. This occurs when there are
|
||||||
# installs in multiple SmartThings locations (valid use-case)
|
# installs in multiple SmartThings locations (valid use-case)
|
||||||
@ -175,34 +178,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
broker.connect()
|
broker.connect()
|
||||||
hass.data[DOMAIN][DATA_BROKERS][entry.entry_id] = broker
|
hass.data[DOMAIN][DATA_BROKERS][entry.entry_id] = broker
|
||||||
|
|
||||||
|
except APIInvalidGrant as ex:
|
||||||
|
raise ConfigEntryAuthFailed from ex
|
||||||
except ClientResponseError as ex:
|
except ClientResponseError as ex:
|
||||||
if ex.status in (HTTPStatus.UNAUTHORIZED, HTTPStatus.FORBIDDEN):
|
if ex.status in (HTTPStatus.UNAUTHORIZED, HTTPStatus.FORBIDDEN):
|
||||||
_LOGGER.exception(
|
raise ConfigEntryError(
|
||||||
(
|
"The access token is no longer valid. Please remove the integration and set up again."
|
||||||
"Unable to setup configuration entry '%s' - please reconfigure the"
|
) from ex
|
||||||
" integration"
|
_LOGGER.debug(ex, exc_info=True)
|
||||||
),
|
raise ConfigEntryNotReady from ex
|
||||||
entry.title,
|
|
||||||
)
|
|
||||||
remove_entry = True
|
|
||||||
else:
|
|
||||||
_LOGGER.debug(ex, exc_info=True)
|
|
||||||
raise ConfigEntryNotReady from ex
|
|
||||||
except (ClientConnectionError, RuntimeWarning) as ex:
|
except (ClientConnectionError, RuntimeWarning) as ex:
|
||||||
_LOGGER.debug(ex, exc_info=True)
|
_LOGGER.debug(ex, exc_info=True)
|
||||||
raise ConfigEntryNotReady from ex
|
raise ConfigEntryNotReady from ex
|
||||||
|
|
||||||
if remove_entry:
|
|
||||||
hass.async_create_task(hass.config_entries.async_remove(entry.entry_id))
|
|
||||||
# only create new flow if there isn't a pending one for SmartThings.
|
|
||||||
if not hass.config_entries.flow.async_progress_by_handler(DOMAIN):
|
|
||||||
hass.async_create_task(
|
|
||||||
hass.config_entries.flow.async_init(
|
|
||||||
DOMAIN, context={"source": SOURCE_IMPORT}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return False
|
|
||||||
|
|
||||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
"""Config flow to configure SmartThings."""
|
"""Config flow to configure SmartThings."""
|
||||||
|
|
||||||
|
from collections.abc import Mapping
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
import logging
|
import logging
|
||||||
from typing import Any
|
from typing import Any
|
||||||
@ -9,7 +10,7 @@ from pysmartthings import APIResponseError, AppOAuth, SmartThings
|
|||||||
from pysmartthings.installedapp import format_install_url
|
from pysmartthings.installedapp import format_install_url
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlow, ConfigFlowResult
|
||||||
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_CLIENT_ID, CONF_CLIENT_SECRET
|
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_CLIENT_ID, CONF_CLIENT_SECRET
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
|
|
||||||
@ -213,7 +214,10 @@ class SmartThingsFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||||||
url = format_install_url(self.app_id, self.location_id)
|
url = format_install_url(self.app_id, self.location_id)
|
||||||
return self.async_external_step(step_id="authorize", url=url)
|
return self.async_external_step(step_id="authorize", url=url)
|
||||||
|
|
||||||
return self.async_external_step_done(next_step_id="install")
|
next_step_id = "install"
|
||||||
|
if self.source == SOURCE_REAUTH:
|
||||||
|
next_step_id = "update"
|
||||||
|
return self.async_external_step_done(next_step_id=next_step_id)
|
||||||
|
|
||||||
def _show_step_pat(self, errors):
|
def _show_step_pat(self, errors):
|
||||||
if self.access_token is None:
|
if self.access_token is None:
|
||||||
@ -240,6 +244,41 @@ class SmartThingsFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def async_step_reauth(
|
||||||
|
self, entry_data: Mapping[str, Any]
|
||||||
|
) -> ConfigFlowResult:
|
||||||
|
"""Handle re-authentication of an existing config entry."""
|
||||||
|
return await self.async_step_reauth_confirm()
|
||||||
|
|
||||||
|
async def async_step_reauth_confirm(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> ConfigFlowResult:
|
||||||
|
"""Handle re-authentication of an existing config entry."""
|
||||||
|
if user_input is None:
|
||||||
|
return self.async_show_form(step_id="reauth_confirm")
|
||||||
|
self.app_id = self._get_reauth_entry().data[CONF_APP_ID]
|
||||||
|
self.location_id = self._get_reauth_entry().data[CONF_LOCATION_ID]
|
||||||
|
self._set_confirm_only()
|
||||||
|
return await self.async_step_authorize()
|
||||||
|
|
||||||
|
async def async_step_update(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> ConfigFlowResult:
|
||||||
|
"""Handle re-authentication of an existing config entry."""
|
||||||
|
return await self.async_step_update_confirm()
|
||||||
|
|
||||||
|
async def async_step_update_confirm(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> ConfigFlowResult:
|
||||||
|
"""Handle re-authentication of an existing config entry."""
|
||||||
|
if user_input is None:
|
||||||
|
self._set_confirm_only()
|
||||||
|
return self.async_show_form(step_id="update_confirm")
|
||||||
|
entry = self._get_reauth_entry()
|
||||||
|
return self.async_update_reload_and_abort(
|
||||||
|
entry, data_updates={CONF_REFRESH_TOKEN: self.refresh_token}
|
||||||
|
)
|
||||||
|
|
||||||
async def async_step_install(
|
async def async_step_install(
|
||||||
self, user_input: dict[str, Any] | None = None
|
self, user_input: dict[str, Any] | None = None
|
||||||
) -> ConfigFlowResult:
|
) -> ConfigFlowResult:
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
"""SmartApp functionality to receive cloud-push notifications."""
|
"""SmartApp functionality to receive cloud-push notifications."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import functools
|
import functools
|
||||||
import logging
|
import logging
|
||||||
@ -27,6 +29,7 @@ from pysmartthings import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
from homeassistant.components import cloud, webhook
|
from homeassistant.components import cloud, webhook
|
||||||
|
from homeassistant.config_entries import ConfigFlowResult
|
||||||
from homeassistant.const import CONF_WEBHOOK_ID
|
from homeassistant.const import CONF_WEBHOOK_ID
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
@ -400,7 +403,7 @@ async def smartapp_sync_subscriptions(
|
|||||||
_LOGGER.debug("Subscriptions for app '%s' are up-to-date", installed_app_id)
|
_LOGGER.debug("Subscriptions for app '%s' are up-to-date", installed_app_id)
|
||||||
|
|
||||||
|
|
||||||
async def _continue_flow(
|
async def _find_and_continue_flow(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
app_id: str,
|
app_id: str,
|
||||||
location_id: str,
|
location_id: str,
|
||||||
@ -418,24 +421,34 @@ async def _continue_flow(
|
|||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
if flow is not None:
|
if flow is not None:
|
||||||
await hass.config_entries.flow.async_configure(
|
await _continue_flow(hass, app_id, installed_app_id, refresh_token, flow)
|
||||||
flow["flow_id"],
|
|
||||||
{
|
|
||||||
CONF_INSTALLED_APP_ID: installed_app_id,
|
async def _continue_flow(
|
||||||
CONF_REFRESH_TOKEN: refresh_token,
|
hass: HomeAssistant,
|
||||||
},
|
app_id: str,
|
||||||
)
|
installed_app_id: str,
|
||||||
_LOGGER.debug(
|
refresh_token: str,
|
||||||
"Continued config flow '%s' for SmartApp '%s' under parent app '%s'",
|
flow: ConfigFlowResult,
|
||||||
flow["flow_id"],
|
) -> None:
|
||||||
installed_app_id,
|
await hass.config_entries.flow.async_configure(
|
||||||
app_id,
|
flow["flow_id"],
|
||||||
)
|
{
|
||||||
|
CONF_INSTALLED_APP_ID: installed_app_id,
|
||||||
|
CONF_REFRESH_TOKEN: refresh_token,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Continued config flow '%s' for SmartApp '%s' under parent app '%s'",
|
||||||
|
flow["flow_id"],
|
||||||
|
installed_app_id,
|
||||||
|
app_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def smartapp_install(hass: HomeAssistant, req, resp, app):
|
async def smartapp_install(hass: HomeAssistant, req, resp, app):
|
||||||
"""Handle a SmartApp installation and continue the config flow."""
|
"""Handle a SmartApp installation and continue the config flow."""
|
||||||
await _continue_flow(
|
await _find_and_continue_flow(
|
||||||
hass, app.app_id, req.location_id, req.installed_app_id, req.refresh_token
|
hass, app.app_id, req.location_id, req.installed_app_id, req.refresh_token
|
||||||
)
|
)
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
@ -447,6 +460,27 @@ async def smartapp_install(hass: HomeAssistant, req, resp, app):
|
|||||||
|
|
||||||
async def smartapp_update(hass: HomeAssistant, req, resp, app):
|
async def smartapp_update(hass: HomeAssistant, req, resp, app):
|
||||||
"""Handle a SmartApp update and either update the entry or continue the flow."""
|
"""Handle a SmartApp update and either update the entry or continue the flow."""
|
||||||
|
unique_id = format_unique_id(app.app_id, req.location_id)
|
||||||
|
flow = next(
|
||||||
|
(
|
||||||
|
flow
|
||||||
|
for flow in hass.config_entries.flow.async_progress_by_handler(DOMAIN)
|
||||||
|
if flow["context"].get("unique_id") == unique_id
|
||||||
|
and flow["step_id"] == "authorize"
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
if flow is not None:
|
||||||
|
await _continue_flow(
|
||||||
|
hass, app.app_id, req.installed_app_id, req.refresh_token, flow
|
||||||
|
)
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Continued reauth flow '%s' for SmartApp '%s' under parent app '%s'",
|
||||||
|
flow["flow_id"],
|
||||||
|
req.installed_app_id,
|
||||||
|
app.app_id,
|
||||||
|
)
|
||||||
|
return
|
||||||
entry = next(
|
entry = next(
|
||||||
(
|
(
|
||||||
entry
|
entry
|
||||||
@ -466,7 +500,7 @@ async def smartapp_update(hass: HomeAssistant, req, resp, app):
|
|||||||
app.app_id,
|
app.app_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
await _continue_flow(
|
await _find_and_continue_flow(
|
||||||
hass, app.app_id, req.location_id, req.installed_app_id, req.refresh_token
|
hass, app.app_id, req.location_id, req.installed_app_id, req.refresh_token
|
||||||
)
|
)
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
},
|
},
|
||||||
"pat": {
|
"pat": {
|
||||||
"title": "Enter Personal Access Token",
|
"title": "Enter Personal Access Token",
|
||||||
"description": "Please enter a SmartThings [Personal Access Token]({token_url}) that has been created per the [instructions]({component_url}). This will be used to create the Home Assistant integration within your SmartThings account.",
|
"description": "Please enter a SmartThings [Personal Access Token]({token_url}) that has been created per the [instructions]({component_url}). This will be used to create the Home Assistant integration within your SmartThings account.\n\n**Please note that all Personal Access Tokens created after 30 December 2024 are only valid for 24 hours, after which the integration will stop working. We are working on a fix.**",
|
||||||
"data": {
|
"data": {
|
||||||
"access_token": "[%key:common::config_flow::data::access_token%]"
|
"access_token": "[%key:common::config_flow::data::access_token%]"
|
||||||
}
|
}
|
||||||
@ -17,11 +17,20 @@
|
|||||||
"description": "Please select the SmartThings Location you wish to add to Home Assistant. We will then open a new window and ask you to login and authorize installation of the Home Assistant integration into the selected location.",
|
"description": "Please select the SmartThings Location you wish to add to Home Assistant. We will then open a new window and ask you to login and authorize installation of the Home Assistant integration into the selected location.",
|
||||||
"data": { "location_id": "[%key:common::config_flow::data::location%]" }
|
"data": { "location_id": "[%key:common::config_flow::data::location%]" }
|
||||||
},
|
},
|
||||||
"authorize": { "title": "Authorize Home Assistant" }
|
"authorize": { "title": "Authorize Home Assistant" },
|
||||||
|
"reauth_confirm": {
|
||||||
|
"title": "Reauthorize Home Assistant",
|
||||||
|
"description": "You are about to reauthorize Home Assistant with SmartThings. This will require you to log in and authorize the integration again."
|
||||||
|
},
|
||||||
|
"update_confirm": {
|
||||||
|
"title": "Finish reauthentication",
|
||||||
|
"description": "You have almost successfully reauthorized Home Assistant with SmartThings. Please press the button down below to finish the process."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"abort": {
|
"abort": {
|
||||||
"invalid_webhook_url": "Home Assistant is not configured correctly to receive updates from SmartThings. The webhook URL is invalid:\n> {webhook_url}\n\nPlease update your configuration per the [instructions]({component_url}), restart Home Assistant, and try again.",
|
"invalid_webhook_url": "Home Assistant is not configured correctly to receive updates from SmartThings. The webhook URL is invalid:\n> {webhook_url}\n\nPlease update your configuration per the [instructions]({component_url}), restart Home Assistant, and try again.",
|
||||||
"no_available_locations": "There are no available SmartThings Locations to set up in Home Assistant."
|
"no_available_locations": "There are no available SmartThings Locations to set up in Home Assistant.",
|
||||||
|
"reauth_successful": "Home Assistant has been successfully reauthorized with SmartThings."
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"token_invalid_format": "The token must be in the UID/GUID format",
|
"token_invalid_format": "The token must be in the UID/GUID format",
|
||||||
|
@ -6,6 +6,6 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/switchbot_cloud",
|
"documentation": "https://www.home-assistant.io/integrations/switchbot_cloud",
|
||||||
"integration_type": "hub",
|
"integration_type": "hub",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["switchbot-api"],
|
"loggers": ["switchbot_api"],
|
||||||
"requirements": ["switchbot-api==2.2.1"]
|
"requirements": ["switchbot-api==2.3.1"]
|
||||||
}
|
}
|
||||||
|
@ -181,7 +181,6 @@ EVENT_DESCRIPTIONS: tuple[ProtectEventEntityDescription, ...] = (
|
|||||||
ProtectEventEntityDescription(
|
ProtectEventEntityDescription(
|
||||||
key="nfc",
|
key="nfc",
|
||||||
translation_key="nfc",
|
translation_key="nfc",
|
||||||
device_class=EventDeviceClass.DOORBELL,
|
|
||||||
icon="mdi:nfc",
|
icon="mdi:nfc",
|
||||||
ufp_required_field="feature_flags.support_nfc",
|
ufp_required_field="feature_flags.support_nfc",
|
||||||
ufp_event_obj="last_nfc_card_scanned_event",
|
ufp_event_obj="last_nfc_card_scanned_event",
|
||||||
@ -191,7 +190,6 @@ EVENT_DESCRIPTIONS: tuple[ProtectEventEntityDescription, ...] = (
|
|||||||
ProtectEventEntityDescription(
|
ProtectEventEntityDescription(
|
||||||
key="fingerprint",
|
key="fingerprint",
|
||||||
translation_key="fingerprint",
|
translation_key="fingerprint",
|
||||||
device_class=EventDeviceClass.DOORBELL,
|
|
||||||
icon="mdi:fingerprint",
|
icon="mdi:fingerprint",
|
||||||
ufp_required_field="feature_flags.has_fingerprint_sensor",
|
ufp_required_field="feature_flags.has_fingerprint_sensor",
|
||||||
ufp_event_obj="last_fingerprint_identified_event",
|
ufp_event_obj="last_fingerprint_identified_event",
|
||||||
|
@ -90,7 +90,7 @@ DESCRIPTIONS: list[WatergateSensorEntityDescription] = [
|
|||||||
WatergateSensorEntityDescription(
|
WatergateSensorEntityDescription(
|
||||||
value_fn=lambda data: (
|
value_fn=lambda data: (
|
||||||
dt_util.as_utc(
|
dt_util.as_utc(
|
||||||
dt_util.now() - timedelta(microseconds=data.networking.wifi_uptime)
|
dt_util.now() - timedelta(milliseconds=data.networking.wifi_uptime)
|
||||||
)
|
)
|
||||||
if data.networking
|
if data.networking
|
||||||
else None
|
else None
|
||||||
@ -104,7 +104,7 @@ DESCRIPTIONS: list[WatergateSensorEntityDescription] = [
|
|||||||
WatergateSensorEntityDescription(
|
WatergateSensorEntityDescription(
|
||||||
value_fn=lambda data: (
|
value_fn=lambda data: (
|
||||||
dt_util.as_utc(
|
dt_util.as_utc(
|
||||||
dt_util.now() - timedelta(microseconds=data.networking.mqtt_uptime)
|
dt_util.now() - timedelta(milliseconds=data.networking.mqtt_uptime)
|
||||||
)
|
)
|
||||||
if data.networking
|
if data.networking
|
||||||
else None
|
else None
|
||||||
@ -158,7 +158,11 @@ DESCRIPTIONS: list[WatergateSensorEntityDescription] = [
|
|||||||
),
|
),
|
||||||
WatergateSensorEntityDescription(
|
WatergateSensorEntityDescription(
|
||||||
value_fn=lambda data: (
|
value_fn=lambda data: (
|
||||||
PowerSupplyMode(data.state.power_supply.replace("+", "_"))
|
PowerSupplyMode(
|
||||||
|
data.state.power_supply.replace("+", "_").replace(
|
||||||
|
"external_battery", "battery_external"
|
||||||
|
)
|
||||||
|
)
|
||||||
if data.state
|
if data.state
|
||||||
else None
|
else None
|
||||||
),
|
),
|
||||||
|
@ -16,6 +16,7 @@ from aiohttp import ClientError
|
|||||||
from aiohttp.hdrs import METH_POST
|
from aiohttp.hdrs import METH_POST
|
||||||
from aiohttp.web import Request, Response
|
from aiohttp.web import Request, Response
|
||||||
from aiowithings import NotificationCategory, WithingsClient
|
from aiowithings import NotificationCategory, WithingsClient
|
||||||
|
from aiowithings.exceptions import WithingsError
|
||||||
from aiowithings.util import to_enum
|
from aiowithings.util import to_enum
|
||||||
from yarl import URL
|
from yarl import URL
|
||||||
|
|
||||||
@ -223,10 +224,13 @@ class WithingsWebhookManager:
|
|||||||
"Unregister Withings webhook (%s)", self.entry.data[CONF_WEBHOOK_ID]
|
"Unregister Withings webhook (%s)", self.entry.data[CONF_WEBHOOK_ID]
|
||||||
)
|
)
|
||||||
webhook_unregister(self.hass, self.entry.data[CONF_WEBHOOK_ID])
|
webhook_unregister(self.hass, self.entry.data[CONF_WEBHOOK_ID])
|
||||||
await async_unsubscribe_webhooks(self.withings_data.client)
|
|
||||||
for coordinator in self.withings_data.coordinators:
|
for coordinator in self.withings_data.coordinators:
|
||||||
coordinator.webhook_subscription_listener(False)
|
coordinator.webhook_subscription_listener(False)
|
||||||
self._webhooks_registered = False
|
self._webhooks_registered = False
|
||||||
|
try:
|
||||||
|
await async_unsubscribe_webhooks(self.withings_data.client)
|
||||||
|
except WithingsError as ex:
|
||||||
|
LOGGER.warning("Failed to unsubscribe from Withings webhook: %s", ex)
|
||||||
|
|
||||||
async def register_webhook(
|
async def register_webhook(
|
||||||
self,
|
self,
|
||||||
|
@ -25,7 +25,7 @@ if TYPE_CHECKING:
|
|||||||
APPLICATION_NAME: Final = "HomeAssistant"
|
APPLICATION_NAME: Final = "HomeAssistant"
|
||||||
MAJOR_VERSION: Final = 2025
|
MAJOR_VERSION: Final = 2025
|
||||||
MINOR_VERSION: Final = 1
|
MINOR_VERSION: Final = 1
|
||||||
PATCH_VERSION: Final = "2"
|
PATCH_VERSION: Final = "3"
|
||||||
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
||||||
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
|
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
|
||||||
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0)
|
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0)
|
||||||
|
@ -67,9 +67,11 @@ class DiscoveryFlowHandler[_R: Awaitable[bool] | bool](config_entries.ConfigFlow
|
|||||||
in_progress = self._async_in_progress()
|
in_progress = self._async_in_progress()
|
||||||
|
|
||||||
if not (has_devices := bool(in_progress)):
|
if not (has_devices := bool(in_progress)):
|
||||||
has_devices = await cast(
|
discovery_result = self._discovery_function(self.hass)
|
||||||
"asyncio.Future[bool]", self._discovery_function(self.hass)
|
if isinstance(discovery_result, bool):
|
||||||
)
|
has_devices = discovery_result
|
||||||
|
else:
|
||||||
|
has_devices = await cast("asyncio.Future[bool]", discovery_result)
|
||||||
|
|
||||||
if not has_devices:
|
if not has_devices:
|
||||||
return self.async_abort(reason="no_devices_found")
|
return self.async_abort(reason="no_devices_found")
|
||||||
|
@ -1589,6 +1589,9 @@ class Script:
|
|||||||
target, referenced, script[CONF_SEQUENCE]
|
target, referenced, script[CONF_SEQUENCE]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
elif action == cv.SCRIPT_ACTION_SEQUENCE:
|
||||||
|
Script._find_referenced_target(target, referenced, step[CONF_SEQUENCE])
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def referenced_devices(self) -> set[str]:
|
def referenced_devices(self) -> set[str]:
|
||||||
"""Return a set of referenced devices."""
|
"""Return a set of referenced devices."""
|
||||||
@ -1636,6 +1639,9 @@ class Script:
|
|||||||
for script in step[CONF_PARALLEL]:
|
for script in step[CONF_PARALLEL]:
|
||||||
Script._find_referenced_devices(referenced, script[CONF_SEQUENCE])
|
Script._find_referenced_devices(referenced, script[CONF_SEQUENCE])
|
||||||
|
|
||||||
|
elif action == cv.SCRIPT_ACTION_SEQUENCE:
|
||||||
|
Script._find_referenced_devices(referenced, step[CONF_SEQUENCE])
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def referenced_entities(self) -> set[str]:
|
def referenced_entities(self) -> set[str]:
|
||||||
"""Return a set of referenced entities."""
|
"""Return a set of referenced entities."""
|
||||||
@ -1684,6 +1690,9 @@ class Script:
|
|||||||
for script in step[CONF_PARALLEL]:
|
for script in step[CONF_PARALLEL]:
|
||||||
Script._find_referenced_entities(referenced, script[CONF_SEQUENCE])
|
Script._find_referenced_entities(referenced, script[CONF_SEQUENCE])
|
||||||
|
|
||||||
|
elif action == cv.SCRIPT_ACTION_SEQUENCE:
|
||||||
|
Script._find_referenced_entities(referenced, step[CONF_SEQUENCE])
|
||||||
|
|
||||||
def run(
|
def run(
|
||||||
self, variables: _VarsType | None = None, context: Context | None = None
|
self, variables: _VarsType | None = None, context: Context | None = None
|
||||||
) -> None:
|
) -> None:
|
||||||
|
@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "homeassistant"
|
name = "homeassistant"
|
||||||
version = "2025.1.2"
|
version = "2025.1.3"
|
||||||
license = {text = "Apache-2.0"}
|
license = {text = "Apache-2.0"}
|
||||||
description = "Open-source home automation platform running on Python 3."
|
description = "Open-source home automation platform running on Python 3."
|
||||||
readme = "README.rst"
|
readme = "README.rst"
|
||||||
|
@ -182,7 +182,7 @@ aioairq==0.4.3
|
|||||||
aioairzone-cloud==0.6.10
|
aioairzone-cloud==0.6.10
|
||||||
|
|
||||||
# homeassistant.components.airzone
|
# homeassistant.components.airzone
|
||||||
aioairzone==0.9.7
|
aioairzone==0.9.9
|
||||||
|
|
||||||
# homeassistant.components.ambient_network
|
# homeassistant.components.ambient_network
|
||||||
# homeassistant.components.ambient_station
|
# homeassistant.components.ambient_station
|
||||||
@ -318,7 +318,7 @@ aiooncue==0.3.7
|
|||||||
aioopenexchangerates==0.6.8
|
aioopenexchangerates==0.6.8
|
||||||
|
|
||||||
# homeassistant.components.nmap_tracker
|
# homeassistant.components.nmap_tracker
|
||||||
aiooui==0.1.7
|
aiooui==0.1.9
|
||||||
|
|
||||||
# homeassistant.components.pegel_online
|
# homeassistant.components.pegel_online
|
||||||
aiopegelonline==0.1.1
|
aiopegelonline==0.1.1
|
||||||
@ -344,7 +344,7 @@ aiopyarr==23.4.0
|
|||||||
aioqsw==0.4.1
|
aioqsw==0.4.1
|
||||||
|
|
||||||
# homeassistant.components.rainforest_raven
|
# homeassistant.components.rainforest_raven
|
||||||
aioraven==0.7.0
|
aioraven==0.7.1
|
||||||
|
|
||||||
# homeassistant.components.recollect_waste
|
# homeassistant.components.recollect_waste
|
||||||
aiorecollect==2023.09.0
|
aiorecollect==2023.09.0
|
||||||
@ -738,7 +738,7 @@ debugpy==1.8.11
|
|||||||
# decora==0.6
|
# decora==0.6
|
||||||
|
|
||||||
# homeassistant.components.ecovacs
|
# homeassistant.components.ecovacs
|
||||||
deebot-client==10.1.0
|
deebot-client==11.0.0
|
||||||
|
|
||||||
# homeassistant.components.ihc
|
# homeassistant.components.ihc
|
||||||
# homeassistant.components.namecheapdns
|
# homeassistant.components.namecheapdns
|
||||||
@ -749,7 +749,7 @@ defusedxml==0.7.1
|
|||||||
deluge-client==1.10.2
|
deluge-client==1.10.2
|
||||||
|
|
||||||
# homeassistant.components.lametric
|
# homeassistant.components.lametric
|
||||||
demetriek==1.1.1
|
demetriek==1.2.0
|
||||||
|
|
||||||
# homeassistant.components.denonavr
|
# homeassistant.components.denonavr
|
||||||
denonavr==1.0.1
|
denonavr==1.0.1
|
||||||
@ -824,7 +824,7 @@ elgato==5.1.2
|
|||||||
eliqonline==1.2.2
|
eliqonline==1.2.2
|
||||||
|
|
||||||
# homeassistant.components.elkm1
|
# homeassistant.components.elkm1
|
||||||
elkm1-lib==2.2.10
|
elkm1-lib==2.2.11
|
||||||
|
|
||||||
# homeassistant.components.elmax
|
# homeassistant.components.elmax
|
||||||
elmax-api==0.0.6.4rc0
|
elmax-api==0.0.6.4rc0
|
||||||
@ -940,7 +940,7 @@ forecast-solar==4.0.0
|
|||||||
fortiosapi==1.0.5
|
fortiosapi==1.0.5
|
||||||
|
|
||||||
# homeassistant.components.freebox
|
# homeassistant.components.freebox
|
||||||
freebox-api==1.2.1
|
freebox-api==1.2.2
|
||||||
|
|
||||||
# homeassistant.components.free_mobile
|
# homeassistant.components.free_mobile
|
||||||
freesms==0.2.0
|
freesms==0.2.0
|
||||||
@ -1260,7 +1260,7 @@ kiwiki-client==0.1.1
|
|||||||
knocki==0.4.2
|
knocki==0.4.2
|
||||||
|
|
||||||
# homeassistant.components.knx
|
# homeassistant.components.knx
|
||||||
knx-frontend==2024.12.26.233449
|
knx-frontend==2025.1.18.164225
|
||||||
|
|
||||||
# homeassistant.components.konnected
|
# homeassistant.components.konnected
|
||||||
konnected==1.2.0
|
konnected==1.2.0
|
||||||
@ -1467,7 +1467,7 @@ nextcord==2.6.0
|
|||||||
nextdns==4.0.0
|
nextdns==4.0.0
|
||||||
|
|
||||||
# homeassistant.components.niko_home_control
|
# homeassistant.components.niko_home_control
|
||||||
nhc==0.3.2
|
nhc==0.3.4
|
||||||
|
|
||||||
# homeassistant.components.nibe_heatpump
|
# homeassistant.components.nibe_heatpump
|
||||||
nibe==2.14.0
|
nibe==2.14.0
|
||||||
@ -1537,7 +1537,7 @@ omnilogic==0.4.5
|
|||||||
ondilo==0.5.0
|
ondilo==0.5.0
|
||||||
|
|
||||||
# homeassistant.components.onvif
|
# homeassistant.components.onvif
|
||||||
onvif-zeep-async==3.1.13
|
onvif-zeep-async==3.2.3
|
||||||
|
|
||||||
# homeassistant.components.opengarage
|
# homeassistant.components.opengarage
|
||||||
open-garage==0.2.0
|
open-garage==0.2.0
|
||||||
@ -1794,7 +1794,7 @@ pyatmo==8.1.0
|
|||||||
pyatv==0.16.0
|
pyatv==0.16.0
|
||||||
|
|
||||||
# homeassistant.components.aussie_broadband
|
# homeassistant.components.aussie_broadband
|
||||||
pyaussiebb==0.1.4
|
pyaussiebb==0.1.5
|
||||||
|
|
||||||
# homeassistant.components.balboa
|
# homeassistant.components.balboa
|
||||||
pybalboa==1.0.2
|
pybalboa==1.0.2
|
||||||
@ -1965,7 +1965,7 @@ pyhaversion==22.8.0
|
|||||||
pyheos==0.7.2
|
pyheos==0.7.2
|
||||||
|
|
||||||
# homeassistant.components.hive
|
# homeassistant.components.hive
|
||||||
pyhiveapi==0.5.16
|
pyhive-integration==1.0.1
|
||||||
|
|
||||||
# homeassistant.components.homematic
|
# homeassistant.components.homematic
|
||||||
pyhomematic==0.1.77
|
pyhomematic==0.1.77
|
||||||
@ -2782,7 +2782,7 @@ surepy==0.9.0
|
|||||||
swisshydrodata==0.1.0
|
swisshydrodata==0.1.0
|
||||||
|
|
||||||
# homeassistant.components.switchbot_cloud
|
# homeassistant.components.switchbot_cloud
|
||||||
switchbot-api==2.2.1
|
switchbot-api==2.3.1
|
||||||
|
|
||||||
# homeassistant.components.synology_srm
|
# homeassistant.components.synology_srm
|
||||||
synology-srm==0.2.0
|
synology-srm==0.2.0
|
||||||
@ -3082,7 +3082,7 @@ youless-api==2.1.2
|
|||||||
youtubeaio==1.1.5
|
youtubeaio==1.1.5
|
||||||
|
|
||||||
# homeassistant.components.media_extractor
|
# homeassistant.components.media_extractor
|
||||||
yt-dlp[default]==2024.12.23
|
yt-dlp[default]==2025.01.15
|
||||||
|
|
||||||
# homeassistant.components.zabbix
|
# homeassistant.components.zabbix
|
||||||
zabbix-utils==2.0.2
|
zabbix-utils==2.0.2
|
||||||
|
@ -170,7 +170,7 @@ aioairq==0.4.3
|
|||||||
aioairzone-cloud==0.6.10
|
aioairzone-cloud==0.6.10
|
||||||
|
|
||||||
# homeassistant.components.airzone
|
# homeassistant.components.airzone
|
||||||
aioairzone==0.9.7
|
aioairzone==0.9.9
|
||||||
|
|
||||||
# homeassistant.components.ambient_network
|
# homeassistant.components.ambient_network
|
||||||
# homeassistant.components.ambient_station
|
# homeassistant.components.ambient_station
|
||||||
@ -300,7 +300,7 @@ aiooncue==0.3.7
|
|||||||
aioopenexchangerates==0.6.8
|
aioopenexchangerates==0.6.8
|
||||||
|
|
||||||
# homeassistant.components.nmap_tracker
|
# homeassistant.components.nmap_tracker
|
||||||
aiooui==0.1.7
|
aiooui==0.1.9
|
||||||
|
|
||||||
# homeassistant.components.pegel_online
|
# homeassistant.components.pegel_online
|
||||||
aiopegelonline==0.1.1
|
aiopegelonline==0.1.1
|
||||||
@ -326,7 +326,7 @@ aiopyarr==23.4.0
|
|||||||
aioqsw==0.4.1
|
aioqsw==0.4.1
|
||||||
|
|
||||||
# homeassistant.components.rainforest_raven
|
# homeassistant.components.rainforest_raven
|
||||||
aioraven==0.7.0
|
aioraven==0.7.1
|
||||||
|
|
||||||
# homeassistant.components.recollect_waste
|
# homeassistant.components.recollect_waste
|
||||||
aiorecollect==2023.09.0
|
aiorecollect==2023.09.0
|
||||||
@ -628,7 +628,7 @@ dbus-fast==2.24.3
|
|||||||
debugpy==1.8.11
|
debugpy==1.8.11
|
||||||
|
|
||||||
# homeassistant.components.ecovacs
|
# homeassistant.components.ecovacs
|
||||||
deebot-client==10.1.0
|
deebot-client==11.0.0
|
||||||
|
|
||||||
# homeassistant.components.ihc
|
# homeassistant.components.ihc
|
||||||
# homeassistant.components.namecheapdns
|
# homeassistant.components.namecheapdns
|
||||||
@ -639,7 +639,7 @@ defusedxml==0.7.1
|
|||||||
deluge-client==1.10.2
|
deluge-client==1.10.2
|
||||||
|
|
||||||
# homeassistant.components.lametric
|
# homeassistant.components.lametric
|
||||||
demetriek==1.1.1
|
demetriek==1.2.0
|
||||||
|
|
||||||
# homeassistant.components.denonavr
|
# homeassistant.components.denonavr
|
||||||
denonavr==1.0.1
|
denonavr==1.0.1
|
||||||
@ -699,7 +699,7 @@ elevenlabs==1.9.0
|
|||||||
elgato==5.1.2
|
elgato==5.1.2
|
||||||
|
|
||||||
# homeassistant.components.elkm1
|
# homeassistant.components.elkm1
|
||||||
elkm1-lib==2.2.10
|
elkm1-lib==2.2.11
|
||||||
|
|
||||||
# homeassistant.components.elmax
|
# homeassistant.components.elmax
|
||||||
elmax-api==0.0.6.4rc0
|
elmax-api==0.0.6.4rc0
|
||||||
@ -796,7 +796,7 @@ foobot_async==1.0.0
|
|||||||
forecast-solar==4.0.0
|
forecast-solar==4.0.0
|
||||||
|
|
||||||
# homeassistant.components.freebox
|
# homeassistant.components.freebox
|
||||||
freebox-api==1.2.1
|
freebox-api==1.2.2
|
||||||
|
|
||||||
# homeassistant.components.fritz
|
# homeassistant.components.fritz
|
||||||
# homeassistant.components.fritzbox_callmonitor
|
# homeassistant.components.fritzbox_callmonitor
|
||||||
@ -1062,7 +1062,7 @@ kegtron-ble==0.4.0
|
|||||||
knocki==0.4.2
|
knocki==0.4.2
|
||||||
|
|
||||||
# homeassistant.components.knx
|
# homeassistant.components.knx
|
||||||
knx-frontend==2024.12.26.233449
|
knx-frontend==2025.1.18.164225
|
||||||
|
|
||||||
# homeassistant.components.konnected
|
# homeassistant.components.konnected
|
||||||
konnected==1.2.0
|
konnected==1.2.0
|
||||||
@ -1230,7 +1230,7 @@ nextcord==2.6.0
|
|||||||
nextdns==4.0.0
|
nextdns==4.0.0
|
||||||
|
|
||||||
# homeassistant.components.niko_home_control
|
# homeassistant.components.niko_home_control
|
||||||
nhc==0.3.2
|
nhc==0.3.4
|
||||||
|
|
||||||
# homeassistant.components.nibe_heatpump
|
# homeassistant.components.nibe_heatpump
|
||||||
nibe==2.14.0
|
nibe==2.14.0
|
||||||
@ -1285,7 +1285,7 @@ omnilogic==0.4.5
|
|||||||
ondilo==0.5.0
|
ondilo==0.5.0
|
||||||
|
|
||||||
# homeassistant.components.onvif
|
# homeassistant.components.onvif
|
||||||
onvif-zeep-async==3.1.13
|
onvif-zeep-async==3.2.3
|
||||||
|
|
||||||
# homeassistant.components.opengarage
|
# homeassistant.components.opengarage
|
||||||
open-garage==0.2.0
|
open-garage==0.2.0
|
||||||
@ -1474,7 +1474,7 @@ pyatmo==8.1.0
|
|||||||
pyatv==0.16.0
|
pyatv==0.16.0
|
||||||
|
|
||||||
# homeassistant.components.aussie_broadband
|
# homeassistant.components.aussie_broadband
|
||||||
pyaussiebb==0.1.4
|
pyaussiebb==0.1.5
|
||||||
|
|
||||||
# homeassistant.components.balboa
|
# homeassistant.components.balboa
|
||||||
pybalboa==1.0.2
|
pybalboa==1.0.2
|
||||||
@ -1594,7 +1594,7 @@ pyhaversion==22.8.0
|
|||||||
pyheos==0.7.2
|
pyheos==0.7.2
|
||||||
|
|
||||||
# homeassistant.components.hive
|
# homeassistant.components.hive
|
||||||
pyhiveapi==0.5.16
|
pyhive-integration==1.0.1
|
||||||
|
|
||||||
# homeassistant.components.homematic
|
# homeassistant.components.homematic
|
||||||
pyhomematic==0.1.77
|
pyhomematic==0.1.77
|
||||||
@ -2237,7 +2237,7 @@ sunweg==3.0.2
|
|||||||
surepy==0.9.0
|
surepy==0.9.0
|
||||||
|
|
||||||
# homeassistant.components.switchbot_cloud
|
# homeassistant.components.switchbot_cloud
|
||||||
switchbot-api==2.2.1
|
switchbot-api==2.3.1
|
||||||
|
|
||||||
# homeassistant.components.system_bridge
|
# homeassistant.components.system_bridge
|
||||||
systembridgeconnector==4.1.5
|
systembridgeconnector==4.1.5
|
||||||
@ -2477,7 +2477,7 @@ youless-api==2.1.2
|
|||||||
youtubeaio==1.1.5
|
youtubeaio==1.1.5
|
||||||
|
|
||||||
# homeassistant.components.media_extractor
|
# homeassistant.components.media_extractor
|
||||||
yt-dlp[default]==2024.12.23
|
yt-dlp[default]==2025.01.15
|
||||||
|
|
||||||
# homeassistant.components.zamg
|
# homeassistant.components.zamg
|
||||||
zamg==0.3.6
|
zamg==0.3.6
|
||||||
|
@ -140,6 +140,7 @@
|
|||||||
'heatStages': 1,
|
'heatStages': 1,
|
||||||
'heatangle': 0,
|
'heatangle': 0,
|
||||||
'humidity': 40,
|
'humidity': 40,
|
||||||
|
'master_zoneID': None,
|
||||||
'maxTemp': 30,
|
'maxTemp': 30,
|
||||||
'minTemp': 15,
|
'minTemp': 15,
|
||||||
'mode': 3,
|
'mode': 3,
|
||||||
|
@ -28,6 +28,7 @@ from aioairzone.const import (
|
|||||||
API_HEAT_STAGES,
|
API_HEAT_STAGES,
|
||||||
API_HUMIDITY,
|
API_HUMIDITY,
|
||||||
API_MAC,
|
API_MAC,
|
||||||
|
API_MASTER_ZONE_ID,
|
||||||
API_MAX_TEMP,
|
API_MAX_TEMP,
|
||||||
API_MIN_TEMP,
|
API_MIN_TEMP,
|
||||||
API_MODE,
|
API_MODE,
|
||||||
@ -214,6 +215,7 @@ HVAC_MOCK = {
|
|||||||
API_FLOOR_DEMAND: 0,
|
API_FLOOR_DEMAND: 0,
|
||||||
API_HEAT_ANGLE: 0,
|
API_HEAT_ANGLE: 0,
|
||||||
API_COLD_ANGLE: 0,
|
API_COLD_ANGLE: 0,
|
||||||
|
API_MASTER_ZONE_ID: None,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -474,6 +474,108 @@
|
|||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
# ---
|
# ---
|
||||||
|
# name: test_stt_language_used_instead_of_conversation_language
|
||||||
|
list([
|
||||||
|
dict({
|
||||||
|
'data': dict({
|
||||||
|
'language': 'en',
|
||||||
|
'pipeline': <ANY>,
|
||||||
|
}),
|
||||||
|
'type': <PipelineEventType.RUN_START: 'run-start'>,
|
||||||
|
}),
|
||||||
|
dict({
|
||||||
|
'data': dict({
|
||||||
|
'conversation_id': None,
|
||||||
|
'device_id': None,
|
||||||
|
'engine': 'conversation.home_assistant',
|
||||||
|
'intent_input': 'test input',
|
||||||
|
'language': 'en-US',
|
||||||
|
'prefer_local_intents': False,
|
||||||
|
}),
|
||||||
|
'type': <PipelineEventType.INTENT_START: 'intent-start'>,
|
||||||
|
}),
|
||||||
|
dict({
|
||||||
|
'data': dict({
|
||||||
|
'intent_output': dict({
|
||||||
|
'conversation_id': None,
|
||||||
|
'response': dict({
|
||||||
|
'card': dict({
|
||||||
|
}),
|
||||||
|
'data': dict({
|
||||||
|
'failed': list([
|
||||||
|
]),
|
||||||
|
'success': list([
|
||||||
|
]),
|
||||||
|
'targets': list([
|
||||||
|
]),
|
||||||
|
}),
|
||||||
|
'language': 'en',
|
||||||
|
'response_type': 'action_done',
|
||||||
|
'speech': dict({
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
'processed_locally': True,
|
||||||
|
}),
|
||||||
|
'type': <PipelineEventType.INTENT_END: 'intent-end'>,
|
||||||
|
}),
|
||||||
|
dict({
|
||||||
|
'data': None,
|
||||||
|
'type': <PipelineEventType.RUN_END: 'run-end'>,
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
# ---
|
||||||
|
# name: test_tts_language_used_instead_of_conversation_language
|
||||||
|
list([
|
||||||
|
dict({
|
||||||
|
'data': dict({
|
||||||
|
'language': 'en',
|
||||||
|
'pipeline': <ANY>,
|
||||||
|
}),
|
||||||
|
'type': <PipelineEventType.RUN_START: 'run-start'>,
|
||||||
|
}),
|
||||||
|
dict({
|
||||||
|
'data': dict({
|
||||||
|
'conversation_id': None,
|
||||||
|
'device_id': None,
|
||||||
|
'engine': 'conversation.home_assistant',
|
||||||
|
'intent_input': 'test input',
|
||||||
|
'language': 'en-us',
|
||||||
|
'prefer_local_intents': False,
|
||||||
|
}),
|
||||||
|
'type': <PipelineEventType.INTENT_START: 'intent-start'>,
|
||||||
|
}),
|
||||||
|
dict({
|
||||||
|
'data': dict({
|
||||||
|
'intent_output': dict({
|
||||||
|
'conversation_id': None,
|
||||||
|
'response': dict({
|
||||||
|
'card': dict({
|
||||||
|
}),
|
||||||
|
'data': dict({
|
||||||
|
'failed': list([
|
||||||
|
]),
|
||||||
|
'success': list([
|
||||||
|
]),
|
||||||
|
'targets': list([
|
||||||
|
]),
|
||||||
|
}),
|
||||||
|
'language': 'en',
|
||||||
|
'response_type': 'action_done',
|
||||||
|
'speech': dict({
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
'processed_locally': True,
|
||||||
|
}),
|
||||||
|
'type': <PipelineEventType.INTENT_END: 'intent-end'>,
|
||||||
|
}),
|
||||||
|
dict({
|
||||||
|
'data': None,
|
||||||
|
'type': <PipelineEventType.RUN_END: 'run-end'>,
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
# ---
|
||||||
# name: test_wake_word_detection_aborted
|
# name: test_wake_word_detection_aborted
|
||||||
list([
|
list([
|
||||||
dict({
|
dict({
|
||||||
|
@ -1102,13 +1102,13 @@ async def test_prefer_local_intents(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def test_pipeline_language_used_instead_of_conversation_language(
|
async def test_stt_language_used_instead_of_conversation_language(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
hass_ws_client: WebSocketGenerator,
|
hass_ws_client: WebSocketGenerator,
|
||||||
init_components,
|
init_components,
|
||||||
snapshot: SnapshotAssertion,
|
snapshot: SnapshotAssertion,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test that the pipeline language is used when the conversation language is '*' (all languages)."""
|
"""Test that the STT language is used first when the conversation language is '*' (all languages)."""
|
||||||
client = await hass_ws_client(hass)
|
client = await hass_ws_client(hass)
|
||||||
|
|
||||||
events: list[assist_pipeline.PipelineEvent] = []
|
events: list[assist_pipeline.PipelineEvent] = []
|
||||||
@ -1165,7 +1165,155 @@ async def test_pipeline_language_used_instead_of_conversation_language(
|
|||||||
|
|
||||||
assert intent_start is not None
|
assert intent_start is not None
|
||||||
|
|
||||||
# Pipeline language (en) should be used instead of '*'
|
# STT language (en-US) should be used instead of '*'
|
||||||
|
assert intent_start.data.get("language") == pipeline.stt_language
|
||||||
|
|
||||||
|
# Check input to async_converse
|
||||||
|
mock_async_converse.assert_called_once()
|
||||||
|
assert (
|
||||||
|
mock_async_converse.call_args_list[0].kwargs.get("language")
|
||||||
|
== pipeline.stt_language
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_tts_language_used_instead_of_conversation_language(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
hass_ws_client: WebSocketGenerator,
|
||||||
|
init_components,
|
||||||
|
snapshot: SnapshotAssertion,
|
||||||
|
) -> None:
|
||||||
|
"""Test that the TTS language is used after STT when the conversation language is '*' (all languages)."""
|
||||||
|
client = await hass_ws_client(hass)
|
||||||
|
|
||||||
|
events: list[assist_pipeline.PipelineEvent] = []
|
||||||
|
|
||||||
|
await client.send_json_auto_id(
|
||||||
|
{
|
||||||
|
"type": "assist_pipeline/pipeline/create",
|
||||||
|
"conversation_engine": "homeassistant",
|
||||||
|
"conversation_language": MATCH_ALL,
|
||||||
|
"language": "en",
|
||||||
|
"name": "test_name",
|
||||||
|
"stt_engine": None,
|
||||||
|
"stt_language": None,
|
||||||
|
"tts_engine": None,
|
||||||
|
"tts_language": "en-us",
|
||||||
|
"tts_voice": "Arnold Schwarzenegger",
|
||||||
|
"wake_word_entity": None,
|
||||||
|
"wake_word_id": None,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
msg = await client.receive_json()
|
||||||
|
assert msg["success"]
|
||||||
|
pipeline_id = msg["result"]["id"]
|
||||||
|
pipeline = assist_pipeline.async_get_pipeline(hass, pipeline_id)
|
||||||
|
|
||||||
|
pipeline_input = assist_pipeline.pipeline.PipelineInput(
|
||||||
|
intent_input="test input",
|
||||||
|
run=assist_pipeline.pipeline.PipelineRun(
|
||||||
|
hass,
|
||||||
|
context=Context(),
|
||||||
|
pipeline=pipeline,
|
||||||
|
start_stage=assist_pipeline.PipelineStage.INTENT,
|
||||||
|
end_stage=assist_pipeline.PipelineStage.INTENT,
|
||||||
|
event_callback=events.append,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
await pipeline_input.validate()
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.assist_pipeline.pipeline.conversation.async_converse",
|
||||||
|
return_value=conversation.ConversationResult(
|
||||||
|
intent.IntentResponse(pipeline.language)
|
||||||
|
),
|
||||||
|
) as mock_async_converse:
|
||||||
|
await pipeline_input.execute()
|
||||||
|
|
||||||
|
# Check intent start event
|
||||||
|
assert process_events(events) == snapshot
|
||||||
|
intent_start: assist_pipeline.PipelineEvent | None = None
|
||||||
|
for event in events:
|
||||||
|
if event.type == assist_pipeline.PipelineEventType.INTENT_START:
|
||||||
|
intent_start = event
|
||||||
|
break
|
||||||
|
|
||||||
|
assert intent_start is not None
|
||||||
|
|
||||||
|
# STT language (en-US) should be used instead of '*'
|
||||||
|
assert intent_start.data.get("language") == pipeline.tts_language
|
||||||
|
|
||||||
|
# Check input to async_converse
|
||||||
|
mock_async_converse.assert_called_once()
|
||||||
|
assert (
|
||||||
|
mock_async_converse.call_args_list[0].kwargs.get("language")
|
||||||
|
== pipeline.tts_language
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_pipeline_language_used_instead_of_conversation_language(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
hass_ws_client: WebSocketGenerator,
|
||||||
|
init_components,
|
||||||
|
snapshot: SnapshotAssertion,
|
||||||
|
) -> None:
|
||||||
|
"""Test that the pipeline language is used last when the conversation language is '*' (all languages)."""
|
||||||
|
client = await hass_ws_client(hass)
|
||||||
|
|
||||||
|
events: list[assist_pipeline.PipelineEvent] = []
|
||||||
|
|
||||||
|
await client.send_json_auto_id(
|
||||||
|
{
|
||||||
|
"type": "assist_pipeline/pipeline/create",
|
||||||
|
"conversation_engine": "homeassistant",
|
||||||
|
"conversation_language": MATCH_ALL,
|
||||||
|
"language": "en",
|
||||||
|
"name": "test_name",
|
||||||
|
"stt_engine": None,
|
||||||
|
"stt_language": None,
|
||||||
|
"tts_engine": None,
|
||||||
|
"tts_language": None,
|
||||||
|
"tts_voice": None,
|
||||||
|
"wake_word_entity": None,
|
||||||
|
"wake_word_id": None,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
msg = await client.receive_json()
|
||||||
|
assert msg["success"]
|
||||||
|
pipeline_id = msg["result"]["id"]
|
||||||
|
pipeline = assist_pipeline.async_get_pipeline(hass, pipeline_id)
|
||||||
|
|
||||||
|
pipeline_input = assist_pipeline.pipeline.PipelineInput(
|
||||||
|
intent_input="test input",
|
||||||
|
run=assist_pipeline.pipeline.PipelineRun(
|
||||||
|
hass,
|
||||||
|
context=Context(),
|
||||||
|
pipeline=pipeline,
|
||||||
|
start_stage=assist_pipeline.PipelineStage.INTENT,
|
||||||
|
end_stage=assist_pipeline.PipelineStage.INTENT,
|
||||||
|
event_callback=events.append,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
await pipeline_input.validate()
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.assist_pipeline.pipeline.conversation.async_converse",
|
||||||
|
return_value=conversation.ConversationResult(
|
||||||
|
intent.IntentResponse(pipeline.language)
|
||||||
|
),
|
||||||
|
) as mock_async_converse:
|
||||||
|
await pipeline_input.execute()
|
||||||
|
|
||||||
|
# Check intent start event
|
||||||
|
assert process_events(events) == snapshot
|
||||||
|
intent_start: assist_pipeline.PipelineEvent | None = None
|
||||||
|
for event in events:
|
||||||
|
if event.type == assist_pipeline.PipelineEventType.INTENT_START:
|
||||||
|
intent_start = event
|
||||||
|
break
|
||||||
|
|
||||||
|
assert intent_start is not None
|
||||||
|
|
||||||
|
# STT language (en-US) should be used instead of '*'
|
||||||
assert intent_start.data.get("language") == pipeline.language
|
assert intent_start.data.get("language") == pipeline.language
|
||||||
|
|
||||||
# Check input to async_converse
|
# Check input to async_converse
|
||||||
|
@ -673,7 +673,7 @@ DEFAULT_BACKUP_OPTIONS = supervisor_backups.PartialBackupOptions(
|
|||||||
"instance_id": ANY,
|
"instance_id": ANY,
|
||||||
"with_automatic_settings": False,
|
"with_automatic_settings": False,
|
||||||
},
|
},
|
||||||
folders=None,
|
folders={"ssl"},
|
||||||
homeassistant_exclude_database=False,
|
homeassistant_exclude_database=False,
|
||||||
homeassistant=True,
|
homeassistant=True,
|
||||||
location=[None],
|
location=[None],
|
||||||
@ -704,7 +704,7 @@ DEFAULT_BACKUP_OPTIONS = supervisor_backups.PartialBackupOptions(
|
|||||||
),
|
),
|
||||||
(
|
(
|
||||||
{"include_folders": ["media", "share"]},
|
{"include_folders": ["media", "share"]},
|
||||||
replace(DEFAULT_BACKUP_OPTIONS, folders={"media", "share"}),
|
replace(DEFAULT_BACKUP_OPTIONS, folders={"media", "share", "ssl"}),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
{
|
{
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
"""Test different accessory types: Lights."""
|
"""Test different accessory types: Lights."""
|
||||||
|
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
import sys
|
||||||
|
|
||||||
from pyhap.const import HAP_REPR_AID, HAP_REPR_CHARS, HAP_REPR_IID, HAP_REPR_VALUE
|
from pyhap.const import HAP_REPR_AID, HAP_REPR_CHARS, HAP_REPR_IID, HAP_REPR_VALUE
|
||||||
import pytest
|
import pytest
|
||||||
@ -540,6 +541,422 @@ async def test_light_color_temperature_and_rgb_color(
|
|||||||
assert acc.char_saturation.value == 100
|
assert acc.char_saturation.value == 100
|
||||||
|
|
||||||
|
|
||||||
|
async def test_light_invalid_hs_color(
|
||||||
|
hass: HomeAssistant, hk_driver, events: list[Event]
|
||||||
|
) -> None:
|
||||||
|
"""Test light that starts out with an invalid hs color."""
|
||||||
|
entity_id = "light.demo"
|
||||||
|
|
||||||
|
hass.states.async_set(
|
||||||
|
entity_id,
|
||||||
|
STATE_ON,
|
||||||
|
{
|
||||||
|
ATTR_SUPPORTED_COLOR_MODES: ["color_temp", "hs"],
|
||||||
|
ATTR_COLOR_MODE: "hs",
|
||||||
|
ATTR_HS_COLOR: 260,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
acc = Light(hass, hk_driver, "Light", entity_id, 1, None)
|
||||||
|
hk_driver.add_accessory(acc)
|
||||||
|
|
||||||
|
assert acc.char_color_temp.value == 153
|
||||||
|
assert acc.char_hue.value == 0
|
||||||
|
assert acc.char_saturation.value == 75
|
||||||
|
|
||||||
|
assert hasattr(acc, "char_color_temp")
|
||||||
|
|
||||||
|
hass.states.async_set(entity_id, STATE_ON, {ATTR_COLOR_TEMP_KELVIN: 4464})
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
acc.run()
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert acc.char_color_temp.value == 224
|
||||||
|
assert acc.char_hue.value == 27
|
||||||
|
assert acc.char_saturation.value == 27
|
||||||
|
|
||||||
|
hass.states.async_set(entity_id, STATE_ON, {ATTR_COLOR_TEMP_KELVIN: 2840})
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
acc.run()
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert acc.char_color_temp.value == 352
|
||||||
|
assert acc.char_hue.value == 28
|
||||||
|
assert acc.char_saturation.value == 61
|
||||||
|
|
||||||
|
char_on_iid = acc.char_on.to_HAP()[HAP_REPR_IID]
|
||||||
|
char_brightness_iid = acc.char_brightness.to_HAP()[HAP_REPR_IID]
|
||||||
|
char_hue_iid = acc.char_hue.to_HAP()[HAP_REPR_IID]
|
||||||
|
char_saturation_iid = acc.char_saturation.to_HAP()[HAP_REPR_IID]
|
||||||
|
char_color_temp_iid = acc.char_color_temp.to_HAP()[HAP_REPR_IID]
|
||||||
|
|
||||||
|
# Set from HomeKit
|
||||||
|
call_turn_on = async_mock_service(hass, LIGHT_DOMAIN, "turn_on")
|
||||||
|
|
||||||
|
hk_driver.set_characteristics(
|
||||||
|
{
|
||||||
|
HAP_REPR_CHARS: [
|
||||||
|
{HAP_REPR_AID: acc.aid, HAP_REPR_IID: char_on_iid, HAP_REPR_VALUE: 1},
|
||||||
|
{
|
||||||
|
HAP_REPR_AID: acc.aid,
|
||||||
|
HAP_REPR_IID: char_brightness_iid,
|
||||||
|
HAP_REPR_VALUE: 20,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
HAP_REPR_AID: acc.aid,
|
||||||
|
HAP_REPR_IID: char_color_temp_iid,
|
||||||
|
HAP_REPR_VALUE: 250,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
HAP_REPR_AID: acc.aid,
|
||||||
|
HAP_REPR_IID: char_hue_iid,
|
||||||
|
HAP_REPR_VALUE: 50,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
HAP_REPR_AID: acc.aid,
|
||||||
|
HAP_REPR_IID: char_saturation_iid,
|
||||||
|
HAP_REPR_VALUE: 50,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"mock_addr",
|
||||||
|
)
|
||||||
|
await _wait_for_light_coalesce(hass)
|
||||||
|
assert call_turn_on[0]
|
||||||
|
assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id
|
||||||
|
assert call_turn_on[0].data[ATTR_BRIGHTNESS_PCT] == 20
|
||||||
|
assert call_turn_on[0].data[ATTR_COLOR_TEMP_KELVIN] == 4000
|
||||||
|
|
||||||
|
assert len(events) == 1
|
||||||
|
assert (
|
||||||
|
events[-1].data[ATTR_VALUE]
|
||||||
|
== f"Set state to 1, brightness at 20{PERCENTAGE}, color temperature at 250"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Only set Hue
|
||||||
|
hk_driver.set_characteristics(
|
||||||
|
{
|
||||||
|
HAP_REPR_CHARS: [
|
||||||
|
{
|
||||||
|
HAP_REPR_AID: acc.aid,
|
||||||
|
HAP_REPR_IID: char_hue_iid,
|
||||||
|
HAP_REPR_VALUE: 30,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"mock_addr",
|
||||||
|
)
|
||||||
|
await _wait_for_light_coalesce(hass)
|
||||||
|
assert call_turn_on[1]
|
||||||
|
assert call_turn_on[1].data[ATTR_HS_COLOR] == (30, 50)
|
||||||
|
|
||||||
|
assert events[-1].data[ATTR_VALUE] == "set color at (30, 50)"
|
||||||
|
|
||||||
|
# Only set Saturation
|
||||||
|
hk_driver.set_characteristics(
|
||||||
|
{
|
||||||
|
HAP_REPR_CHARS: [
|
||||||
|
{
|
||||||
|
HAP_REPR_AID: acc.aid,
|
||||||
|
HAP_REPR_IID: char_saturation_iid,
|
||||||
|
HAP_REPR_VALUE: 20,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"mock_addr",
|
||||||
|
)
|
||||||
|
await _wait_for_light_coalesce(hass)
|
||||||
|
assert call_turn_on[2]
|
||||||
|
assert call_turn_on[2].data[ATTR_HS_COLOR] == (30, 20)
|
||||||
|
|
||||||
|
assert events[-1].data[ATTR_VALUE] == "set color at (30, 20)"
|
||||||
|
|
||||||
|
# Generate a conflict by setting hue and then color temp
|
||||||
|
hk_driver.set_characteristics(
|
||||||
|
{
|
||||||
|
HAP_REPR_CHARS: [
|
||||||
|
{
|
||||||
|
HAP_REPR_AID: acc.aid,
|
||||||
|
HAP_REPR_IID: char_hue_iid,
|
||||||
|
HAP_REPR_VALUE: 80,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"mock_addr",
|
||||||
|
)
|
||||||
|
hk_driver.set_characteristics(
|
||||||
|
{
|
||||||
|
HAP_REPR_CHARS: [
|
||||||
|
{
|
||||||
|
HAP_REPR_AID: acc.aid,
|
||||||
|
HAP_REPR_IID: char_color_temp_iid,
|
||||||
|
HAP_REPR_VALUE: 320,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"mock_addr",
|
||||||
|
)
|
||||||
|
await _wait_for_light_coalesce(hass)
|
||||||
|
assert call_turn_on[3]
|
||||||
|
assert call_turn_on[3].data[ATTR_COLOR_TEMP_KELVIN] == 3125
|
||||||
|
assert events[-1].data[ATTR_VALUE] == "color temperature at 320"
|
||||||
|
|
||||||
|
# Generate a conflict by setting color temp then saturation
|
||||||
|
hk_driver.set_characteristics(
|
||||||
|
{
|
||||||
|
HAP_REPR_CHARS: [
|
||||||
|
{
|
||||||
|
HAP_REPR_AID: acc.aid,
|
||||||
|
HAP_REPR_IID: char_color_temp_iid,
|
||||||
|
HAP_REPR_VALUE: 404,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"mock_addr",
|
||||||
|
)
|
||||||
|
hk_driver.set_characteristics(
|
||||||
|
{
|
||||||
|
HAP_REPR_CHARS: [
|
||||||
|
{
|
||||||
|
HAP_REPR_AID: acc.aid,
|
||||||
|
HAP_REPR_IID: char_saturation_iid,
|
||||||
|
HAP_REPR_VALUE: 35,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"mock_addr",
|
||||||
|
)
|
||||||
|
await _wait_for_light_coalesce(hass)
|
||||||
|
assert call_turn_on[4]
|
||||||
|
assert call_turn_on[4].data[ATTR_HS_COLOR] == (80, 35)
|
||||||
|
assert events[-1].data[ATTR_VALUE] == "set color at (80, 35)"
|
||||||
|
|
||||||
|
# Set from HASS
|
||||||
|
hass.states.async_set(entity_id, STATE_ON, {ATTR_HS_COLOR: (100, 100)})
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
acc.run()
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert acc.char_color_temp.value == 404
|
||||||
|
assert acc.char_hue.value == 100
|
||||||
|
assert acc.char_saturation.value == 100
|
||||||
|
|
||||||
|
|
||||||
|
async def test_light_invalid_values(
|
||||||
|
hass: HomeAssistant, hk_driver, events: list[Event]
|
||||||
|
) -> None:
|
||||||
|
"""Test light with a variety of invalid values."""
|
||||||
|
entity_id = "light.demo"
|
||||||
|
|
||||||
|
hass.states.async_set(
|
||||||
|
entity_id,
|
||||||
|
STATE_ON,
|
||||||
|
{
|
||||||
|
ATTR_SUPPORTED_COLOR_MODES: ["color_temp", "hs"],
|
||||||
|
ATTR_COLOR_MODE: "hs",
|
||||||
|
ATTR_HS_COLOR: (-1, -1),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
acc = Light(hass, hk_driver, "Light", entity_id, 1, None)
|
||||||
|
hk_driver.add_accessory(acc)
|
||||||
|
|
||||||
|
assert acc.char_color_temp.value == 153
|
||||||
|
assert acc.char_hue.value == 0
|
||||||
|
assert acc.char_saturation.value == 0
|
||||||
|
hass.states.async_set(
|
||||||
|
entity_id,
|
||||||
|
STATE_ON,
|
||||||
|
{
|
||||||
|
ATTR_SUPPORTED_COLOR_MODES: ["color_temp", "hs"],
|
||||||
|
ATTR_COLOR_MODE: "color_temp",
|
||||||
|
ATTR_COLOR_TEMP_KELVIN: -1,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
acc.run()
|
||||||
|
|
||||||
|
assert acc.char_color_temp.value == 153
|
||||||
|
assert acc.char_hue.value == 16
|
||||||
|
assert acc.char_saturation.value == 100
|
||||||
|
hass.states.async_set(
|
||||||
|
entity_id,
|
||||||
|
STATE_ON,
|
||||||
|
{
|
||||||
|
ATTR_SUPPORTED_COLOR_MODES: ["color_temp", "hs"],
|
||||||
|
ATTR_COLOR_MODE: "color_temp",
|
||||||
|
ATTR_COLOR_TEMP_KELVIN: sys.maxsize,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert acc.char_color_temp.value == 153
|
||||||
|
assert acc.char_hue.value == 220
|
||||||
|
assert acc.char_saturation.value == 41
|
||||||
|
|
||||||
|
hass.states.async_set(
|
||||||
|
entity_id,
|
||||||
|
STATE_ON,
|
||||||
|
{
|
||||||
|
ATTR_SUPPORTED_COLOR_MODES: ["color_temp", "hs"],
|
||||||
|
ATTR_COLOR_MODE: "color_temp",
|
||||||
|
ATTR_COLOR_TEMP_KELVIN: 2000,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert acc.char_color_temp.value == 500
|
||||||
|
assert acc.char_hue.value == 31
|
||||||
|
assert acc.char_saturation.value == 95
|
||||||
|
|
||||||
|
|
||||||
|
async def test_light_out_of_range_color_temp(hass: HomeAssistant, hk_driver) -> None:
|
||||||
|
"""Test light with an out of range color temp."""
|
||||||
|
entity_id = "light.demo"
|
||||||
|
|
||||||
|
hass.states.async_set(
|
||||||
|
entity_id,
|
||||||
|
STATE_ON,
|
||||||
|
{
|
||||||
|
ATTR_SUPPORTED_COLOR_MODES: ["color_temp", "hs"],
|
||||||
|
ATTR_COLOR_MODE: "hs",
|
||||||
|
ATTR_COLOR_TEMP_KELVIN: 2000,
|
||||||
|
ATTR_MAX_COLOR_TEMP_KELVIN: 4000,
|
||||||
|
ATTR_MIN_COLOR_TEMP_KELVIN: 3000,
|
||||||
|
ATTR_HS_COLOR: (-1, -1),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
acc = Light(hass, hk_driver, "Light", entity_id, 1, None)
|
||||||
|
hk_driver.add_accessory(acc)
|
||||||
|
|
||||||
|
assert acc.char_color_temp.value == 333
|
||||||
|
assert acc.char_color_temp.properties[PROP_MAX_VALUE] == 333
|
||||||
|
assert acc.char_color_temp.properties[PROP_MIN_VALUE] == 250
|
||||||
|
assert acc.char_hue.value == 31
|
||||||
|
assert acc.char_saturation.value == 95
|
||||||
|
hass.states.async_set(
|
||||||
|
entity_id,
|
||||||
|
STATE_ON,
|
||||||
|
{
|
||||||
|
ATTR_SUPPORTED_COLOR_MODES: ["color_temp", "hs"],
|
||||||
|
ATTR_COLOR_MODE: "color_temp",
|
||||||
|
ATTR_MAX_COLOR_TEMP_KELVIN: 4000,
|
||||||
|
ATTR_MIN_COLOR_TEMP_KELVIN: 3000,
|
||||||
|
ATTR_COLOR_TEMP_KELVIN: -1,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
acc.run()
|
||||||
|
|
||||||
|
assert acc.char_color_temp.value == 250
|
||||||
|
assert acc.char_hue.value == 16
|
||||||
|
assert acc.char_saturation.value == 100
|
||||||
|
hass.states.async_set(
|
||||||
|
entity_id,
|
||||||
|
STATE_ON,
|
||||||
|
{
|
||||||
|
ATTR_SUPPORTED_COLOR_MODES: ["color_temp", "hs"],
|
||||||
|
ATTR_COLOR_MODE: "color_temp",
|
||||||
|
ATTR_MAX_COLOR_TEMP_KELVIN: 4000,
|
||||||
|
ATTR_MIN_COLOR_TEMP_KELVIN: 3000,
|
||||||
|
ATTR_COLOR_TEMP_KELVIN: sys.maxsize,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert acc.char_color_temp.value == 250
|
||||||
|
assert acc.char_hue.value == 220
|
||||||
|
assert acc.char_saturation.value == 41
|
||||||
|
|
||||||
|
hass.states.async_set(
|
||||||
|
entity_id,
|
||||||
|
STATE_ON,
|
||||||
|
{
|
||||||
|
ATTR_SUPPORTED_COLOR_MODES: ["color_temp", "hs"],
|
||||||
|
ATTR_COLOR_MODE: "color_temp",
|
||||||
|
ATTR_COLOR_TEMP_KELVIN: 2000,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert acc.char_color_temp.value == 250
|
||||||
|
assert acc.char_hue.value == 220
|
||||||
|
assert acc.char_saturation.value == 41
|
||||||
|
|
||||||
|
|
||||||
|
async def test_reversed_color_temp_min_max(hass: HomeAssistant, hk_driver) -> None:
|
||||||
|
"""Test light with a reversed color temp min max."""
|
||||||
|
entity_id = "light.demo"
|
||||||
|
|
||||||
|
hass.states.async_set(
|
||||||
|
entity_id,
|
||||||
|
STATE_ON,
|
||||||
|
{
|
||||||
|
ATTR_SUPPORTED_COLOR_MODES: ["color_temp", "hs"],
|
||||||
|
ATTR_COLOR_MODE: "hs",
|
||||||
|
ATTR_COLOR_TEMP_KELVIN: 2000,
|
||||||
|
ATTR_MAX_COLOR_TEMP_KELVIN: 3000,
|
||||||
|
ATTR_MIN_COLOR_TEMP_KELVIN: 4000,
|
||||||
|
ATTR_HS_COLOR: (-1, -1),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
acc = Light(hass, hk_driver, "Light", entity_id, 1, None)
|
||||||
|
hk_driver.add_accessory(acc)
|
||||||
|
|
||||||
|
assert acc.char_color_temp.value == 333
|
||||||
|
assert acc.char_color_temp.properties[PROP_MAX_VALUE] == 333
|
||||||
|
assert acc.char_color_temp.properties[PROP_MIN_VALUE] == 250
|
||||||
|
assert acc.char_hue.value == 31
|
||||||
|
assert acc.char_saturation.value == 95
|
||||||
|
hass.states.async_set(
|
||||||
|
entity_id,
|
||||||
|
STATE_ON,
|
||||||
|
{
|
||||||
|
ATTR_SUPPORTED_COLOR_MODES: ["color_temp", "hs"],
|
||||||
|
ATTR_COLOR_MODE: "color_temp",
|
||||||
|
ATTR_MAX_COLOR_TEMP_KELVIN: 4000,
|
||||||
|
ATTR_MIN_COLOR_TEMP_KELVIN: 3000,
|
||||||
|
ATTR_COLOR_TEMP_KELVIN: -1,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
acc.run()
|
||||||
|
|
||||||
|
assert acc.char_color_temp.value == 250
|
||||||
|
assert acc.char_hue.value == 16
|
||||||
|
assert acc.char_saturation.value == 100
|
||||||
|
hass.states.async_set(
|
||||||
|
entity_id,
|
||||||
|
STATE_ON,
|
||||||
|
{
|
||||||
|
ATTR_SUPPORTED_COLOR_MODES: ["color_temp", "hs"],
|
||||||
|
ATTR_COLOR_MODE: "color_temp",
|
||||||
|
ATTR_MAX_COLOR_TEMP_KELVIN: 4000,
|
||||||
|
ATTR_MIN_COLOR_TEMP_KELVIN: 3000,
|
||||||
|
ATTR_COLOR_TEMP_KELVIN: sys.maxsize,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert acc.char_color_temp.value == 250
|
||||||
|
assert acc.char_hue.value == 220
|
||||||
|
assert acc.char_saturation.value == 41
|
||||||
|
|
||||||
|
hass.states.async_set(
|
||||||
|
entity_id,
|
||||||
|
STATE_ON,
|
||||||
|
{
|
||||||
|
ATTR_SUPPORTED_COLOR_MODES: ["color_temp", "hs"],
|
||||||
|
ATTR_COLOR_MODE: "color_temp",
|
||||||
|
ATTR_COLOR_TEMP_KELVIN: 2000,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert acc.char_color_temp.value == 250
|
||||||
|
assert acc.char_hue.value == 220
|
||||||
|
assert acc.char_saturation.value == 41
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"supported_color_modes", [[ColorMode.HS], [ColorMode.RGB], [ColorMode.XY]]
|
"supported_color_modes", [[ColorMode.HS], [ColorMode.RGB], [ColorMode.XY]]
|
||||||
)
|
)
|
||||||
|
@ -26,6 +26,7 @@ from homeassistant.components.climate import (
|
|||||||
ATTR_TARGET_TEMP_STEP,
|
ATTR_TARGET_TEMP_STEP,
|
||||||
DEFAULT_MAX_TEMP,
|
DEFAULT_MAX_TEMP,
|
||||||
DEFAULT_MIN_HUMIDITY,
|
DEFAULT_MIN_HUMIDITY,
|
||||||
|
DEFAULT_MIN_TEMP,
|
||||||
DOMAIN as DOMAIN_CLIMATE,
|
DOMAIN as DOMAIN_CLIMATE,
|
||||||
FAN_AUTO,
|
FAN_AUTO,
|
||||||
FAN_HIGH,
|
FAN_HIGH,
|
||||||
@ -2009,8 +2010,8 @@ async def test_thermostat_with_temp_clamps(hass: HomeAssistant, hk_driver) -> No
|
|||||||
ATTR_SUPPORTED_FEATURES: ClimateEntityFeature.TARGET_TEMPERATURE
|
ATTR_SUPPORTED_FEATURES: ClimateEntityFeature.TARGET_TEMPERATURE
|
||||||
| ClimateEntityFeature.TARGET_TEMPERATURE_RANGE,
|
| ClimateEntityFeature.TARGET_TEMPERATURE_RANGE,
|
||||||
ATTR_HVAC_MODES: [HVACMode.HEAT_COOL, HVACMode.AUTO],
|
ATTR_HVAC_MODES: [HVACMode.HEAT_COOL, HVACMode.AUTO],
|
||||||
ATTR_MAX_TEMP: 50,
|
ATTR_MAX_TEMP: 100,
|
||||||
ATTR_MIN_TEMP: 100,
|
ATTR_MIN_TEMP: 50,
|
||||||
}
|
}
|
||||||
hass.states.async_set(
|
hass.states.async_set(
|
||||||
entity_id,
|
entity_id,
|
||||||
@ -2024,14 +2025,14 @@ async def test_thermostat_with_temp_clamps(hass: HomeAssistant, hk_driver) -> No
|
|||||||
acc.run()
|
acc.run()
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert acc.char_cooling_thresh_temp.value == 100
|
assert acc.char_cooling_thresh_temp.value == 50
|
||||||
assert acc.char_heating_thresh_temp.value == 100
|
assert acc.char_heating_thresh_temp.value == 50
|
||||||
|
|
||||||
assert acc.char_cooling_thresh_temp.properties[PROP_MAX_VALUE] == 100
|
assert acc.char_cooling_thresh_temp.properties[PROP_MAX_VALUE] == 100
|
||||||
assert acc.char_cooling_thresh_temp.properties[PROP_MIN_VALUE] == 100
|
assert acc.char_cooling_thresh_temp.properties[PROP_MIN_VALUE] == 50
|
||||||
assert acc.char_cooling_thresh_temp.properties[PROP_MIN_STEP] == 0.1
|
assert acc.char_cooling_thresh_temp.properties[PROP_MIN_STEP] == 0.1
|
||||||
assert acc.char_heating_thresh_temp.properties[PROP_MAX_VALUE] == 100
|
assert acc.char_heating_thresh_temp.properties[PROP_MAX_VALUE] == 100
|
||||||
assert acc.char_heating_thresh_temp.properties[PROP_MIN_VALUE] == 100
|
assert acc.char_heating_thresh_temp.properties[PROP_MIN_VALUE] == 50
|
||||||
assert acc.char_heating_thresh_temp.properties[PROP_MIN_STEP] == 0.1
|
assert acc.char_heating_thresh_temp.properties[PROP_MIN_STEP] == 0.1
|
||||||
|
|
||||||
assert acc.char_target_heat_cool.value == 3
|
assert acc.char_target_heat_cool.value == 3
|
||||||
@ -2048,7 +2049,7 @@ async def test_thermostat_with_temp_clamps(hass: HomeAssistant, hk_driver) -> No
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
assert acc.char_heating_thresh_temp.value == 100.0
|
assert acc.char_heating_thresh_temp.value == 50.0
|
||||||
assert acc.char_cooling_thresh_temp.value == 100.0
|
assert acc.char_cooling_thresh_temp.value == 100.0
|
||||||
assert acc.char_current_heat_cool.value == 1
|
assert acc.char_current_heat_cool.value == 1
|
||||||
assert acc.char_target_heat_cool.value == 3
|
assert acc.char_target_heat_cool.value == 3
|
||||||
@ -2633,3 +2634,44 @@ async def test_thermostat_handles_unknown_state(hass: HomeAssistant, hk_driver)
|
|||||||
assert call_set_hvac_mode
|
assert call_set_hvac_mode
|
||||||
assert call_set_hvac_mode[1].data[ATTR_ENTITY_ID] == entity_id
|
assert call_set_hvac_mode[1].data[ATTR_ENTITY_ID] == entity_id
|
||||||
assert call_set_hvac_mode[1].data[ATTR_HVAC_MODE] == HVACMode.HEAT
|
assert call_set_hvac_mode[1].data[ATTR_HVAC_MODE] == HVACMode.HEAT
|
||||||
|
|
||||||
|
|
||||||
|
async def test_thermostat_reversed_min_max(hass: HomeAssistant, hk_driver) -> None:
|
||||||
|
"""Test reversed min/max temperatures."""
|
||||||
|
entity_id = "climate.test"
|
||||||
|
base_attrs = {
|
||||||
|
ATTR_SUPPORTED_FEATURES: ClimateEntityFeature.TARGET_TEMPERATURE
|
||||||
|
| ClimateEntityFeature.TARGET_TEMPERATURE_RANGE,
|
||||||
|
ATTR_HVAC_MODES: [
|
||||||
|
HVACMode.HEAT,
|
||||||
|
HVACMode.HEAT_COOL,
|
||||||
|
HVACMode.FAN_ONLY,
|
||||||
|
HVACMode.COOL,
|
||||||
|
HVACMode.OFF,
|
||||||
|
HVACMode.AUTO,
|
||||||
|
],
|
||||||
|
ATTR_MAX_TEMP: DEFAULT_MAX_TEMP,
|
||||||
|
ATTR_MIN_TEMP: DEFAULT_MIN_TEMP,
|
||||||
|
}
|
||||||
|
# support_auto = True
|
||||||
|
hass.states.async_set(
|
||||||
|
entity_id,
|
||||||
|
HVACMode.OFF,
|
||||||
|
base_attrs,
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None)
|
||||||
|
hk_driver.add_accessory(acc)
|
||||||
|
|
||||||
|
acc.run()
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert acc.char_cooling_thresh_temp.value == 23.0
|
||||||
|
assert acc.char_heating_thresh_temp.value == 19.0
|
||||||
|
|
||||||
|
assert acc.char_cooling_thresh_temp.properties[PROP_MAX_VALUE] == DEFAULT_MAX_TEMP
|
||||||
|
assert acc.char_cooling_thresh_temp.properties[PROP_MIN_VALUE] == 7.0
|
||||||
|
assert acc.char_cooling_thresh_temp.properties[PROP_MIN_STEP] == 0.1
|
||||||
|
assert acc.char_heating_thresh_temp.properties[PROP_MAX_VALUE] == DEFAULT_MAX_TEMP
|
||||||
|
assert acc.char_heating_thresh_temp.properties[PROP_MIN_VALUE] == 7.0
|
||||||
|
assert acc.char_heating_thresh_temp.properties[PROP_MIN_STEP] == 0.1
|
||||||
|
68
tests/components/lametric/fixtures/computer_powered.json
Normal file
68
tests/components/lametric/fixtures/computer_powered.json
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
{
|
||||||
|
"audio": {
|
||||||
|
"available": true,
|
||||||
|
"volume": 53,
|
||||||
|
"volume_limit": {
|
||||||
|
"max": 100,
|
||||||
|
"min": 0
|
||||||
|
},
|
||||||
|
"volume_range": {
|
||||||
|
"max": 100,
|
||||||
|
"min": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"bluetooth": {
|
||||||
|
"active": false,
|
||||||
|
"address": "40:F4:C9:AA:AA:AA",
|
||||||
|
"available": true,
|
||||||
|
"discoverable": true,
|
||||||
|
"mac": "40:F4:C9:AA:AA:AA",
|
||||||
|
"name": "LM8367",
|
||||||
|
"pairable": false
|
||||||
|
},
|
||||||
|
"display": {
|
||||||
|
"brightness": 75,
|
||||||
|
"brightness_limit": {
|
||||||
|
"max": 76,
|
||||||
|
"min": 2
|
||||||
|
},
|
||||||
|
"brightness_mode": "manual",
|
||||||
|
"brightness_range": {
|
||||||
|
"max": 100,
|
||||||
|
"min": 0
|
||||||
|
},
|
||||||
|
"height": 8,
|
||||||
|
"on": true,
|
||||||
|
"screensaver": {
|
||||||
|
"enabled": true,
|
||||||
|
"modes": {
|
||||||
|
"time_based": {
|
||||||
|
"enabled": false
|
||||||
|
},
|
||||||
|
"when_dark": {
|
||||||
|
"enabled": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"widget": "1_com.lametric.clock"
|
||||||
|
},
|
||||||
|
"type": "mixed",
|
||||||
|
"width": 37
|
||||||
|
},
|
||||||
|
"id": "67790",
|
||||||
|
"mode": "manual",
|
||||||
|
"model": "sa8",
|
||||||
|
"name": "TIME",
|
||||||
|
"os_version": "3.1.3",
|
||||||
|
"serial_number": "SA840700836700W00BAA",
|
||||||
|
"wifi": {
|
||||||
|
"active": true,
|
||||||
|
"mac": "40:F4:C9:AA:AA:AA",
|
||||||
|
"available": true,
|
||||||
|
"encryption": "WPA",
|
||||||
|
"ssid": "My wifi",
|
||||||
|
"ip": "10.0.0.99",
|
||||||
|
"mode": "dhcp",
|
||||||
|
"netmask": "255.255.255.0",
|
||||||
|
"rssi": 78
|
||||||
|
}
|
||||||
|
}
|
@ -24,7 +24,15 @@
|
|||||||
'device_id': '**REDACTED**',
|
'device_id': '**REDACTED**',
|
||||||
'display': dict({
|
'display': dict({
|
||||||
'brightness': 100,
|
'brightness': 100,
|
||||||
|
'brightness_limit': dict({
|
||||||
|
'range_max': 100,
|
||||||
|
'range_min': 2,
|
||||||
|
}),
|
||||||
'brightness_mode': 'auto',
|
'brightness_mode': 'auto',
|
||||||
|
'brightness_range': dict({
|
||||||
|
'range_max': 100,
|
||||||
|
'range_min': 0,
|
||||||
|
}),
|
||||||
'display_type': 'mixed',
|
'display_type': 'mixed',
|
||||||
'height': 8,
|
'height': 8,
|
||||||
'on': None,
|
'on': None,
|
||||||
|
@ -42,7 +42,7 @@ async def test_brightness(
|
|||||||
assert state.attributes.get(ATTR_DEVICE_CLASS) is None
|
assert state.attributes.get(ATTR_DEVICE_CLASS) is None
|
||||||
assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Frenck's LaMetric Brightness"
|
assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Frenck's LaMetric Brightness"
|
||||||
assert state.attributes.get(ATTR_MAX) == 100
|
assert state.attributes.get(ATTR_MAX) == 100
|
||||||
assert state.attributes.get(ATTR_MIN) == 0
|
assert state.attributes.get(ATTR_MIN) == 2
|
||||||
assert state.attributes.get(ATTR_STEP) == 1
|
assert state.attributes.get(ATTR_STEP) == 1
|
||||||
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE
|
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE
|
||||||
assert state.state == "100"
|
assert state.state == "100"
|
||||||
@ -183,3 +183,16 @@ async def test_number_connection_error(
|
|||||||
state = hass.states.get("number.frenck_s_lametric_volume")
|
state = hass.states.get("number.frenck_s_lametric_volume")
|
||||||
assert state
|
assert state
|
||||||
assert state.state == STATE_UNAVAILABLE
|
assert state.state == STATE_UNAVAILABLE
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("device_fixture", ["computer_powered"])
|
||||||
|
async def test_computer_powered_devices(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_lametric: MagicMock,
|
||||||
|
) -> None:
|
||||||
|
"""Test Brightness is properly limited for computer powered devices."""
|
||||||
|
state = hass.states.get("number.time_brightness")
|
||||||
|
assert state
|
||||||
|
assert state.state == "75"
|
||||||
|
assert state.attributes[ATTR_MIN] == 2
|
||||||
|
assert state.attributes[ATTR_MAX] == 76
|
||||||
|
@ -29,6 +29,7 @@ from homeassistant.const import (
|
|||||||
UnitOfTemperature,
|
UnitOfTemperature,
|
||||||
)
|
)
|
||||||
from homeassistant.core import HomeAssistant, State
|
from homeassistant.core import HomeAssistant, State
|
||||||
|
from homeassistant.util.unit_system import US_CUSTOMARY_SYSTEM
|
||||||
|
|
||||||
from .test_common import (
|
from .test_common import (
|
||||||
help_custom_config,
|
help_custom_config,
|
||||||
@ -157,6 +158,101 @@ async def test_run_number_setup(
|
|||||||
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == unit_of_measurement
|
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == unit_of_measurement
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"hass_config",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
mqtt.DOMAIN: {
|
||||||
|
number.DOMAIN: {
|
||||||
|
"state_topic": "test/state_number",
|
||||||
|
"command_topic": "test/cmd_number",
|
||||||
|
"name": "Test Number",
|
||||||
|
"min": 15,
|
||||||
|
"max": 28,
|
||||||
|
"device_class": "temperature",
|
||||||
|
"unit_of_measurement": UnitOfTemperature.CELSIUS.value,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_native_value_validation(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mqtt_mock_entry: MqttMockHAClientGenerator,
|
||||||
|
caplog: pytest.LogCaptureFixture,
|
||||||
|
) -> None:
|
||||||
|
"""Test state validation and native value conversion."""
|
||||||
|
mqtt_mock = await mqtt_mock_entry()
|
||||||
|
|
||||||
|
async_fire_mqtt_message(hass, "test/state_number", "23.5")
|
||||||
|
state = hass.states.get("number.test_number")
|
||||||
|
assert state is not None
|
||||||
|
assert state.attributes.get(ATTR_MIN) == 15
|
||||||
|
assert state.attributes.get(ATTR_MAX) == 28
|
||||||
|
assert (
|
||||||
|
state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
|
||||||
|
== UnitOfTemperature.CELSIUS.value
|
||||||
|
)
|
||||||
|
assert state.state == "23.5"
|
||||||
|
|
||||||
|
# Test out of range validation
|
||||||
|
async_fire_mqtt_message(hass, "test/state_number", "29.5")
|
||||||
|
state = hass.states.get("number.test_number")
|
||||||
|
assert state is not None
|
||||||
|
assert state.attributes.get(ATTR_MIN) == 15
|
||||||
|
assert state.attributes.get(ATTR_MAX) == 28
|
||||||
|
assert (
|
||||||
|
state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
|
||||||
|
== UnitOfTemperature.CELSIUS.value
|
||||||
|
)
|
||||||
|
assert state.state == "23.5"
|
||||||
|
assert (
|
||||||
|
"Invalid value for number.test_number: 29.5 (range 15.0 - 28.0)" in caplog.text
|
||||||
|
)
|
||||||
|
caplog.clear()
|
||||||
|
|
||||||
|
# Check if validation still works when changing unit system
|
||||||
|
hass.config.units = US_CUSTOMARY_SYSTEM
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
async_fire_mqtt_message(hass, "test/state_number", "24.5")
|
||||||
|
state = hass.states.get("number.test_number")
|
||||||
|
assert state is not None
|
||||||
|
assert state.attributes.get(ATTR_MIN) == 59.0
|
||||||
|
assert state.attributes.get(ATTR_MAX) == 82.4
|
||||||
|
assert (
|
||||||
|
state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
|
||||||
|
== UnitOfTemperature.FAHRENHEIT.value
|
||||||
|
)
|
||||||
|
assert state.state == "76.1"
|
||||||
|
|
||||||
|
# Test out of range validation again
|
||||||
|
async_fire_mqtt_message(hass, "test/state_number", "29.5")
|
||||||
|
state = hass.states.get("number.test_number")
|
||||||
|
assert state is not None
|
||||||
|
assert state.attributes.get(ATTR_MIN) == 59.0
|
||||||
|
assert state.attributes.get(ATTR_MAX) == 82.4
|
||||||
|
assert (
|
||||||
|
state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
|
||||||
|
== UnitOfTemperature.FAHRENHEIT.value
|
||||||
|
)
|
||||||
|
assert state.state == "76.1"
|
||||||
|
assert (
|
||||||
|
"Invalid value for number.test_number: 29.5 (range 15.0 - 28.0)" in caplog.text
|
||||||
|
)
|
||||||
|
caplog.clear()
|
||||||
|
|
||||||
|
await hass.services.async_call(
|
||||||
|
NUMBER_DOMAIN,
|
||||||
|
SERVICE_SET_VALUE,
|
||||||
|
{ATTR_ENTITY_ID: "number.test_number", ATTR_VALUE: 68},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
mqtt_mock.async_publish.assert_called_once_with("test/cmd_number", "20", 0, False)
|
||||||
|
mqtt_mock.async_publish.reset_mock()
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"hass_config",
|
"hass_config",
|
||||||
[
|
[
|
||||||
|
@ -42,11 +42,11 @@ async def test_entities(
|
|||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
("light_id", "data", "set_brightness"),
|
("light_id", "data", "set_brightness"),
|
||||||
[
|
[
|
||||||
(0, {ATTR_ENTITY_ID: "light.light"}, 100.0),
|
(0, {ATTR_ENTITY_ID: "light.light"}, 100),
|
||||||
(
|
(
|
||||||
1,
|
1,
|
||||||
{ATTR_ENTITY_ID: "light.dimmable_light", ATTR_BRIGHTNESS: 50},
|
{ATTR_ENTITY_ID: "light.dimmable_light", ATTR_BRIGHTNESS: 50},
|
||||||
19.607843137254903,
|
20,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
@ -726,7 +726,6 @@ async def test_options_add_and_configure_device(
|
|||||||
result["flow_id"],
|
result["flow_id"],
|
||||||
user_input={
|
user_input={
|
||||||
"data_bits": 4,
|
"data_bits": 4,
|
||||||
"off_delay": "abcdef",
|
|
||||||
"command_on": "xyz",
|
"command_on": "xyz",
|
||||||
"command_off": "xyz",
|
"command_off": "xyz",
|
||||||
},
|
},
|
||||||
@ -735,7 +734,6 @@ async def test_options_add_and_configure_device(
|
|||||||
assert result["type"] is FlowResultType.FORM
|
assert result["type"] is FlowResultType.FORM
|
||||||
assert result["step_id"] == "set_device_options"
|
assert result["step_id"] == "set_device_options"
|
||||||
assert result["errors"]
|
assert result["errors"]
|
||||||
assert result["errors"]["off_delay"] == "invalid_input_off_delay"
|
|
||||||
assert result["errors"]["command_on"] == "invalid_input_2262_on"
|
assert result["errors"]["command_on"] == "invalid_input_2262_on"
|
||||||
assert result["errors"]["command_off"] == "invalid_input_2262_off"
|
assert result["errors"]["command_off"] == "invalid_input_2262_off"
|
||||||
|
|
||||||
@ -745,7 +743,7 @@ async def test_options_add_and_configure_device(
|
|||||||
"data_bits": 4,
|
"data_bits": 4,
|
||||||
"command_on": "0xE",
|
"command_on": "0xE",
|
||||||
"command_off": "0x7",
|
"command_off": "0x7",
|
||||||
"off_delay": "9",
|
"off_delay": 9,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -14,6 +14,7 @@ from homeassistant.components.smartthings.const import (
|
|||||||
CONF_APP_ID,
|
CONF_APP_ID,
|
||||||
CONF_INSTALLED_APP_ID,
|
CONF_INSTALLED_APP_ID,
|
||||||
CONF_LOCATION_ID,
|
CONF_LOCATION_ID,
|
||||||
|
CONF_REFRESH_TOKEN,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
)
|
)
|
||||||
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_CLIENT_ID, CONF_CLIENT_SECRET
|
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_CLIENT_ID, CONF_CLIENT_SECRET
|
||||||
@ -757,3 +758,56 @@ async def test_no_available_locations_aborts(
|
|||||||
)
|
)
|
||||||
assert result["type"] is FlowResultType.ABORT
|
assert result["type"] is FlowResultType.ABORT
|
||||||
assert result["reason"] == "no_available_locations"
|
assert result["reason"] == "no_available_locations"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_reauth(
|
||||||
|
hass: HomeAssistant, app, app_oauth_client, location, smartthings_mock
|
||||||
|
) -> None:
|
||||||
|
"""Test reauth flow."""
|
||||||
|
token = str(uuid4())
|
||||||
|
installed_app_id = str(uuid4())
|
||||||
|
refresh_token = str(uuid4())
|
||||||
|
smartthings_mock.apps.return_value = []
|
||||||
|
smartthings_mock.create_app.return_value = (app, app_oauth_client)
|
||||||
|
smartthings_mock.locations.return_value = [location]
|
||||||
|
request = Mock()
|
||||||
|
request.installed_app_id = installed_app_id
|
||||||
|
request.auth_token = token
|
||||||
|
request.location_id = location.location_id
|
||||||
|
request.refresh_token = refresh_token
|
||||||
|
|
||||||
|
entry = MockConfigEntry(
|
||||||
|
domain=DOMAIN,
|
||||||
|
data={
|
||||||
|
CONF_APP_ID: app.app_id,
|
||||||
|
CONF_CLIENT_ID: app_oauth_client.client_id,
|
||||||
|
CONF_CLIENT_SECRET: app_oauth_client.client_secret,
|
||||||
|
CONF_LOCATION_ID: location.location_id,
|
||||||
|
CONF_INSTALLED_APP_ID: installed_app_id,
|
||||||
|
CONF_ACCESS_TOKEN: token,
|
||||||
|
CONF_REFRESH_TOKEN: "abc",
|
||||||
|
},
|
||||||
|
unique_id=smartapp.format_unique_id(app.app_id, location.location_id),
|
||||||
|
)
|
||||||
|
entry.add_to_hass(hass)
|
||||||
|
result = await entry.start_reauth_flow(hass)
|
||||||
|
assert result["type"] is FlowResultType.FORM
|
||||||
|
assert result["step_id"] == "reauth_confirm"
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
|
||||||
|
assert result["type"] is FlowResultType.EXTERNAL_STEP
|
||||||
|
assert result["step_id"] == "authorize"
|
||||||
|
assert result["url"] == format_install_url(app.app_id, location.location_id)
|
||||||
|
|
||||||
|
await smartapp.smartapp_update(hass, request, None, app)
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_configure(result["flow_id"])
|
||||||
|
assert result["type"] is FlowResultType.FORM
|
||||||
|
assert result["step_id"] == "update_confirm"
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
|
||||||
|
|
||||||
|
assert result["type"] is FlowResultType.ABORT
|
||||||
|
assert result["reason"] == "reauth_successful"
|
||||||
|
|
||||||
|
assert entry.data[CONF_REFRESH_TOKEN] == refresh_token
|
||||||
|
@ -23,6 +23,7 @@ from homeassistant.components.smartthings.const import (
|
|||||||
PLATFORMS,
|
PLATFORMS,
|
||||||
SIGNAL_SMARTTHINGS_UPDATE,
|
SIGNAL_SMARTTHINGS_UPDATE,
|
||||||
)
|
)
|
||||||
|
from homeassistant.config_entries import ConfigEntryState
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.core_config import async_process_ha_core_config
|
from homeassistant.core_config import async_process_ha_core_config
|
||||||
from homeassistant.exceptions import ConfigEntryNotReady
|
from homeassistant.exceptions import ConfigEntryNotReady
|
||||||
@ -68,17 +69,10 @@ async def test_unrecoverable_api_errors_create_new_flow(
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Assert setup returns false
|
# Assert setup returns false
|
||||||
result = await smartthings.async_setup_entry(hass, config_entry)
|
result = await hass.config_entries.async_setup(config_entry.entry_id)
|
||||||
assert not result
|
assert not result
|
||||||
|
|
||||||
# Assert entry was removed and new flow created
|
assert config_entry.state == ConfigEntryState.SETUP_ERROR
|
||||||
await hass.async_block_till_done()
|
|
||||||
assert not hass.config_entries.async_entries(DOMAIN)
|
|
||||||
flows = hass.config_entries.flow.async_progress()
|
|
||||||
assert len(flows) == 1
|
|
||||||
assert flows[0]["handler"] == "smartthings"
|
|
||||||
assert flows[0]["context"] == {"source": config_entries.SOURCE_IMPORT}
|
|
||||||
hass.config_entries.flow.async_abort(flows[0]["flow_id"])
|
|
||||||
|
|
||||||
|
|
||||||
async def test_recoverable_api_errors_raise_not_ready(
|
async def test_recoverable_api_errors_raise_not_ready(
|
||||||
|
@ -43,7 +43,7 @@
|
|||||||
'last_changed': <ANY>,
|
'last_changed': <ANY>,
|
||||||
'last_reported': <ANY>,
|
'last_reported': <ANY>,
|
||||||
'last_updated': <ANY>,
|
'last_updated': <ANY>,
|
||||||
'state': '2021-01-09T11:59:59+00:00',
|
'state': '2021-01-09T11:59:58+00:00',
|
||||||
})
|
})
|
||||||
# ---
|
# ---
|
||||||
# name: test_sensor[sensor.sonic_power_supply_mode-entry]
|
# name: test_sensor[sensor.sonic_power_supply_mode-entry]
|
||||||
@ -501,6 +501,6 @@
|
|||||||
'last_changed': <ANY>,
|
'last_changed': <ANY>,
|
||||||
'last_reported': <ANY>,
|
'last_reported': <ANY>,
|
||||||
'last_updated': <ANY>,
|
'last_updated': <ANY>,
|
||||||
'state': '2021-01-09T11:59:59+00:00',
|
'state': '2021-01-09T11:59:57+00:00',
|
||||||
})
|
})
|
||||||
# ---
|
# ---
|
||||||
|
@ -140,11 +140,11 @@ async def test_power_supply_webhook(
|
|||||||
|
|
||||||
power_supply_change_data = {
|
power_supply_change_data = {
|
||||||
"type": "power-supply-changed",
|
"type": "power-supply-changed",
|
||||||
"data": {"supply": "external"},
|
"data": {"supply": "external_battery"},
|
||||||
}
|
}
|
||||||
client = await hass_client_no_auth()
|
client = await hass_client_no_auth()
|
||||||
await client.post(f"/api/webhook/{MOCK_WEBHOOK_ID}", json=power_supply_change_data)
|
await client.post(f"/api/webhook/{MOCK_WEBHOOK_ID}", json=power_supply_change_data)
|
||||||
|
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert hass.states.get(entity_id).state == "external"
|
assert hass.states.get(entity_id).state == "battery_external"
|
||||||
|
@ -10,6 +10,7 @@ from aiohttp.hdrs import METH_HEAD
|
|||||||
from aiowithings import (
|
from aiowithings import (
|
||||||
NotificationCategory,
|
NotificationCategory,
|
||||||
WithingsAuthenticationFailedError,
|
WithingsAuthenticationFailedError,
|
||||||
|
WithingsConnectionError,
|
||||||
WithingsUnauthorizedError,
|
WithingsUnauthorizedError,
|
||||||
)
|
)
|
||||||
from freezegun.api import FrozenDateTimeFactory
|
from freezegun.api import FrozenDateTimeFactory
|
||||||
@ -532,6 +533,59 @@ async def test_cloud_disconnect_retry(
|
|||||||
assert mock_async_active_subscription.call_count == 4
|
assert mock_async_active_subscription.call_count == 4
|
||||||
|
|
||||||
|
|
||||||
|
async def test_internet_timeout_then_restore(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
withings: AsyncMock,
|
||||||
|
webhook_config_entry: MockConfigEntry,
|
||||||
|
hass_client_no_auth: ClientSessionGenerator,
|
||||||
|
freezer: FrozenDateTimeFactory,
|
||||||
|
) -> None:
|
||||||
|
"""Test we can recover from internet disconnects."""
|
||||||
|
await mock_cloud(hass)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch("homeassistant.components.cloud.async_is_logged_in", return_value=True),
|
||||||
|
patch.object(cloud, "async_is_connected", return_value=True),
|
||||||
|
patch.object(cloud, "async_active_subscription", return_value=True),
|
||||||
|
patch(
|
||||||
|
"homeassistant.components.cloud.async_create_cloudhook",
|
||||||
|
return_value="https://hooks.nabu.casa/ABCD",
|
||||||
|
),
|
||||||
|
patch(
|
||||||
|
"homeassistant.components.withings.async_get_config_entry_implementation",
|
||||||
|
),
|
||||||
|
patch(
|
||||||
|
"homeassistant.components.cloud.async_delete_cloudhook",
|
||||||
|
),
|
||||||
|
patch(
|
||||||
|
"homeassistant.components.withings.webhook_generate_url",
|
||||||
|
),
|
||||||
|
):
|
||||||
|
await setup_integration(hass, webhook_config_entry)
|
||||||
|
await prepare_webhook_setup(hass, freezer)
|
||||||
|
|
||||||
|
assert cloud.async_active_subscription(hass) is True
|
||||||
|
assert cloud.async_is_connected(hass) is True
|
||||||
|
assert withings.revoke_notification_configurations.call_count == 3
|
||||||
|
assert withings.subscribe_notification.call_count == 6
|
||||||
|
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
withings.list_notification_configurations.side_effect = WithingsConnectionError
|
||||||
|
|
||||||
|
async_mock_cloud_connection_status(hass, False)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert withings.revoke_notification_configurations.call_count == 3
|
||||||
|
withings.list_notification_configurations.side_effect = None
|
||||||
|
|
||||||
|
async_mock_cloud_connection_status(hass, True)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert withings.subscribe_notification.call_count == 12
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
("body", "expected_code"),
|
("body", "expected_code"),
|
||||||
[
|
[
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
"""Tests for the Config Entry Flow helper."""
|
"""Tests for the Config Entry Flow helper."""
|
||||||
|
|
||||||
from collections.abc import Generator
|
import asyncio
|
||||||
|
from collections.abc import Callable, Generator
|
||||||
|
from contextlib import contextmanager
|
||||||
from unittest.mock import Mock, PropertyMock, patch
|
from unittest.mock import Mock, PropertyMock, patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
@ -13,22 +15,44 @@ from homeassistant.helpers import config_entry_flow
|
|||||||
from tests.common import MockConfigEntry, MockModule, mock_integration, mock_platform
|
from tests.common import MockConfigEntry, MockModule, mock_integration, mock_platform
|
||||||
|
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def _make_discovery_flow_conf(
|
||||||
|
has_discovered_devices: Callable[[], asyncio.Future[bool] | bool],
|
||||||
|
) -> Generator[None]:
|
||||||
|
with patch.dict(config_entries.HANDLERS):
|
||||||
|
config_entry_flow.register_discovery_flow(
|
||||||
|
"test", "Test", has_discovered_devices
|
||||||
|
)
|
||||||
|
yield
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def discovery_flow_conf(hass: HomeAssistant) -> Generator[dict[str, bool]]:
|
def async_discovery_flow_conf(hass: HomeAssistant) -> Generator[dict[str, bool]]:
|
||||||
"""Register a handler."""
|
"""Register a handler with an async discovery function."""
|
||||||
handler_conf = {"discovered": False}
|
handler_conf = {"discovered": False}
|
||||||
|
|
||||||
async def has_discovered_devices(hass: HomeAssistant) -> bool:
|
async def has_discovered_devices(hass: HomeAssistant) -> bool:
|
||||||
"""Mock if we have discovered devices."""
|
"""Mock if we have discovered devices."""
|
||||||
return handler_conf["discovered"]
|
return handler_conf["discovered"]
|
||||||
|
|
||||||
with patch.dict(config_entries.HANDLERS):
|
with _make_discovery_flow_conf(has_discovered_devices):
|
||||||
config_entry_flow.register_discovery_flow(
|
|
||||||
"test", "Test", has_discovered_devices
|
|
||||||
)
|
|
||||||
yield handler_conf
|
yield handler_conf
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def discovery_flow_conf(hass: HomeAssistant) -> Generator[dict[str, bool]]:
|
||||||
|
"""Register a handler with a async friendly callback function."""
|
||||||
|
handler_conf = {"discovered": False}
|
||||||
|
|
||||||
|
def has_discovered_devices(hass: HomeAssistant) -> bool:
|
||||||
|
"""Mock if we have discovered devices."""
|
||||||
|
return handler_conf["discovered"]
|
||||||
|
|
||||||
|
with _make_discovery_flow_conf(has_discovered_devices):
|
||||||
|
yield handler_conf
|
||||||
|
handler_conf = {"discovered": False}
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def webhook_flow_conf(hass: HomeAssistant) -> Generator[None]:
|
def webhook_flow_conf(hass: HomeAssistant) -> Generator[None]:
|
||||||
"""Register a handler."""
|
"""Register a handler."""
|
||||||
@ -95,6 +119,33 @@ async def test_user_has_confirmation(
|
|||||||
assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
|
assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
|
||||||
|
|
||||||
|
|
||||||
|
async def test_user_has_confirmation_async_discovery_flow(
|
||||||
|
hass: HomeAssistant, async_discovery_flow_conf: dict[str, bool]
|
||||||
|
) -> None:
|
||||||
|
"""Test user requires confirmation to setup with an async has_discovered_devices."""
|
||||||
|
async_discovery_flow_conf["discovered"] = True
|
||||||
|
mock_platform(hass, "test.config_flow", None)
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
"test", context={"source": config_entries.SOURCE_USER}, data={}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] == data_entry_flow.FlowResultType.FORM
|
||||||
|
assert result["step_id"] == "confirm"
|
||||||
|
|
||||||
|
progress = hass.config_entries.flow.async_progress()
|
||||||
|
assert len(progress) == 1
|
||||||
|
assert progress[0]["flow_id"] == result["flow_id"]
|
||||||
|
assert progress[0]["context"] == {
|
||||||
|
"confirm_only": True,
|
||||||
|
"source": config_entries.SOURCE_USER,
|
||||||
|
"unique_id": "test",
|
||||||
|
}
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
|
||||||
|
assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"source",
|
"source",
|
||||||
[
|
[
|
||||||
|
@ -4118,6 +4118,14 @@ async def test_referenced_labels(hass: HomeAssistant) -> None:
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"sequence": [
|
||||||
|
{
|
||||||
|
"action": "test.script",
|
||||||
|
"data": {"label_id": "label_sequence"},
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
"Test Name",
|
"Test Name",
|
||||||
@ -4135,6 +4143,7 @@ async def test_referenced_labels(hass: HomeAssistant) -> None:
|
|||||||
"label_if_then",
|
"label_if_then",
|
||||||
"label_if_else",
|
"label_if_else",
|
||||||
"label_parallel",
|
"label_parallel",
|
||||||
|
"label_sequence",
|
||||||
}
|
}
|
||||||
# Test we cache results.
|
# Test we cache results.
|
||||||
assert script_obj.referenced_labels is script_obj.referenced_labels
|
assert script_obj.referenced_labels is script_obj.referenced_labels
|
||||||
@ -4220,6 +4229,14 @@ async def test_referenced_floors(hass: HomeAssistant) -> None:
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"sequence": [
|
||||||
|
{
|
||||||
|
"action": "test.script",
|
||||||
|
"data": {"floor_id": "floor_sequence"},
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
"Test Name",
|
"Test Name",
|
||||||
@ -4236,6 +4253,7 @@ async def test_referenced_floors(hass: HomeAssistant) -> None:
|
|||||||
"floor_if_then",
|
"floor_if_then",
|
||||||
"floor_if_else",
|
"floor_if_else",
|
||||||
"floor_parallel",
|
"floor_parallel",
|
||||||
|
"floor_sequence",
|
||||||
}
|
}
|
||||||
# Test we cache results.
|
# Test we cache results.
|
||||||
assert script_obj.referenced_floors is script_obj.referenced_floors
|
assert script_obj.referenced_floors is script_obj.referenced_floors
|
||||||
@ -4321,6 +4339,14 @@ async def test_referenced_areas(hass: HomeAssistant) -> None:
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"sequence": [
|
||||||
|
{
|
||||||
|
"action": "test.script",
|
||||||
|
"data": {"area_id": "area_sequence"},
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
"Test Name",
|
"Test Name",
|
||||||
@ -4337,6 +4363,7 @@ async def test_referenced_areas(hass: HomeAssistant) -> None:
|
|||||||
"area_if_then",
|
"area_if_then",
|
||||||
"area_if_else",
|
"area_if_else",
|
||||||
"area_parallel",
|
"area_parallel",
|
||||||
|
"area_sequence",
|
||||||
# 'area_service_template', # no area extraction from template
|
# 'area_service_template', # no area extraction from template
|
||||||
}
|
}
|
||||||
# Test we cache results.
|
# Test we cache results.
|
||||||
@ -4437,6 +4464,14 @@ async def test_referenced_entities(hass: HomeAssistant) -> None:
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"sequence": [
|
||||||
|
{
|
||||||
|
"action": "test.script",
|
||||||
|
"data": {"entity_id": "light.sequence"},
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
"Test Name",
|
"Test Name",
|
||||||
@ -4456,6 +4491,7 @@ async def test_referenced_entities(hass: HomeAssistant) -> None:
|
|||||||
"light.if_then",
|
"light.if_then",
|
||||||
"light.if_else",
|
"light.if_else",
|
||||||
"light.parallel",
|
"light.parallel",
|
||||||
|
"light.sequence",
|
||||||
# "light.service_template", # no entity extraction from template
|
# "light.service_template", # no entity extraction from template
|
||||||
"scene.hello",
|
"scene.hello",
|
||||||
"sensor.condition",
|
"sensor.condition",
|
||||||
@ -4554,6 +4590,14 @@ async def test_referenced_devices(hass: HomeAssistant) -> None:
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"sequence": [
|
||||||
|
{
|
||||||
|
"action": "test.script",
|
||||||
|
"target": {"device_id": "sequence-device"},
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
"Test Name",
|
"Test Name",
|
||||||
@ -4575,6 +4619,7 @@ async def test_referenced_devices(hass: HomeAssistant) -> None:
|
|||||||
"if-then",
|
"if-then",
|
||||||
"if-else",
|
"if-else",
|
||||||
"parallel-device",
|
"parallel-device",
|
||||||
|
"sequence-device",
|
||||||
}
|
}
|
||||||
# Test we cache results.
|
# Test we cache results.
|
||||||
assert script_obj.referenced_devices is script_obj.referenced_devices
|
assert script_obj.referenced_devices is script_obj.referenced_devices
|
||||||
|
Loading…
x
Reference in New Issue
Block a user