Merge pull request #63004 from home-assistant/rc

This commit is contained in:
Franck Nijhof 2021-12-29 18:17:01 +01:00 committed by GitHub
commit 92066d2a62
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 350 additions and 98 deletions

View File

@ -3,7 +3,7 @@
"name": "Flux LED/MagicHome",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/flux_led",
"requirements": ["flux_led==0.27.13"],
"requirements": ["flux_led==0.27.21"],
"quality_scale": "platinum",
"codeowners": ["@icemanch"],
"iot_class": "local_push",

View File

@ -3,7 +3,7 @@
"name": "Home Assistant Frontend",
"documentation": "https://www.home-assistant.io/integrations/frontend",
"requirements": [
"home-assistant-frontend==20211227.0"
"home-assistant-frontend==20211229.0"
],
"dependencies": [
"api",

View File

@ -3,7 +3,7 @@
"name": "Philips Hue",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/hue",
"requirements": ["aiohue==3.0.10"],
"requirements": ["aiohue==3.0.11"],
"ssdp": [
{
"manufacturer": "Royal Philips Electronics",

View File

@ -1,6 +1,7 @@
"""Support for Hue groups (room/zone)."""
from __future__ import annotations
import asyncio
from typing import Any
from aiohue.v2 import HueBridgeV2
@ -18,6 +19,7 @@ from homeassistant.components.light import (
COLOR_MODE_COLOR_TEMP,
COLOR_MODE_ONOFF,
COLOR_MODE_XY,
FLASH_SHORT,
SUPPORT_FLASH,
SUPPORT_TRANSITION,
LightEntity,
@ -29,14 +31,17 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from ..bridge import HueBridge
from ..const import CONF_ALLOW_HUE_GROUPS, DOMAIN
from .entity import HueBaseEntity
from .helpers import normalize_hue_brightness, normalize_hue_transition
from .helpers import (
normalize_hue_brightness,
normalize_hue_colortemp,
normalize_hue_transition,
)
ALLOWED_ERRORS = [
"device (groupedLight) has communication issues, command (on) may not have effect",
'device (groupedLight) is "soft off", command (on) may not have effect',
"device (light) has communication issues, command (on) may not have effect",
'device (light) is "soft off", command (on) may not have effect',
"attribute (supportedAlertActions) cannot be written",
]
@ -150,10 +155,15 @@ class GroupedHueLight(HueBaseEntity, LightEntity):
"""Turn the light on."""
transition = normalize_hue_transition(kwargs.get(ATTR_TRANSITION))
xy_color = kwargs.get(ATTR_XY_COLOR)
color_temp = kwargs.get(ATTR_COLOR_TEMP)
color_temp = normalize_hue_colortemp(kwargs.get(ATTR_COLOR_TEMP))
brightness = normalize_hue_brightness(kwargs.get(ATTR_BRIGHTNESS))
flash = kwargs.get(ATTR_FLASH)
if flash is not None:
await self.async_set_flash(flash)
# flash can not be sent with other commands at the same time
return
# NOTE: a grouped_light can only handle turn on/off
# To set other features, you'll have to control the attached lights
if (
@ -173,22 +183,32 @@ class GroupedHueLight(HueBaseEntity, LightEntity):
# redirect all other feature commands to underlying lights
# note that this silently ignores params sent to light that are not supported
for light in self.controller.get_lights(self.resource.id):
await self.bridge.async_request_call(
self.api.lights.set_state,
light.id,
on=True,
brightness=brightness if light.supports_dimming else None,
color_xy=xy_color if light.supports_color else None,
color_temp=color_temp if light.supports_color_temperature else None,
transition_time=transition,
alert=AlertEffectType.BREATHE if flash is not None else None,
allowed_errors=ALLOWED_ERRORS,
)
await asyncio.gather(
*[
self.bridge.async_request_call(
self.api.lights.set_state,
light.id,
on=True,
brightness=brightness if light.supports_dimming else None,
color_xy=xy_color if light.supports_color else None,
color_temp=color_temp if light.supports_color_temperature else None,
transition_time=transition,
alert=AlertEffectType.BREATHE if flash is not None else None,
allowed_errors=ALLOWED_ERRORS,
)
for light in self.controller.get_lights(self.resource.id)
]
)
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the light off."""
transition = normalize_hue_transition(kwargs.get(ATTR_TRANSITION))
flash = kwargs.get(ATTR_FLASH)
if flash is not None:
await self.async_set_flash(flash)
# flash can not be sent with other commands at the same time
return
# NOTE: a grouped_light can only handle turn on/off
# To set other features, you'll have to control the attached lights
@ -202,14 +222,31 @@ class GroupedHueLight(HueBaseEntity, LightEntity):
return
# redirect all other feature commands to underlying lights
for light in self.controller.get_lights(self.resource.id):
await self.bridge.async_request_call(
self.api.lights.set_state,
light.id,
on=False,
transition_time=transition,
allowed_errors=ALLOWED_ERRORS,
)
await asyncio.gather(
*[
self.bridge.async_request_call(
self.api.lights.set_state,
light.id,
on=False,
transition_time=transition,
allowed_errors=ALLOWED_ERRORS,
)
for light in self.controller.get_lights(self.resource.id)
]
)
async def async_set_flash(self, flash: str) -> None:
"""Send flash command to light."""
await asyncio.gather(
*[
self.bridge.async_request_call(
self.api.lights.set_flash,
id=light.id,
short=flash == FLASH_SHORT,
)
for light in self.controller.get_lights(self.resource.id)
]
)
@callback
def on_update(self) -> None:

View File

@ -1,7 +1,8 @@
"""Helper functions for Philips Hue v2."""
from __future__ import annotations
def normalize_hue_brightness(brightness):
def normalize_hue_brightness(brightness: float | None) -> float | None:
"""Return calculated brightness values."""
if brightness is not None:
# Hue uses a range of [0, 100] to control brightness.
@ -10,10 +11,19 @@ def normalize_hue_brightness(brightness):
return brightness
def normalize_hue_transition(transition):
def normalize_hue_transition(transition: float | None) -> float | None:
"""Return rounded transition values."""
if transition is not None:
# hue transition duration is in milliseconds and round them to 100ms
transition = int(round(transition, 1) * 1000)
return transition
def normalize_hue_colortemp(colortemp: int | None) -> int | None:
"""Return color temperature within Hue's ranges."""
if colortemp is not None:
# Hue only accepts a range between 153..500
colortemp = min(colortemp, 500)
colortemp = max(colortemp, 153)
return colortemp

View File

@ -4,7 +4,7 @@ from typing import TYPE_CHECKING
from aiohue.v2 import HueBridgeV2
from aiohue.v2.controllers.events import EventType
from aiohue.v2.models.button import Button, ButtonEvent
from aiohue.v2.models.button import Button
from homeassistant.const import CONF_DEVICE_ID, CONF_ID, CONF_TYPE, CONF_UNIQUE_ID
from homeassistant.core import callback
@ -27,11 +27,6 @@ async def async_setup_hue_events(bridge: "HueBridge"):
api: HueBridgeV2 = bridge.api # to satisfy typing
conf_entry = bridge.config_entry
dev_reg = device_registry.async_get(hass)
last_state = {
x.id: x.button.last_event
for x in api.sensors.button.items
if x.button is not None
}
# at this time the `button` resource is the only source of hue events
btn_controller = api.sensors.button
@ -45,26 +40,16 @@ async def async_setup_hue_events(bridge: "HueBridge"):
if hue_resource.button is None:
return
cur_event = hue_resource.button.last_event
last_event = last_state.get(hue_resource.id)
# ignore the event if the last_event value is exactly the same
# this may happen if some other metadata of the button resource is adjusted
if cur_event == last_event:
return
if cur_event != ButtonEvent.REPEAT:
# do not store repeat event
last_state[hue_resource.id] = cur_event
hue_device = btn_controller.get_device(hue_resource.id)
device = dev_reg.async_get_device({(DOMAIN, hue_device.id)})
# Fire event
data = {
# send slugified entity name as id = backwards compatibility with previous version
CONF_ID: slugify(f"{hue_device.metadata.name}: Button"),
CONF_ID: slugify(f"{hue_device.metadata.name} Button"),
CONF_DEVICE_ID: device.id, # type: ignore
CONF_UNIQUE_ID: hue_resource.id,
CONF_TYPE: cur_event.value,
CONF_TYPE: hue_resource.button.last_event.value,
CONF_SUBTYPE: hue_resource.metadata.control_id,
}
hass.bus.async_fire(ATTR_HUE_EVENT, data)

View File

@ -6,7 +6,6 @@ from typing import Any
from aiohue import HueBridgeV2
from aiohue.v2.controllers.events import EventType
from aiohue.v2.controllers.lights import LightsController
from aiohue.v2.models.feature import AlertEffectType
from aiohue.v2.models.light import Light
from homeassistant.components.light import (
@ -19,6 +18,7 @@ from homeassistant.components.light import (
COLOR_MODE_COLOR_TEMP,
COLOR_MODE_ONOFF,
COLOR_MODE_XY,
FLASH_SHORT,
SUPPORT_FLASH,
SUPPORT_TRANSITION,
LightEntity,
@ -30,12 +30,15 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from ..bridge import HueBridge
from ..const import DOMAIN
from .entity import HueBaseEntity
from .helpers import normalize_hue_brightness, normalize_hue_transition
from .helpers import (
normalize_hue_brightness,
normalize_hue_colortemp,
normalize_hue_transition,
)
ALLOWED_ERRORS = [
"device (light) has communication issues, command (on) may not have effect",
'device (light) is "soft off", command (on) may not have effect',
"attribute (supportedAlertActions) cannot be written",
]
@ -73,7 +76,8 @@ class HueLight(HueBaseEntity, LightEntity):
) -> None:
"""Initialize the light."""
super().__init__(bridge, controller, resource)
self._attr_supported_features |= SUPPORT_FLASH
if self.resource.alert and self.resource.alert.action_values:
self._attr_supported_features |= SUPPORT_FLASH
self.resource = resource
self.controller = controller
self._supported_color_modes = set()
@ -158,10 +162,18 @@ class HueLight(HueBaseEntity, LightEntity):
"""Turn the device on."""
transition = normalize_hue_transition(kwargs.get(ATTR_TRANSITION))
xy_color = kwargs.get(ATTR_XY_COLOR)
color_temp = kwargs.get(ATTR_COLOR_TEMP)
color_temp = normalize_hue_colortemp(kwargs.get(ATTR_COLOR_TEMP))
brightness = normalize_hue_brightness(kwargs.get(ATTR_BRIGHTNESS))
flash = kwargs.get(ATTR_FLASH)
if flash is not None:
await self.async_set_flash(flash)
# flash can not be sent with other commands at the same time or result will be flaky
# Hue's default behavior is that a light returns to its previous state for short
# flash (identify) and the light is kept turned on for long flash (breathe effect)
# Why is this flash alert/effect hidden in the turn_on/off commands ?
return
await self.bridge.async_request_call(
self.controller.set_state,
id=self.resource.id,
@ -170,7 +182,6 @@ class HueLight(HueBaseEntity, LightEntity):
color_xy=xy_color,
color_temp=color_temp,
transition_time=transition,
alert=AlertEffectType.BREATHE if flash is not None else None,
allowed_errors=ALLOWED_ERRORS,
)
@ -179,11 +190,25 @@ class HueLight(HueBaseEntity, LightEntity):
transition = normalize_hue_transition(kwargs.get(ATTR_TRANSITION))
flash = kwargs.get(ATTR_FLASH)
if flash is not None:
await self.async_set_flash(flash)
# flash can not be sent with other commands at the same time or result will be flaky
# Hue's default behavior is that a light returns to its previous state for short
# flash (identify) and the light is kept turned on for long flash (breathe effect)
return
await self.bridge.async_request_call(
self.controller.set_state,
id=self.resource.id,
on=False,
transition_time=transition,
alert=AlertEffectType.BREATHE if flash is not None else None,
allowed_errors=ALLOWED_ERRORS,
)
async def async_set_flash(self, flash: str) -> None:
"""Send flash command to light."""
await self.bridge.async_request_call(
self.controller.set_flash,
id=self.resource.id,
short=flash == FLASH_SHORT,
)

View File

@ -27,6 +27,7 @@ from .const import (
DOMAIN,
ERROR_STATES,
)
from .helpers import parse_id
_LOGGER = logging.getLogger(__name__)
@ -80,6 +81,14 @@ async def async_setup_entry(hass, entry):
hass.data.setdefault(DOMAIN, {})
# Migration of entry unique_id
if isinstance(entry.unique_id, int):
new_id = parse_id(entry.unique_id)
params = {"unique_id": new_id}
if entry.title == entry.unique_id:
params["title"] = new_id
hass.config_entries.async_update_entry(entry, **params)
try:
bridge = await hass.async_add_executor_job(
NukiBridge,

View File

@ -12,6 +12,7 @@ from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TOKEN
from homeassistant.data_entry_flow import FlowResult
from .const import DEFAULT_PORT, DEFAULT_TIMEOUT, DOMAIN
from .helpers import parse_id
_LOGGER = logging.getLogger(__name__)
@ -69,7 +70,7 @@ class NukiConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult:
"""Prepare configuration for a DHCP discovered Nuki bridge."""
await self.async_set_unique_id(int(discovery_info.hostname[12:], 16))
await self.async_set_unique_id(discovery_info.hostname[12:].upper())
self._abort_if_unique_id_configured()
@ -114,7 +115,9 @@ class NukiConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
errors["base"] = "unknown"
if not errors:
existing_entry = await self.async_set_unique_id(info["ids"]["hardwareId"])
existing_entry = await self.async_set_unique_id(
parse_id(info["ids"]["hardwareId"])
)
if existing_entry:
self.hass.config_entries.async_update_entry(existing_entry, data=conf)
self.hass.async_create_task(
@ -143,11 +146,10 @@ class NukiConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
errors["base"] = "unknown"
if "base" not in errors:
await self.async_set_unique_id(info["ids"]["hardwareId"])
bridge_id = parse_id(info["ids"]["hardwareId"])
await self.async_set_unique_id(bridge_id)
self._abort_if_unique_id_configured()
return self.async_create_entry(
title=info["ids"]["hardwareId"], data=user_input
)
return self.async_create_entry(title=bridge_id, data=user_input)
data_schema = self.discovery_schema or USER_SCHEMA
return self.async_show_form(

View File

@ -0,0 +1,6 @@
"""nuki integration helpers."""
def parse_id(hardware_id):
"""Parse Nuki ID."""
return hex(hardware_id).split("x")[-1].upper()

View File

@ -5,7 +5,7 @@
"documentation": "https://www.home-assistant.io/integrations/smarttub",
"dependencies": [],
"codeowners": ["@mdz"],
"requirements": ["python-smarttub==0.0.28"],
"requirements": ["python-smarttub==0.0.29"],
"quality_scale": "platinum",
"iot_class": "cloud_polling"
}

View File

@ -482,7 +482,7 @@ class SonosSpeaker:
for bool_var in (
"dialog_level",
"night_level",
"night_mode",
"sub_enabled",
"surround_enabled",
):

View File

@ -7,8 +7,9 @@ import logging
from soco.exceptions import SoCoException, SoCoSlaveException, SoCoUPnPException
from homeassistant.components.switch import ENTITY_ID_FORMAT, SwitchEntity
from homeassistant.const import ATTR_TIME, ENTITY_CATEGORY_CONFIG
from homeassistant.core import callback
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_TIME, ENTITY_CATEGORY_CONFIG, Platform
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers.dispatcher import async_dispatcher_connect
@ -113,6 +114,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
available_soco_attributes, speaker
)
for feature_type in available_features:
if feature_type == ATTR_SPEECH_ENHANCEMENT:
async_migrate_speech_enhancement_entity_unique_id(
hass, config_entry, speaker
)
_LOGGER.debug(
"Creating %s switch on %s",
FRIENDLY_NAMES[feature_type],
@ -344,3 +349,48 @@ class SonosAlarmEntity(SonosEntity, SwitchEntity):
await self.hass.async_add_executor_job(self.alarm.save)
except (OSError, SoCoException, SoCoUPnPException) as exc:
_LOGGER.error("Could not update %s: %s", self.entity_id, exc)
@callback
def async_migrate_speech_enhancement_entity_unique_id(
hass: HomeAssistant,
config_entry: ConfigEntry,
speaker: SonosSpeaker,
) -> None:
"""Migrate Speech Enhancement switch entity unique_id."""
entity_registry = er.async_get(hass)
registry_entries = er.async_entries_for_config_entry(
entity_registry, config_entry.entry_id
)
speech_enhancement_entries = [
entry
for entry in registry_entries
if entry.domain == Platform.SWITCH
and entry.original_icon == FEATURE_ICONS[ATTR_SPEECH_ENHANCEMENT]
and entry.unique_id.startswith(speaker.soco.uid)
]
if len(speech_enhancement_entries) > 1:
_LOGGER.warning(
"Migration of Speech Enhancement switches on %s failed, manual cleanup required: %s",
speaker.zone_name,
[e.entity_id for e in speech_enhancement_entries],
)
return
if len(speech_enhancement_entries) == 1:
old_entry = speech_enhancement_entries[0]
if old_entry.unique_id.endswith("dialog_level"):
return
new_unique_id = f"{speaker.soco.uid}-{ATTR_SPEECH_ENHANCEMENT}"
_LOGGER.debug(
"Migrating unique_id for %s from %s to %s",
old_entry.entity_id,
old_entry.unique_id,
new_unique_id,
)
entity_registry.async_update_entity(
old_entry.entity_id, new_unique_id=new_unique_id
)

View File

@ -32,7 +32,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import HomeAssistantTuyaData
from .base import EnumTypeData, IntegerTypeData, TuyaEntity
from .const import DOMAIN, TUYA_DISCOVERY_NEW, DPCode
from .const import DOMAIN, LOGGER, TUYA_DISCOVERY_NEW, DPCode
TUYA_HVAC_TO_HA = {
"auto": HVAC_MODE_HEAT_COOL,
@ -182,10 +182,10 @@ class TuyaClimateEntity(TuyaEntity, ClimateEntity):
# it to define min, max & step temperatures
if (
self._set_temperature_dpcode
and self._set_temperature_dpcode in device.status_range
and self._set_temperature_dpcode in device.function
):
type_data = IntegerTypeData.from_json(
device.status_range[self._set_temperature_dpcode].values
device.function[self._set_temperature_dpcode].values
)
self._attr_supported_features |= SUPPORT_TARGET_TEMPERATURE
self._set_temperature_type = type_data
@ -232,14 +232,11 @@ class TuyaClimateEntity(TuyaEntity, ClimateEntity):
]
# Determine dpcode to use for setting the humidity
if (
DPCode.HUMIDITY_SET in device.status
and DPCode.HUMIDITY_SET in device.status_range
):
if DPCode.HUMIDITY_SET in device.function:
self._attr_supported_features |= SUPPORT_TARGET_HUMIDITY
self._set_humidity_dpcode = DPCode.HUMIDITY_SET
type_data = IntegerTypeData.from_json(
device.status_range[DPCode.HUMIDITY_SET].values
device.function[DPCode.HUMIDITY_SET].values
)
self._set_humidity_type = type_data
self._attr_min_humidity = int(type_data.min_scaled)
@ -298,6 +295,21 @@ class TuyaClimateEntity(TuyaEntity, ClimateEntity):
if DPCode.SWITCH_VERTICAL in device.function:
self._attr_swing_modes.append(SWING_VERTICAL)
async def async_added_to_hass(self) -> None:
"""Call when entity is added to hass."""
await super().async_added_to_hass()
# Log unknown modes
if DPCode.MODE in self.device.function:
data_type = EnumTypeData.from_json(self.device.function[DPCode.MODE].values)
for tuya_mode in data_type.range:
if tuya_mode not in TUYA_HVAC_TO_HA:
LOGGER.warning(
"Unknown HVAC mode '%s' for device %s; assuming it as off",
tuya_mode,
self.device.name,
)
def set_hvac_mode(self, hvac_mode: str) -> None:
"""Set new target hvac mode."""
commands = [{"code": DPCode.SWITCH, "value": hvac_mode != HVAC_MODE_OFF}]
@ -436,8 +448,11 @@ class TuyaClimateEntity(TuyaEntity, ClimateEntity):
return self.entity_description.switch_only_hvac_mode
return HVAC_MODE_OFF
if self.device.status.get(DPCode.MODE) is not None:
return TUYA_HVAC_TO_HA[self.device.status[DPCode.MODE]]
if (
mode := self.device.status.get(DPCode.MODE)
) is not None and mode in TUYA_HVAC_TO_HA:
return TUYA_HVAC_TO_HA[mode]
return HVAC_MODE_OFF
@property

View File

@ -4,6 +4,7 @@ from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass, field
from enum import Enum
import logging
from tuya_iot import TuyaCloudOpenAPIEndpoint
@ -38,6 +39,7 @@ from homeassistant.const import (
)
DOMAIN = "tuya"
LOGGER = logging.getLogger(__package__)
CONF_AUTH_TYPE = "auth_type"
CONF_PROJECT_TYPE = "tuya_project_type"

View File

@ -160,9 +160,9 @@ class TuyaFanEntity(TuyaEntity, FanEntity):
return self.ha_preset_modes
@property
def preset_mode(self) -> str:
def preset_mode(self) -> str | None:
"""Return the current preset_mode."""
return self.device.status[DPCode.MODE]
return self.device.status.get(DPCode.MODE)
@property
def percentage(self) -> int | None:

View File

@ -405,7 +405,7 @@ class TuyaLightEntity(TuyaEntity, LightEntity):
if self._brightness_dpcode:
self._attr_supported_color_modes.add(COLOR_MODE_BRIGHTNESS)
self._brightness_type = IntegerTypeData.from_json(
device.status_range[self._brightness_dpcode].values
device.function[self._brightness_dpcode].values
)
# Check if min/max capable
@ -416,17 +416,17 @@ class TuyaLightEntity(TuyaEntity, LightEntity):
and description.brightness_min in device.function
):
self._brightness_max_type = IntegerTypeData.from_json(
device.status_range[description.brightness_max].values
device.function[description.brightness_max].values
)
self._brightness_min_type = IntegerTypeData.from_json(
device.status_range[description.brightness_min].values
device.function[description.brightness_min].values
)
# Update internals based on found color temperature dpcode
if self._color_temp_dpcode:
self._attr_supported_color_modes.add(COLOR_MODE_COLOR_TEMP)
self._color_temp_type = IntegerTypeData.from_json(
device.status_range[self._color_temp_dpcode].values
device.function[self._color_temp_dpcode].values
)
# Update internals based on found color data dpcode

View File

@ -727,15 +727,15 @@ class TuyaSensorEntity(TuyaEntity, SensorEntity):
# We cannot have a device class, if the UOM isn't set or the
# device class cannot be found in the validation mapping.
if (
self.unit_of_measurement is None
self.native_unit_of_measurement is None
or self.device_class not in DEVICE_CLASS_UNITS
):
self._attr_device_class = None
return
uoms = DEVICE_CLASS_UNITS[self.device_class]
self._uom = uoms.get(self.unit_of_measurement) or uoms.get(
self.unit_of_measurement.lower()
self._uom = uoms.get(self.native_unit_of_measurement) or uoms.get(
self.native_unit_of_measurement.lower()
)
# Unknown unit of measurement, device class should not be used.

View File

@ -7,7 +7,7 @@ from homeassistant.backports.enum import StrEnum
MAJOR_VERSION: Final = 2021
MINOR_VERSION: Final = 12
PATCH_VERSION: Final = "6"
PATCH_VERSION: Final = "7"
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 8, 0)

View File

@ -16,7 +16,7 @@ ciso8601==2.2.0
cryptography==35.0.0
emoji==1.5.0
hass-nabucasa==0.50.0
home-assistant-frontend==20211227.0
home-assistant-frontend==20211229.0
httpx==0.21.0
ifaddr==0.1.7
jinja2==3.0.3

View File

@ -187,7 +187,7 @@ aiohomekit==0.6.4
aiohttp_cors==0.7.0
# homeassistant.components.hue
aiohue==3.0.10
aiohue==3.0.11
# homeassistant.components.imap
aioimaplib==0.9.0
@ -659,7 +659,7 @@ fjaraskupan==1.0.2
flipr-api==1.4.1
# homeassistant.components.flux_led
flux_led==0.27.13
flux_led==0.27.21
# homeassistant.components.homekit
fnvhash==0.1.0
@ -820,7 +820,7 @@ hole==0.7.0
holidays==0.11.3.1
# homeassistant.components.frontend
home-assistant-frontend==20211227.0
home-assistant-frontend==20211229.0
# homeassistant.components.zwave
homeassistant-pyozw==0.1.10
@ -1929,7 +1929,7 @@ python-qbittorrent==0.4.2
python-ripple-api==0.0.3
# homeassistant.components.smarttub
python-smarttub==0.0.28
python-smarttub==0.0.29
# homeassistant.components.sochain
python-sochain-api==0.0.2

View File

@ -131,7 +131,7 @@ aiohomekit==0.6.4
aiohttp_cors==0.7.0
# homeassistant.components.hue
aiohue==3.0.10
aiohue==3.0.11
# homeassistant.components.apache_kafka
aiokafka==0.6.0
@ -399,7 +399,7 @@ fjaraskupan==1.0.2
flipr-api==1.4.1
# homeassistant.components.flux_led
flux_led==0.27.13
flux_led==0.27.21
# homeassistant.components.homekit
fnvhash==0.1.0
@ -515,7 +515,7 @@ hole==0.7.0
holidays==0.11.3.1
# homeassistant.components.frontend
home-assistant-frontend==20211227.0
home-assistant-frontend==20211229.0
# homeassistant.components.zwave
homeassistant-pyozw==0.1.10
@ -1157,7 +1157,7 @@ python-openzwave-mqtt[mqtt-client]==1.4.0
python-picnic-api==1.1.0
# homeassistant.components.smarttub
python-smarttub==0.0.28
python-smarttub==0.0.29
# homeassistant.components.songpal
python-songpal==0.12

View File

@ -121,7 +121,7 @@ async def test_light_turn_on_service(hass, mock_bridge_v2, v2_resources_test_dat
assert mock_bridge_v2.mock_requests[1]["json"]["on"]["on"] is True
assert mock_bridge_v2.mock_requests[1]["json"]["dynamics"]["duration"] == 200
# test again with sending flash/alert
# test again with sending long flash
await hass.services.async_call(
"light",
"turn_on",
@ -129,9 +129,37 @@ async def test_light_turn_on_service(hass, mock_bridge_v2, v2_resources_test_dat
blocking=True,
)
assert len(mock_bridge_v2.mock_requests) == 3
assert mock_bridge_v2.mock_requests[2]["json"]["on"]["on"] is True
assert mock_bridge_v2.mock_requests[2]["json"]["alert"]["action"] == "breathe"
# test again with sending short flash
await hass.services.async_call(
"light",
"turn_on",
{"entity_id": test_light_id, "flash": "short"},
blocking=True,
)
assert len(mock_bridge_v2.mock_requests) == 4
assert mock_bridge_v2.mock_requests[3]["json"]["identify"]["action"] == "identify"
# test again with sending a colortemperature which is out of range
# which should be normalized to the upper/lower bounds Hue can handle
await hass.services.async_call(
"light",
"turn_on",
{"entity_id": test_light_id, "color_temp": 50},
blocking=True,
)
assert len(mock_bridge_v2.mock_requests) == 5
assert mock_bridge_v2.mock_requests[4]["json"]["color_temperature"]["mirek"] == 153
await hass.services.async_call(
"light",
"turn_on",
{"entity_id": test_light_id, "color_temp": 550},
blocking=True,
)
assert len(mock_bridge_v2.mock_requests) == 6
assert mock_bridge_v2.mock_requests[5]["json"]["color_temperature"]["mirek"] == 500
async def test_light_turn_off_service(hass, mock_bridge_v2, v2_resources_test_data):
"""Test calling the turn off service on a light."""
@ -177,6 +205,26 @@ async def test_light_turn_off_service(hass, mock_bridge_v2, v2_resources_test_da
assert mock_bridge_v2.mock_requests[1]["json"]["on"]["on"] is False
assert mock_bridge_v2.mock_requests[1]["json"]["dynamics"]["duration"] == 200
# test again with sending long flash
await hass.services.async_call(
"light",
"turn_off",
{"entity_id": test_light_id, "flash": "long"},
blocking=True,
)
assert len(mock_bridge_v2.mock_requests) == 3
assert mock_bridge_v2.mock_requests[2]["json"]["alert"]["action"] == "breathe"
# test again with sending short flash
await hass.services.async_call(
"light",
"turn_off",
{"entity_id": test_light_id, "flash": "short"},
blocking=True,
)
assert len(mock_bridge_v2.mock_requests) == 4
assert mock_bridge_v2.mock_requests[3]["json"]["identify"]["action"] == "identify"
async def test_light_added(hass, mock_bridge_v2):
"""Test new light added to bridge."""
@ -386,3 +434,65 @@ async def test_grouped_lights(hass, mock_bridge_v2, v2_resources_test_data):
assert (
mock_bridge_v2.mock_requests[index]["json"]["dynamics"]["duration"] == 200
)
# Test sending short flash effect to a grouped light
mock_bridge_v2.mock_requests.clear()
test_light_id = "light.test_zone"
await hass.services.async_call(
"light",
"turn_on",
{
"entity_id": test_light_id,
"flash": "short",
},
blocking=True,
)
# PUT request should have been sent to ALL group lights with correct params
assert len(mock_bridge_v2.mock_requests) == 3
for index in range(0, 3):
assert (
mock_bridge_v2.mock_requests[index]["json"]["identify"]["action"]
== "identify"
)
# Test sending long flash effect to a grouped light
mock_bridge_v2.mock_requests.clear()
test_light_id = "light.test_zone"
await hass.services.async_call(
"light",
"turn_on",
{
"entity_id": test_light_id,
"flash": "long",
},
blocking=True,
)
# PUT request should have been sent to ALL group lights with correct params
assert len(mock_bridge_v2.mock_requests) == 3
for index in range(0, 3):
assert (
mock_bridge_v2.mock_requests[index]["json"]["alert"]["action"] == "breathe"
)
# Test sending flash effect in turn_off call
mock_bridge_v2.mock_requests.clear()
test_light_id = "light.test_zone"
await hass.services.async_call(
"light",
"turn_off",
{
"entity_id": test_light_id,
"flash": "short",
},
blocking=True,
)
# PUT request should have been sent to ALL group lights with correct params
assert len(mock_bridge_v2.mock_requests) == 3
for index in range(0, 3):
assert (
mock_bridge_v2.mock_requests[index]["json"]["identify"]["action"]
== "identify"
)

View File

@ -7,6 +7,7 @@ HOST = "1.1.1.1"
MAC = "01:23:45:67:89:ab"
HW_ID = 123456789
ID_HEX = "75BCD15"
MOCK_INFO = {"ids": {"hardwareId": HW_ID}}
@ -16,7 +17,7 @@ async def setup_nuki_integration(hass):
entry = MockConfigEntry(
domain="nuki",
unique_id=HW_ID,
unique_id=ID_HEX,
data={"host": HOST, "port": 8080, "token": "test-token"},
)
entry.add_to_hass(hass)

View File

@ -41,7 +41,7 @@ async def test_form(hass):
await hass.async_block_till_done()
assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result2["title"] == 123456789
assert result2["title"] == "75BCD15"
assert result2["data"] == {
"host": "1.1.1.1",
"port": 8080,
@ -69,7 +69,7 @@ async def test_import(hass):
data={"host": "1.1.1.1", "port": 8080, "token": "test-token"},
)
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result["title"] == 123456789
assert result["title"] == "75BCD15"
assert result["data"] == {
"host": "1.1.1.1",
"port": 8080,
@ -204,7 +204,7 @@ async def test_dhcp_flow(hass):
)
assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result2["title"] == 123456789
assert result2["title"] == "75BCD15"
assert result2["data"] == {
"host": "1.1.1.1",
"port": 8080,