master 2025.1.3
Franck Nijhof 2025-01-20 18:04:03 +01:00 committed by GitHub
commit 3e1d13b6ad
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
64 changed files with 1430 additions and 189 deletions

View File

@ -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"]
}

View File

@ -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",

View File

@ -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

View File

@ -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"]
}

View File

@ -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"
}
}
}

View File

@ -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"]
}

View File

@ -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"]
}

View File

@ -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:

View File

@ -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."]
}

View File

@ -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])

View File

@ -9,5 +9,5 @@
},
"iot_class": "cloud_polling",
"loggers": ["apyhiveapi"],
"requirements": ["pyhiveapi==0.5.16"]
"requirements": ["pyhive-integration==1.0.1"]
}

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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
}

View File

@ -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"

View File

@ -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."""

View File

@ -66,6 +66,14 @@
"name": "Dismiss all notifications"
}
},
"number": {
"brightness": {
"name": "Brightness"
},
"volume": {
"name": "Volume"
}
},
"sensor": {
"rssi": {
"name": "Wi-Fi signal"

View File

@ -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
}

View File

@ -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

View File

@ -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."""

View File

@ -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"]
}

View File

@ -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"]
}

View File

@ -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",

View File

@ -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)

View File

@ -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"]
}

View File

@ -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)
)

View File

@ -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",

View File

@ -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)

View File

@ -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%]"
}
},

View File

@ -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

View File

@ -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:

View File

@ -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(

View File

@ -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",

View File

@ -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"]
}

View File

@ -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",

View File

@ -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
),

View File

@ -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,

View File

@ -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)

View File

@ -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")

View File

@ -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:

View File

@ -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"

View File

@ -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

View File

@ -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

View File

@ -140,6 +140,7 @@
'heatStages': 1,
'heatangle': 0,
'humidity': 40,
'master_zoneID': None,
'maxTemp': 30,
'minTemp': 15,
'mode': 3,

View File

@ -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,
},
]
},

View File

@ -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({

View File

@ -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

View File

@ -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"}),
),
(
{

View File

@ -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]]
)

View File

@ -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

View File

@ -0,0 +1,68 @@
{
"audio": {
"available": true,
"volume": 53,
"volume_limit": {
"max": 100,
"min": 0
},
"volume_range": {
"max": 100,
"min": 0
}
},
"bluetooth": {
"active": false,
"address": "40:F4:C9:AA:AA:AA",
"available": true,
"discoverable": true,
"mac": "40:F4:C9:AA:AA:AA",
"name": "LM8367",
"pairable": false
},
"display": {
"brightness": 75,
"brightness_limit": {
"max": 76,
"min": 2
},
"brightness_mode": "manual",
"brightness_range": {
"max": 100,
"min": 0
},
"height": 8,
"on": true,
"screensaver": {
"enabled": true,
"modes": {
"time_based": {
"enabled": false
},
"when_dark": {
"enabled": true
}
},
"widget": "1_com.lametric.clock"
},
"type": "mixed",
"width": 37
},
"id": "67790",
"mode": "manual",
"model": "sa8",
"name": "TIME",
"os_version": "3.1.3",
"serial_number": "SA840700836700W00BAA",
"wifi": {
"active": true,
"mac": "40:F4:C9:AA:AA:AA",
"available": true,
"encryption": "WPA",
"ssid": "My wifi",
"ip": "10.0.0.99",
"mode": "dhcp",
"netmask": "255.255.255.0",
"rssi": 78
}
}

View File

@ -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,

View File

@ -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

View File

@ -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",
[

View File

@ -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,
),
],
)

View File

@ -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,
},
)

View File

@ -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

View File

@ -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(

View File

@ -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',
})
# ---

View File

@ -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"

View File

@ -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"),
[

View File

@ -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",
[

View File

@ -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