commit
3e1d13b6ad
|
@ -11,5 +11,5 @@
|
|||
"documentation": "https://www.home-assistant.io/integrations/airzone",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["aioairzone"],
|
||||
"requirements": ["aioairzone==0.9.7"]
|
||||
"requirements": ["aioairzone==0.9.9"]
|
||||
}
|
||||
|
|
|
@ -50,7 +50,7 @@ async def async_setup_entry(
|
|||
|
||||
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(
|
||||
AprilaireHumidifierDescription(
|
||||
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(
|
||||
AprilaireHumidifierDescription(
|
||||
key="dehumidifier",
|
||||
|
|
|
@ -1017,9 +1017,18 @@ class PipelineRun:
|
|||
raise RuntimeError("Recognize intent was not prepared")
|
||||
|
||||
if self.pipeline.conversation_language == MATCH_ALL:
|
||||
# LLMs support all languages ('*') so use pipeline language for
|
||||
# intent fallback.
|
||||
input_language = self.pipeline.language
|
||||
# LLMs support all languages ('*') so use languages from the
|
||||
# pipeline for intent fallback.
|
||||
#
|
||||
# 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:
|
||||
input_language = self.pipeline.conversation_language
|
||||
|
||||
|
|
|
@ -6,5 +6,5 @@
|
|||
"documentation": "https://www.home-assistant.io/integrations/aussie_broadband",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aussiebb"],
|
||||
"requirements": ["pyaussiebb==0.1.4"]
|
||||
"requirements": ["pyaussiebb==0.1.5"]
|
||||
}
|
||||
|
|
|
@ -111,7 +111,7 @@
|
|||
"services": {
|
||||
"send_message": {
|
||||
"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": {
|
||||
"entity_id": {
|
||||
"name": "List",
|
||||
|
@ -122,8 +122,8 @@
|
|||
"description": "Type of push notification to send to list members."
|
||||
},
|
||||
"item": {
|
||||
"name": "Article (Required if message type `Urgent Message` selected)",
|
||||
"description": "Article name to include in an urgent message e.g. `Urgent Message - Please buy Cilantro urgently`"
|
||||
"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`"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -134,7 +134,7 @@
|
|||
"going_shopping": "I'm going shopping! - Last chance to make changes",
|
||||
"changed_list": "List updated - Take a look at the articles",
|
||||
"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",
|
||||
"iot_class": "cloud_push",
|
||||
"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",
|
||||
"iot_class": "local_push",
|
||||
"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 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.core import HomeAssistant, callback
|
||||
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)
|
||||
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:
|
||||
|
|
|
@ -7,6 +7,6 @@
|
|||
"documentation": "https://www.home-assistant.io/integrations/freebox",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["freebox_api"],
|
||||
"requirements": ["freebox-api==1.2.1"],
|
||||
"requirements": ["freebox-api==1.2.2"],
|
||||
"zeroconf": ["_fbx-api._tcp.local."]
|
||||
}
|
||||
|
|
|
@ -227,11 +227,12 @@ class SupervisorBackupReaderWriter(BackupReaderWriter):
|
|||
include_addons_set = supervisor_backups.AddonSet.ALL
|
||||
elif include_addons:
|
||||
include_addons_set = set(include_addons)
|
||||
include_folders_set = (
|
||||
{supervisor_backups.Folder(folder) for folder in include_folders}
|
||||
if include_folders
|
||||
else None
|
||||
)
|
||||
include_folders_set = {
|
||||
supervisor_backups.Folder(folder) for folder in include_folders or []
|
||||
}
|
||||
# Always include SSL if Home Assistant is included
|
||||
if include_homeassistant:
|
||||
include_folders_set.add(supervisor_backups.Folder.SSL)
|
||||
|
||||
hassio_agents: list[SupervisorBackupAgent] = [
|
||||
cast(SupervisorBackupAgent, manager.backup_agents[agent_id])
|
||||
|
|
|
@ -9,5 +9,5 @@
|
|||
},
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["apyhiveapi"],
|
||||
"requirements": ["pyhiveapi==0.5.16"]
|
||||
"requirements": ["pyhive-integration==1.0.1"]
|
||||
}
|
||||
|
|
|
@ -52,6 +52,7 @@ from .const import (
|
|||
PROP_MIN_VALUE,
|
||||
SERV_LIGHTBULB,
|
||||
)
|
||||
from .util import get_min_max
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -120,12 +121,14 @@ class Light(HomeAccessory):
|
|||
self.char_brightness = serv_light.configure_char(CHAR_BRIGHTNESS, value=100)
|
||||
|
||||
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)
|
||||
)
|
||||
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)
|
||||
)
|
||||
# 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:
|
||||
self.max_mireds = self.min_mireds
|
||||
self.char_color_temp = serv_light.configure_char(
|
||||
|
@ -282,7 +285,11 @@ class Light(HomeAccessory):
|
|||
hue, saturation = color_temperature_to_hs(color_temp)
|
||||
elif color_mode == ColorMode.WHITE:
|
||||
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
|
||||
else:
|
||||
hue = None
|
||||
|
|
|
@ -14,6 +14,7 @@ from homeassistant.components.climate import (
|
|||
ATTR_HVAC_ACTION,
|
||||
ATTR_HVAC_MODE,
|
||||
ATTR_HVAC_MODES,
|
||||
ATTR_MAX_HUMIDITY,
|
||||
ATTR_MAX_TEMP,
|
||||
ATTR_MIN_HUMIDITY,
|
||||
ATTR_MIN_TEMP,
|
||||
|
@ -21,6 +22,7 @@ from homeassistant.components.climate import (
|
|||
ATTR_SWING_MODES,
|
||||
ATTR_TARGET_TEMP_HIGH,
|
||||
ATTR_TARGET_TEMP_LOW,
|
||||
DEFAULT_MAX_HUMIDITY,
|
||||
DEFAULT_MAX_TEMP,
|
||||
DEFAULT_MIN_HUMIDITY,
|
||||
DEFAULT_MIN_TEMP,
|
||||
|
@ -90,7 +92,7 @@ from .const import (
|
|||
SERV_FANV2,
|
||||
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__)
|
||||
|
||||
|
@ -208,7 +210,10 @@ class Thermostat(HomeAccessory):
|
|||
self.fan_chars: list[str] = []
|
||||
|
||||
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)
|
||||
|
||||
if features & ClimateEntityFeature.TARGET_TEMPERATURE_RANGE:
|
||||
|
@ -839,6 +844,9 @@ def _get_temperature_range_from_state(
|
|||
else:
|
||||
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
|
||||
# the max to appears to work, but less than 0 causes
|
||||
# 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"]
|
||||
new_state = event_data["new_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": [
|
||||
"xknx==3.4.0",
|
||||
"xknxproject==3.8.1",
|
||||
"knx-frontend==2024.12.26.233449"
|
||||
"knx-frontend==2025.1.18.164225"
|
||||
],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["demetriek"],
|
||||
"requirements": ["demetriek==1.1.1"],
|
||||
"requirements": ["demetriek==1.2.0"],
|
||||
"ssdp": [
|
||||
{
|
||||
"deviceType": "urn:schemas-upnp-org:device:LaMetric:1"
|
||||
|
|
|
@ -6,7 +6,7 @@ from collections.abc import Awaitable, Callable
|
|||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from demetriek import Device, LaMetricDevice
|
||||
from demetriek import Device, LaMetricDevice, Range
|
||||
|
||||
from homeassistant.components.number import NumberEntity, NumberEntityDescription
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
|
@ -25,6 +25,7 @@ class LaMetricNumberEntityDescription(NumberEntityDescription):
|
|||
"""Class describing LaMetric number entities."""
|
||||
|
||||
value_fn: Callable[[Device], int | None]
|
||||
range_fn: Callable[[Device], Range | None]
|
||||
has_fn: Callable[[Device], bool] = lambda device: True
|
||||
set_value_fn: Callable[[LaMetricDevice, float], Awaitable[Any]]
|
||||
|
||||
|
@ -33,11 +34,9 @@ NUMBERS = [
|
|||
LaMetricNumberEntityDescription(
|
||||
key="brightness",
|
||||
translation_key="brightness",
|
||||
name="Brightness",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
native_step=1,
|
||||
native_min_value=0,
|
||||
native_max_value=100,
|
||||
range_fn=lambda device: device.display.brightness_limit,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
value_fn=lambda device: device.display.brightness,
|
||||
set_value_fn=lambda device, bri: device.display(brightness=int(bri)),
|
||||
|
@ -45,11 +44,10 @@ NUMBERS = [
|
|||
LaMetricNumberEntityDescription(
|
||||
key="volume",
|
||||
translation_key="volume",
|
||||
name="Volume",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
native_step=1,
|
||||
native_min_value=0,
|
||||
native_max_value=100,
|
||||
range_fn=lambda device: device.audio.volume_range if device.audio else None,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
has_fn=lambda device: bool(device.audio and device.audio.available),
|
||||
value_fn=lambda device: device.audio.volume if device.audio else 0,
|
||||
set_value_fn=lambda api, volume: api.audio(volume=int(volume)),
|
||||
|
@ -93,6 +91,20 @@ class LaMetricNumberEntity(LaMetricEntity, NumberEntity):
|
|||
"""Return the number value."""
|
||||
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
|
||||
async def async_set_native_value(self, value: float) -> None:
|
||||
"""Change to new number value."""
|
||||
|
|
|
@ -66,6 +66,14 @@
|
|||
"name": "Dismiss all notifications"
|
||||
}
|
||||
},
|
||||
"number": {
|
||||
"brightness": {
|
||||
"name": "Brightness"
|
||||
},
|
||||
"volume": {
|
||||
"name": "Volume"
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
"rssi": {
|
||||
"name": "Wi-Fi signal"
|
||||
|
|
|
@ -8,6 +8,6 @@
|
|||
"iot_class": "calculated",
|
||||
"loggers": ["yt_dlp"],
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["yt-dlp[default]==2024.12.23"],
|
||||
"requirements": ["yt-dlp[default]==2025.01.15"],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
|
|
@ -179,14 +179,14 @@ class MqttNumber(MqttEntity, RestoreNumber):
|
|||
return
|
||||
|
||||
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(
|
||||
"Invalid value for %s: %s (range %s - %s)",
|
||||
self.entity_id,
|
||||
num_value,
|
||||
self.min_value,
|
||||
self.max_value,
|
||||
self.native_min_value,
|
||||
self.native_max_value,
|
||||
)
|
||||
return
|
||||
|
||||
|
|
|
@ -112,7 +112,7 @@ class NikoHomeControlLight(NikoHomeControlEntity, LightEntity):
|
|||
|
||||
def turn_on(self, **kwargs: Any) -> None:
|
||||
"""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:
|
||||
"""Instruct the light to turn off."""
|
||||
|
|
|
@ -6,5 +6,5 @@
|
|||
"documentation": "https://www.home-assistant.io/integrations/niko_home_control",
|
||||
"iot_class": "local_push",
|
||||
"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",
|
||||
"iot_class": "local_polling",
|
||||
"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",
|
||||
"granite-code",
|
||||
"granite3-dense",
|
||||
"granite3-guardian" "granite3-moe",
|
||||
"granite3-guardian",
|
||||
"granite3-moe",
|
||||
"hermes3",
|
||||
"internlm2",
|
||||
"llama-guard3",
|
||||
|
|
|
@ -263,16 +263,22 @@ class ONVIFDevice:
|
|||
LOGGER.warning("%s: Could not retrieve date/time on this camera", self.name)
|
||||
return
|
||||
|
||||
cam_date = dt.datetime(
|
||||
cdate.Date.Year,
|
||||
cdate.Date.Month,
|
||||
cdate.Date.Day,
|
||||
cdate.Time.Hour,
|
||||
cdate.Time.Minute,
|
||||
cdate.Time.Second,
|
||||
0,
|
||||
tzone,
|
||||
)
|
||||
try:
|
||||
cam_date = dt.datetime(
|
||||
cdate.Date.Year,
|
||||
cdate.Date.Month,
|
||||
cdate.Date.Day,
|
||||
cdate.Time.Hour,
|
||||
cdate.Time.Minute,
|
||||
cdate.Time.Second,
|
||||
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)
|
||||
|
||||
|
|
|
@ -8,5 +8,5 @@
|
|||
"documentation": "https://www.home-assistant.io/integrations/onvif",
|
||||
"iot_class": "local_push",
|
||||
"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))
|
||||
|
||||
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"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/rainforest_raven",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["aioraven==0.7.0"],
|
||||
"requirements": ["aioraven==0.7.1"],
|
||||
"usb": [
|
||||
{
|
||||
"vid": "0403",
|
||||
|
|
|
@ -209,10 +209,7 @@ class RfxtrxOptionsFlow(OptionsFlow):
|
|||
except ValueError:
|
||||
errors[CONF_COMMAND_OFF] = "invalid_input_2262_off"
|
||||
|
||||
try:
|
||||
off_delay = none_or_int(user_input.get(CONF_OFF_DELAY), 10)
|
||||
except ValueError:
|
||||
errors[CONF_OFF_DELAY] = "invalid_input_off_delay"
|
||||
off_delay = user_input.get(CONF_OFF_DELAY)
|
||||
|
||||
if not errors:
|
||||
devices = {}
|
||||
|
@ -252,11 +249,11 @@ class RfxtrxOptionsFlow(OptionsFlow):
|
|||
vol.Optional(
|
||||
CONF_OFF_DELAY,
|
||||
description={"suggested_value": device_data[CONF_OFF_DELAY]},
|
||||
): str,
|
||||
): int,
|
||||
}
|
||||
else:
|
||||
off_delay_schema = {
|
||||
vol.Optional(CONF_OFF_DELAY): str,
|
||||
vol.Optional(CONF_OFF_DELAY): int,
|
||||
}
|
||||
data_schema.update(off_delay_schema)
|
||||
|
||||
|
|
|
@ -68,7 +68,6 @@
|
|||
"invalid_event_code": "Invalid event code",
|
||||
"invalid_input_2262_on": "Invalid input for command on",
|
||||
"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%]"
|
||||
}
|
||||
},
|
||||
|
|
|
@ -10,12 +10,16 @@ import logging
|
|||
|
||||
from aiohttp.client_exceptions import ClientConnectionError, ClientResponseError
|
||||
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.const import CONF_ACCESS_TOKEN, CONF_CLIENT_ID, CONF_CLIENT_SECRET
|
||||
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.aiohttp_client import async_get_clientsession
|
||||
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.
|
||||
await async_get_loaded_integration(hass, DOMAIN).async_get_platforms(PLATFORMS)
|
||||
|
||||
remove_entry = False
|
||||
try:
|
||||
# See if the app is already setup. This occurs when there are
|
||||
# installs in multiple SmartThings locations (valid use-case)
|
||||
|
@ -175,34 +178,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||
broker.connect()
|
||||
hass.data[DOMAIN][DATA_BROKERS][entry.entry_id] = broker
|
||||
|
||||
except APIInvalidGrant as ex:
|
||||
raise ConfigEntryAuthFailed from ex
|
||||
except ClientResponseError as ex:
|
||||
if ex.status in (HTTPStatus.UNAUTHORIZED, HTTPStatus.FORBIDDEN):
|
||||
_LOGGER.exception(
|
||||
(
|
||||
"Unable to setup configuration entry '%s' - please reconfigure the"
|
||||
" integration"
|
||||
),
|
||||
entry.title,
|
||||
)
|
||||
remove_entry = True
|
||||
else:
|
||||
_LOGGER.debug(ex, exc_info=True)
|
||||
raise ConfigEntryNotReady from ex
|
||||
raise ConfigEntryError(
|
||||
"The access token is no longer valid. Please remove the integration and set up again."
|
||||
) from ex
|
||||
_LOGGER.debug(ex, exc_info=True)
|
||||
raise ConfigEntryNotReady from ex
|
||||
except (ClientConnectionError, RuntimeWarning) as ex:
|
||||
_LOGGER.debug(ex, exc_info=True)
|
||||
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)
|
||||
return True
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
"""Config flow to configure SmartThings."""
|
||||
|
||||
from collections.abc import Mapping
|
||||
from http import HTTPStatus
|
||||
import logging
|
||||
from typing import Any
|
||||
|
@ -9,7 +10,7 @@ from pysmartthings import APIResponseError, AppOAuth, SmartThings
|
|||
from pysmartthings.installedapp import format_install_url
|
||||
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.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)
|
||||
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):
|
||||
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(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
"""SmartApp functionality to receive cloud-push notifications."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import functools
|
||||
import logging
|
||||
|
@ -27,6 +29,7 @@ from pysmartthings import (
|
|||
)
|
||||
|
||||
from homeassistant.components import cloud, webhook
|
||||
from homeassistant.config_entries import ConfigFlowResult
|
||||
from homeassistant.const import CONF_WEBHOOK_ID
|
||||
from homeassistant.core import HomeAssistant
|
||||
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)
|
||||
|
||||
|
||||
async def _continue_flow(
|
||||
async def _find_and_continue_flow(
|
||||
hass: HomeAssistant,
|
||||
app_id: str,
|
||||
location_id: str,
|
||||
|
@ -418,24 +421,34 @@ async def _continue_flow(
|
|||
None,
|
||||
)
|
||||
if flow is not None:
|
||||
await hass.config_entries.flow.async_configure(
|
||||
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,
|
||||
)
|
||||
await _continue_flow(hass, app_id, installed_app_id, refresh_token, flow)
|
||||
|
||||
|
||||
async def _continue_flow(
|
||||
hass: HomeAssistant,
|
||||
app_id: str,
|
||||
installed_app_id: str,
|
||||
refresh_token: str,
|
||||
flow: ConfigFlowResult,
|
||||
) -> None:
|
||||
await hass.config_entries.flow.async_configure(
|
||||
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):
|
||||
"""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
|
||||
)
|
||||
_LOGGER.debug(
|
||||
|
@ -447,6 +460,27 @@ async def smartapp_install(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."""
|
||||
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
|
||||
|
@ -466,7 +500,7 @@ async def smartapp_update(hass: HomeAssistant, req, resp, app):
|
|||
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
|
||||
)
|
||||
_LOGGER.debug(
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
},
|
||||
"pat": {
|
||||
"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": {
|
||||
"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.",
|
||||
"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": {
|
||||
"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": {
|
||||
"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",
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["switchbot-api"],
|
||||
"requirements": ["switchbot-api==2.2.1"]
|
||||
"loggers": ["switchbot_api"],
|
||||
"requirements": ["switchbot-api==2.3.1"]
|
||||
}
|
||||
|
|
|
@ -181,7 +181,6 @@ EVENT_DESCRIPTIONS: tuple[ProtectEventEntityDescription, ...] = (
|
|||
ProtectEventEntityDescription(
|
||||
key="nfc",
|
||||
translation_key="nfc",
|
||||
device_class=EventDeviceClass.DOORBELL,
|
||||
icon="mdi:nfc",
|
||||
ufp_required_field="feature_flags.support_nfc",
|
||||
ufp_event_obj="last_nfc_card_scanned_event",
|
||||
|
@ -191,7 +190,6 @@ EVENT_DESCRIPTIONS: tuple[ProtectEventEntityDescription, ...] = (
|
|||
ProtectEventEntityDescription(
|
||||
key="fingerprint",
|
||||
translation_key="fingerprint",
|
||||
device_class=EventDeviceClass.DOORBELL,
|
||||
icon="mdi:fingerprint",
|
||||
ufp_required_field="feature_flags.has_fingerprint_sensor",
|
||||
ufp_event_obj="last_fingerprint_identified_event",
|
||||
|
|
|
@ -90,7 +90,7 @@ DESCRIPTIONS: list[WatergateSensorEntityDescription] = [
|
|||
WatergateSensorEntityDescription(
|
||||
value_fn=lambda data: (
|
||||
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
|
||||
else None
|
||||
|
@ -104,7 +104,7 @@ DESCRIPTIONS: list[WatergateSensorEntityDescription] = [
|
|||
WatergateSensorEntityDescription(
|
||||
value_fn=lambda data: (
|
||||
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
|
||||
else None
|
||||
|
@ -158,7 +158,11 @@ DESCRIPTIONS: list[WatergateSensorEntityDescription] = [
|
|||
),
|
||||
WatergateSensorEntityDescription(
|
||||
value_fn=lambda data: (
|
||||
PowerSupplyMode(data.state.power_supply.replace("+", "_"))
|
||||
PowerSupplyMode(
|
||||
data.state.power_supply.replace("+", "_").replace(
|
||||
"external_battery", "battery_external"
|
||||
)
|
||||
)
|
||||
if data.state
|
||||
else None
|
||||
),
|
||||
|
|
|
@ -16,6 +16,7 @@ from aiohttp import ClientError
|
|||
from aiohttp.hdrs import METH_POST
|
||||
from aiohttp.web import Request, Response
|
||||
from aiowithings import NotificationCategory, WithingsClient
|
||||
from aiowithings.exceptions import WithingsError
|
||||
from aiowithings.util import to_enum
|
||||
from yarl import URL
|
||||
|
||||
|
@ -223,10 +224,13 @@ class WithingsWebhookManager:
|
|||
"Unregister Withings webhook (%s)", 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:
|
||||
coordinator.webhook_subscription_listener(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(
|
||||
self,
|
||||
|
|
|
@ -25,7 +25,7 @@ if TYPE_CHECKING:
|
|||
APPLICATION_NAME: Final = "HomeAssistant"
|
||||
MAJOR_VERSION: Final = 2025
|
||||
MINOR_VERSION: Final = 1
|
||||
PATCH_VERSION: Final = "2"
|
||||
PATCH_VERSION: Final = "3"
|
||||
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
||||
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
|
||||
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()
|
||||
|
||||
if not (has_devices := bool(in_progress)):
|
||||
has_devices = await cast(
|
||||
"asyncio.Future[bool]", self._discovery_function(self.hass)
|
||||
)
|
||||
discovery_result = 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:
|
||||
return self.async_abort(reason="no_devices_found")
|
||||
|
|
|
@ -1589,6 +1589,9 @@ class Script:
|
|||
target, referenced, script[CONF_SEQUENCE]
|
||||
)
|
||||
|
||||
elif action == cv.SCRIPT_ACTION_SEQUENCE:
|
||||
Script._find_referenced_target(target, referenced, step[CONF_SEQUENCE])
|
||||
|
||||
@cached_property
|
||||
def referenced_devices(self) -> set[str]:
|
||||
"""Return a set of referenced devices."""
|
||||
|
@ -1636,6 +1639,9 @@ class Script:
|
|||
for script in step[CONF_PARALLEL]:
|
||||
Script._find_referenced_devices(referenced, script[CONF_SEQUENCE])
|
||||
|
||||
elif action == cv.SCRIPT_ACTION_SEQUENCE:
|
||||
Script._find_referenced_devices(referenced, step[CONF_SEQUENCE])
|
||||
|
||||
@cached_property
|
||||
def referenced_entities(self) -> set[str]:
|
||||
"""Return a set of referenced entities."""
|
||||
|
@ -1684,6 +1690,9 @@ class Script:
|
|||
for script in step[CONF_PARALLEL]:
|
||||
Script._find_referenced_entities(referenced, script[CONF_SEQUENCE])
|
||||
|
||||
elif action == cv.SCRIPT_ACTION_SEQUENCE:
|
||||
Script._find_referenced_entities(referenced, step[CONF_SEQUENCE])
|
||||
|
||||
def run(
|
||||
self, variables: _VarsType | None = None, context: Context | None = None
|
||||
) -> None:
|
||||
|
|
|
@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||
|
||||
[project]
|
||||
name = "homeassistant"
|
||||
version = "2025.1.2"
|
||||
version = "2025.1.3"
|
||||
license = {text = "Apache-2.0"}
|
||||
description = "Open-source home automation platform running on Python 3."
|
||||
readme = "README.rst"
|
||||
|
|
|
@ -182,7 +182,7 @@ aioairq==0.4.3
|
|||
aioairzone-cloud==0.6.10
|
||||
|
||||
# homeassistant.components.airzone
|
||||
aioairzone==0.9.7
|
||||
aioairzone==0.9.9
|
||||
|
||||
# homeassistant.components.ambient_network
|
||||
# homeassistant.components.ambient_station
|
||||
|
@ -318,7 +318,7 @@ aiooncue==0.3.7
|
|||
aioopenexchangerates==0.6.8
|
||||
|
||||
# homeassistant.components.nmap_tracker
|
||||
aiooui==0.1.7
|
||||
aiooui==0.1.9
|
||||
|
||||
# homeassistant.components.pegel_online
|
||||
aiopegelonline==0.1.1
|
||||
|
@ -344,7 +344,7 @@ aiopyarr==23.4.0
|
|||
aioqsw==0.4.1
|
||||
|
||||
# homeassistant.components.rainforest_raven
|
||||
aioraven==0.7.0
|
||||
aioraven==0.7.1
|
||||
|
||||
# homeassistant.components.recollect_waste
|
||||
aiorecollect==2023.09.0
|
||||
|
@ -738,7 +738,7 @@ debugpy==1.8.11
|
|||
# decora==0.6
|
||||
|
||||
# homeassistant.components.ecovacs
|
||||
deebot-client==10.1.0
|
||||
deebot-client==11.0.0
|
||||
|
||||
# homeassistant.components.ihc
|
||||
# homeassistant.components.namecheapdns
|
||||
|
@ -749,7 +749,7 @@ defusedxml==0.7.1
|
|||
deluge-client==1.10.2
|
||||
|
||||
# homeassistant.components.lametric
|
||||
demetriek==1.1.1
|
||||
demetriek==1.2.0
|
||||
|
||||
# homeassistant.components.denonavr
|
||||
denonavr==1.0.1
|
||||
|
@ -824,7 +824,7 @@ elgato==5.1.2
|
|||
eliqonline==1.2.2
|
||||
|
||||
# homeassistant.components.elkm1
|
||||
elkm1-lib==2.2.10
|
||||
elkm1-lib==2.2.11
|
||||
|
||||
# homeassistant.components.elmax
|
||||
elmax-api==0.0.6.4rc0
|
||||
|
@ -940,7 +940,7 @@ forecast-solar==4.0.0
|
|||
fortiosapi==1.0.5
|
||||
|
||||
# homeassistant.components.freebox
|
||||
freebox-api==1.2.1
|
||||
freebox-api==1.2.2
|
||||
|
||||
# homeassistant.components.free_mobile
|
||||
freesms==0.2.0
|
||||
|
@ -1260,7 +1260,7 @@ kiwiki-client==0.1.1
|
|||
knocki==0.4.2
|
||||
|
||||
# homeassistant.components.knx
|
||||
knx-frontend==2024.12.26.233449
|
||||
knx-frontend==2025.1.18.164225
|
||||
|
||||
# homeassistant.components.konnected
|
||||
konnected==1.2.0
|
||||
|
@ -1467,7 +1467,7 @@ nextcord==2.6.0
|
|||
nextdns==4.0.0
|
||||
|
||||
# homeassistant.components.niko_home_control
|
||||
nhc==0.3.2
|
||||
nhc==0.3.4
|
||||
|
||||
# homeassistant.components.nibe_heatpump
|
||||
nibe==2.14.0
|
||||
|
@ -1537,7 +1537,7 @@ omnilogic==0.4.5
|
|||
ondilo==0.5.0
|
||||
|
||||
# homeassistant.components.onvif
|
||||
onvif-zeep-async==3.1.13
|
||||
onvif-zeep-async==3.2.3
|
||||
|
||||
# homeassistant.components.opengarage
|
||||
open-garage==0.2.0
|
||||
|
@ -1794,7 +1794,7 @@ pyatmo==8.1.0
|
|||
pyatv==0.16.0
|
||||
|
||||
# homeassistant.components.aussie_broadband
|
||||
pyaussiebb==0.1.4
|
||||
pyaussiebb==0.1.5
|
||||
|
||||
# homeassistant.components.balboa
|
||||
pybalboa==1.0.2
|
||||
|
@ -1965,7 +1965,7 @@ pyhaversion==22.8.0
|
|||
pyheos==0.7.2
|
||||
|
||||
# homeassistant.components.hive
|
||||
pyhiveapi==0.5.16
|
||||
pyhive-integration==1.0.1
|
||||
|
||||
# homeassistant.components.homematic
|
||||
pyhomematic==0.1.77
|
||||
|
@ -2782,7 +2782,7 @@ surepy==0.9.0
|
|||
swisshydrodata==0.1.0
|
||||
|
||||
# homeassistant.components.switchbot_cloud
|
||||
switchbot-api==2.2.1
|
||||
switchbot-api==2.3.1
|
||||
|
||||
# homeassistant.components.synology_srm
|
||||
synology-srm==0.2.0
|
||||
|
@ -3082,7 +3082,7 @@ youless-api==2.1.2
|
|||
youtubeaio==1.1.5
|
||||
|
||||
# homeassistant.components.media_extractor
|
||||
yt-dlp[default]==2024.12.23
|
||||
yt-dlp[default]==2025.01.15
|
||||
|
||||
# homeassistant.components.zabbix
|
||||
zabbix-utils==2.0.2
|
||||
|
|
|
@ -170,7 +170,7 @@ aioairq==0.4.3
|
|||
aioairzone-cloud==0.6.10
|
||||
|
||||
# homeassistant.components.airzone
|
||||
aioairzone==0.9.7
|
||||
aioairzone==0.9.9
|
||||
|
||||
# homeassistant.components.ambient_network
|
||||
# homeassistant.components.ambient_station
|
||||
|
@ -300,7 +300,7 @@ aiooncue==0.3.7
|
|||
aioopenexchangerates==0.6.8
|
||||
|
||||
# homeassistant.components.nmap_tracker
|
||||
aiooui==0.1.7
|
||||
aiooui==0.1.9
|
||||
|
||||
# homeassistant.components.pegel_online
|
||||
aiopegelonline==0.1.1
|
||||
|
@ -326,7 +326,7 @@ aiopyarr==23.4.0
|
|||
aioqsw==0.4.1
|
||||
|
||||
# homeassistant.components.rainforest_raven
|
||||
aioraven==0.7.0
|
||||
aioraven==0.7.1
|
||||
|
||||
# homeassistant.components.recollect_waste
|
||||
aiorecollect==2023.09.0
|
||||
|
@ -628,7 +628,7 @@ dbus-fast==2.24.3
|
|||
debugpy==1.8.11
|
||||
|
||||
# homeassistant.components.ecovacs
|
||||
deebot-client==10.1.0
|
||||
deebot-client==11.0.0
|
||||
|
||||
# homeassistant.components.ihc
|
||||
# homeassistant.components.namecheapdns
|
||||
|
@ -639,7 +639,7 @@ defusedxml==0.7.1
|
|||
deluge-client==1.10.2
|
||||
|
||||
# homeassistant.components.lametric
|
||||
demetriek==1.1.1
|
||||
demetriek==1.2.0
|
||||
|
||||
# homeassistant.components.denonavr
|
||||
denonavr==1.0.1
|
||||
|
@ -699,7 +699,7 @@ elevenlabs==1.9.0
|
|||
elgato==5.1.2
|
||||
|
||||
# homeassistant.components.elkm1
|
||||
elkm1-lib==2.2.10
|
||||
elkm1-lib==2.2.11
|
||||
|
||||
# homeassistant.components.elmax
|
||||
elmax-api==0.0.6.4rc0
|
||||
|
@ -796,7 +796,7 @@ foobot_async==1.0.0
|
|||
forecast-solar==4.0.0
|
||||
|
||||
# homeassistant.components.freebox
|
||||
freebox-api==1.2.1
|
||||
freebox-api==1.2.2
|
||||
|
||||
# homeassistant.components.fritz
|
||||
# homeassistant.components.fritzbox_callmonitor
|
||||
|
@ -1062,7 +1062,7 @@ kegtron-ble==0.4.0
|
|||
knocki==0.4.2
|
||||
|
||||
# homeassistant.components.knx
|
||||
knx-frontend==2024.12.26.233449
|
||||
knx-frontend==2025.1.18.164225
|
||||
|
||||
# homeassistant.components.konnected
|
||||
konnected==1.2.0
|
||||
|
@ -1230,7 +1230,7 @@ nextcord==2.6.0
|
|||
nextdns==4.0.0
|
||||
|
||||
# homeassistant.components.niko_home_control
|
||||
nhc==0.3.2
|
||||
nhc==0.3.4
|
||||
|
||||
# homeassistant.components.nibe_heatpump
|
||||
nibe==2.14.0
|
||||
|
@ -1285,7 +1285,7 @@ omnilogic==0.4.5
|
|||
ondilo==0.5.0
|
||||
|
||||
# homeassistant.components.onvif
|
||||
onvif-zeep-async==3.1.13
|
||||
onvif-zeep-async==3.2.3
|
||||
|
||||
# homeassistant.components.opengarage
|
||||
open-garage==0.2.0
|
||||
|
@ -1474,7 +1474,7 @@ pyatmo==8.1.0
|
|||
pyatv==0.16.0
|
||||
|
||||
# homeassistant.components.aussie_broadband
|
||||
pyaussiebb==0.1.4
|
||||
pyaussiebb==0.1.5
|
||||
|
||||
# homeassistant.components.balboa
|
||||
pybalboa==1.0.2
|
||||
|
@ -1594,7 +1594,7 @@ pyhaversion==22.8.0
|
|||
pyheos==0.7.2
|
||||
|
||||
# homeassistant.components.hive
|
||||
pyhiveapi==0.5.16
|
||||
pyhive-integration==1.0.1
|
||||
|
||||
# homeassistant.components.homematic
|
||||
pyhomematic==0.1.77
|
||||
|
@ -2237,7 +2237,7 @@ sunweg==3.0.2
|
|||
surepy==0.9.0
|
||||
|
||||
# homeassistant.components.switchbot_cloud
|
||||
switchbot-api==2.2.1
|
||||
switchbot-api==2.3.1
|
||||
|
||||
# homeassistant.components.system_bridge
|
||||
systembridgeconnector==4.1.5
|
||||
|
@ -2477,7 +2477,7 @@ youless-api==2.1.2
|
|||
youtubeaio==1.1.5
|
||||
|
||||
# homeassistant.components.media_extractor
|
||||
yt-dlp[default]==2024.12.23
|
||||
yt-dlp[default]==2025.01.15
|
||||
|
||||
# homeassistant.components.zamg
|
||||
zamg==0.3.6
|
||||
|
|
|
@ -140,6 +140,7 @@
|
|||
'heatStages': 1,
|
||||
'heatangle': 0,
|
||||
'humidity': 40,
|
||||
'master_zoneID': None,
|
||||
'maxTemp': 30,
|
||||
'minTemp': 15,
|
||||
'mode': 3,
|
||||
|
|
|
@ -28,6 +28,7 @@ from aioairzone.const import (
|
|||
API_HEAT_STAGES,
|
||||
API_HUMIDITY,
|
||||
API_MAC,
|
||||
API_MASTER_ZONE_ID,
|
||||
API_MAX_TEMP,
|
||||
API_MIN_TEMP,
|
||||
API_MODE,
|
||||
|
@ -214,6 +215,7 @@ HVAC_MOCK = {
|
|||
API_FLOOR_DEMAND: 0,
|
||||
API_HEAT_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
|
||||
list([
|
||||
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_ws_client: WebSocketGenerator,
|
||||
init_components,
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> 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)
|
||||
|
||||
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
|
||||
|
||||
# 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
|
||||
|
||||
# Check input to async_converse
|
||||
|
|
|
@ -673,7 +673,7 @@ DEFAULT_BACKUP_OPTIONS = supervisor_backups.PartialBackupOptions(
|
|||
"instance_id": ANY,
|
||||
"with_automatic_settings": False,
|
||||
},
|
||||
folders=None,
|
||||
folders={"ssl"},
|
||||
homeassistant_exclude_database=False,
|
||||
homeassistant=True,
|
||||
location=[None],
|
||||
|
@ -704,7 +704,7 @@ DEFAULT_BACKUP_OPTIONS = supervisor_backups.PartialBackupOptions(
|
|||
),
|
||||
(
|
||||
{"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."""
|
||||
|
||||
from datetime import timedelta
|
||||
import sys
|
||||
|
||||
from pyhap.const import HAP_REPR_AID, HAP_REPR_CHARS, HAP_REPR_IID, HAP_REPR_VALUE
|
||||
import pytest
|
||||
|
@ -540,6 +541,422 @@ async def test_light_color_temperature_and_rgb_color(
|
|||
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(
|
||||
"supported_color_modes", [[ColorMode.HS], [ColorMode.RGB], [ColorMode.XY]]
|
||||
)
|
||||
|
|
|
@ -26,6 +26,7 @@ from homeassistant.components.climate import (
|
|||
ATTR_TARGET_TEMP_STEP,
|
||||
DEFAULT_MAX_TEMP,
|
||||
DEFAULT_MIN_HUMIDITY,
|
||||
DEFAULT_MIN_TEMP,
|
||||
DOMAIN as DOMAIN_CLIMATE,
|
||||
FAN_AUTO,
|
||||
FAN_HIGH,
|
||||
|
@ -2009,8 +2010,8 @@ async def test_thermostat_with_temp_clamps(hass: HomeAssistant, hk_driver) -> No
|
|||
ATTR_SUPPORTED_FEATURES: ClimateEntityFeature.TARGET_TEMPERATURE
|
||||
| ClimateEntityFeature.TARGET_TEMPERATURE_RANGE,
|
||||
ATTR_HVAC_MODES: [HVACMode.HEAT_COOL, HVACMode.AUTO],
|
||||
ATTR_MAX_TEMP: 50,
|
||||
ATTR_MIN_TEMP: 100,
|
||||
ATTR_MAX_TEMP: 100,
|
||||
ATTR_MIN_TEMP: 50,
|
||||
}
|
||||
hass.states.async_set(
|
||||
entity_id,
|
||||
|
@ -2024,14 +2025,14 @@ async def test_thermostat_with_temp_clamps(hass: HomeAssistant, hk_driver) -> No
|
|||
acc.run()
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert acc.char_cooling_thresh_temp.value == 100
|
||||
assert acc.char_heating_thresh_temp.value == 100
|
||||
assert acc.char_cooling_thresh_temp.value == 50
|
||||
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_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_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_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()
|
||||
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_current_heat_cool.value == 1
|
||||
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[1].data[ATTR_ENTITY_ID] == entity_id
|
||||
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
|
||||
|
|
|
@ -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**',
|
||||
'display': dict({
|
||||
'brightness': 100,
|
||||
'brightness_limit': dict({
|
||||
'range_max': 100,
|
||||
'range_min': 2,
|
||||
}),
|
||||
'brightness_mode': 'auto',
|
||||
'brightness_range': dict({
|
||||
'range_max': 100,
|
||||
'range_min': 0,
|
||||
}),
|
||||
'display_type': 'mixed',
|
||||
'height': 8,
|
||||
'on': None,
|
||||
|
|
|
@ -42,7 +42,7 @@ async def test_brightness(
|
|||
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_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_UNIT_OF_MEASUREMENT) == PERCENTAGE
|
||||
assert state.state == "100"
|
||||
|
@ -183,3 +183,16 @@ async def test_number_connection_error(
|
|||
state = hass.states.get("number.frenck_s_lametric_volume")
|
||||
assert state
|
||||
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,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, State
|
||||
from homeassistant.util.unit_system import US_CUSTOMARY_SYSTEM
|
||||
|
||||
from .test_common import (
|
||||
help_custom_config,
|
||||
|
@ -157,6 +158,101 @@ async def test_run_number_setup(
|
|||
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(
|
||||
"hass_config",
|
||||
[
|
||||
|
|
|
@ -42,11 +42,11 @@ async def test_entities(
|
|||
@pytest.mark.parametrize(
|
||||
("light_id", "data", "set_brightness"),
|
||||
[
|
||||
(0, {ATTR_ENTITY_ID: "light.light"}, 100.0),
|
||||
(0, {ATTR_ENTITY_ID: "light.light"}, 100),
|
||||
(
|
||||
1,
|
||||
{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"],
|
||||
user_input={
|
||||
"data_bits": 4,
|
||||
"off_delay": "abcdef",
|
||||
"command_on": "xyz",
|
||||
"command_off": "xyz",
|
||||
},
|
||||
|
@ -735,7 +734,6 @@ async def test_options_add_and_configure_device(
|
|||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "set_device_options"
|
||||
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_off"] == "invalid_input_2262_off"
|
||||
|
||||
|
@ -745,7 +743,7 @@ async def test_options_add_and_configure_device(
|
|||
"data_bits": 4,
|
||||
"command_on": "0xE",
|
||||
"command_off": "0x7",
|
||||
"off_delay": "9",
|
||||
"off_delay": 9,
|
||||
},
|
||||
)
|
||||
|
||||
|
|
|
@ -14,6 +14,7 @@ from homeassistant.components.smartthings.const import (
|
|||
CONF_APP_ID,
|
||||
CONF_INSTALLED_APP_ID,
|
||||
CONF_LOCATION_ID,
|
||||
CONF_REFRESH_TOKEN,
|
||||
DOMAIN,
|
||||
)
|
||||
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["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,
|
||||
SIGNAL_SMARTTHINGS_UPDATE,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.core_config import async_process_ha_core_config
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
|
@ -68,17 +69,10 @@ async def test_unrecoverable_api_errors_create_new_flow(
|
|||
)
|
||||
|
||||
# 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 entry was removed and new flow created
|
||||
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"])
|
||||
assert config_entry.state == ConfigEntryState.SETUP_ERROR
|
||||
|
||||
|
||||
async def test_recoverable_api_errors_raise_not_ready(
|
||||
|
|
|
@ -43,7 +43,7 @@
|
|||
'last_changed': <ANY>,
|
||||
'last_reported': <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]
|
||||
|
@ -501,6 +501,6 @@
|
|||
'last_changed': <ANY>,
|
||||
'last_reported': <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 = {
|
||||
"type": "power-supply-changed",
|
||||
"data": {"supply": "external"},
|
||||
"data": {"supply": "external_battery"},
|
||||
}
|
||||
client = await hass_client_no_auth()
|
||||
await client.post(f"/api/webhook/{MOCK_WEBHOOK_ID}", json=power_supply_change_data)
|
||||
|
||||
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 (
|
||||
NotificationCategory,
|
||||
WithingsAuthenticationFailedError,
|
||||
WithingsConnectionError,
|
||||
WithingsUnauthorizedError,
|
||||
)
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
|
@ -532,6 +533,59 @@ async def test_cloud_disconnect_retry(
|
|||
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(
|
||||
("body", "expected_code"),
|
||||
[
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
"""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
|
||||
|
||||
import pytest
|
||||
|
@ -13,22 +15,44 @@ from homeassistant.helpers import config_entry_flow
|
|||
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
|
||||
def discovery_flow_conf(hass: HomeAssistant) -> Generator[dict[str, bool]]:
|
||||
"""Register a handler."""
|
||||
def async_discovery_flow_conf(hass: HomeAssistant) -> Generator[dict[str, bool]]:
|
||||
"""Register a handler with an async discovery function."""
|
||||
handler_conf = {"discovered": False}
|
||||
|
||||
async def has_discovered_devices(hass: HomeAssistant) -> bool:
|
||||
"""Mock if we have discovered devices."""
|
||||
return handler_conf["discovered"]
|
||||
|
||||
with patch.dict(config_entries.HANDLERS):
|
||||
config_entry_flow.register_discovery_flow(
|
||||
"test", "Test", has_discovered_devices
|
||||
)
|
||||
with _make_discovery_flow_conf(has_discovered_devices):
|
||||
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
|
||||
def webhook_flow_conf(hass: HomeAssistant) -> Generator[None]:
|
||||
"""Register a handler."""
|
||||
|
@ -95,6 +119,33 @@ async def test_user_has_confirmation(
|
|||
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(
|
||||
"source",
|
||||
[
|
||||
|
|
|
@ -4118,6 +4118,14 @@ async def test_referenced_labels(hass: HomeAssistant) -> None:
|
|||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
"sequence": [
|
||||
{
|
||||
"action": "test.script",
|
||||
"data": {"label_id": "label_sequence"},
|
||||
}
|
||||
],
|
||||
},
|
||||
]
|
||||
),
|
||||
"Test Name",
|
||||
|
@ -4135,6 +4143,7 @@ async def test_referenced_labels(hass: HomeAssistant) -> None:
|
|||
"label_if_then",
|
||||
"label_if_else",
|
||||
"label_parallel",
|
||||
"label_sequence",
|
||||
}
|
||||
# Test we cache results.
|
||||
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",
|
||||
|
@ -4236,6 +4253,7 @@ async def test_referenced_floors(hass: HomeAssistant) -> None:
|
|||
"floor_if_then",
|
||||
"floor_if_else",
|
||||
"floor_parallel",
|
||||
"floor_sequence",
|
||||
}
|
||||
# Test we cache results.
|
||||
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",
|
||||
|
@ -4337,6 +4363,7 @@ async def test_referenced_areas(hass: HomeAssistant) -> None:
|
|||
"area_if_then",
|
||||
"area_if_else",
|
||||
"area_parallel",
|
||||
"area_sequence",
|
||||
# 'area_service_template', # no area extraction from template
|
||||
}
|
||||
# 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",
|
||||
|
@ -4456,6 +4491,7 @@ async def test_referenced_entities(hass: HomeAssistant) -> None:
|
|||
"light.if_then",
|
||||
"light.if_else",
|
||||
"light.parallel",
|
||||
"light.sequence",
|
||||
# "light.service_template", # no entity extraction from template
|
||||
"scene.hello",
|
||||
"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",
|
||||
|
@ -4575,6 +4619,7 @@ async def test_referenced_devices(hass: HomeAssistant) -> None:
|
|||
"if-then",
|
||||
"if-else",
|
||||
"parallel-device",
|
||||
"sequence-device",
|
||||
}
|
||||
# Test we cache results.
|
||||
assert script_obj.referenced_devices is script_obj.referenced_devices
|
||||
|
|
Loading…
Reference in New Issue