diff --git a/homeassistant/components/flux_led/manifest.json b/homeassistant/components/flux_led/manifest.json index 9b55c9f4549d03..9f1eaa5d4a4cdc 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -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", diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index a2c7e49f6e64a1..c1d833ac169eb9 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -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", diff --git a/homeassistant/components/hue/manifest.json b/homeassistant/components/hue/manifest.json index 7a69637fbb16ae..832592f3f1bf56 100644 --- a/homeassistant/components/hue/manifest.json +++ b/homeassistant/components/hue/manifest.json @@ -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", diff --git a/homeassistant/components/hue/v2/group.py b/homeassistant/components/hue/v2/group.py index add3336764d16a..7ef91f684fe6d9 100644 --- a/homeassistant/components/hue/v2/group.py +++ b/homeassistant/components/hue/v2/group.py @@ -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 @@ COLOR_MODE_COLOR_TEMP, COLOR_MODE_ONOFF, COLOR_MODE_XY, + FLASH_SHORT, SUPPORT_FLASH, SUPPORT_TRANSITION, LightEntity, @@ -29,14 +31,17 @@ 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 @@ async def async_turn_on(self, **kwargs: Any) -> None: """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 @@ async def async_turn_on(self, **kwargs: Any) -> None: # 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 @@ async def async_turn_off(self, **kwargs: Any) -> None: 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: diff --git a/homeassistant/components/hue/v2/helpers.py b/homeassistant/components/hue/v2/helpers.py index 307e7c55e034c0..97fdbe6160aa38 100644 --- a/homeassistant/components/hue/v2/helpers.py +++ b/homeassistant/components/hue/v2/helpers.py @@ -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 diff --git a/homeassistant/components/hue/v2/hue_event.py b/homeassistant/components/hue/v2/hue_event.py index 496507aff4d12e..1d45293012cfd5 100644 --- a/homeassistant/components/hue/v2/hue_event.py +++ b/homeassistant/components/hue/v2/hue_event.py @@ -4,7 +4,7 @@ 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 @@ def handle_button_event(evt_type: EventType, hue_resource: Button) -> None: 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) diff --git a/homeassistant/components/hue/v2/light.py b/homeassistant/components/hue/v2/light.py index d6578c8ef9a03d..42444fd9ad07d0 100644 --- a/homeassistant/components/hue/v2/light.py +++ b/homeassistant/components/hue/v2/light.py @@ -6,7 +6,6 @@ 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 @@ COLOR_MODE_COLOR_TEMP, COLOR_MODE_ONOFF, COLOR_MODE_XY, + FLASH_SHORT, SUPPORT_FLASH, SUPPORT_TRANSITION, LightEntity, @@ -30,12 +30,15 @@ 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 @@ def __init__( ) -> 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 @@ async def async_turn_on(self, **kwargs: Any) -> None: """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 @@ async def async_turn_on(self, **kwargs: Any) -> None: 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 @@ async def async_turn_off(self, **kwargs: Any) -> None: 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, + ) diff --git a/homeassistant/components/nuki/__init__.py b/homeassistant/components/nuki/__init__.py index 99def8d41178c3..09f36661c5d27f 100644 --- a/homeassistant/components/nuki/__init__.py +++ b/homeassistant/components/nuki/__init__.py @@ -27,6 +27,7 @@ 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, diff --git a/homeassistant/components/nuki/config_flow.py b/homeassistant/components/nuki/config_flow.py index bd2c5a0d750ca7..54796d538901d4 100644 --- a/homeassistant/components/nuki/config_flow.py +++ b/homeassistant/components/nuki/config_flow.py @@ -12,6 +12,7 @@ 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 @@ async def async_step_user(self, user_input=None): 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 @@ async def async_step_reauth_confirm(self, user_input=None): 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 @@ async def async_step_validate(self, user_input=None): 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( diff --git a/homeassistant/components/nuki/helpers.py b/homeassistant/components/nuki/helpers.py new file mode 100644 index 00000000000000..3deedf9d8dbe6a --- /dev/null +++ b/homeassistant/components/nuki/helpers.py @@ -0,0 +1,6 @@ +"""nuki integration helpers.""" + + +def parse_id(hardware_id): + """Parse Nuki ID.""" + return hex(hardware_id).split("x")[-1].upper() diff --git a/homeassistant/components/smarttub/manifest.json b/homeassistant/components/smarttub/manifest.json index 5972d755e11baf..7d9a963b26c8c4 100644 --- a/homeassistant/components/smarttub/manifest.json +++ b/homeassistant/components/smarttub/manifest.json @@ -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" } diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index ffc0d08a1b430e..385af8224a6ddf 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -482,7 +482,7 @@ def async_update_volume(self, event: SonosEvent) -> None: for bool_var in ( "dialog_level", - "night_level", + "night_mode", "sub_enabled", "surround_enabled", ): diff --git a/homeassistant/components/sonos/switch.py b/homeassistant/components/sonos/switch.py index ad98523258ae36..ce3130ceaf27c9 100644 --- a/homeassistant/components/sonos/switch.py +++ b/homeassistant/components/sonos/switch.py @@ -7,8 +7,9 @@ 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_create_switches(speaker: SonosSpeaker) -> None: 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 @@ async def async_handle_switch_on_off(self, turn_on: bool) -> None: 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 + ) diff --git a/homeassistant/components/tuya/climate.py b/homeassistant/components/tuya/climate.py index cb70fc5515aafb..35e6f5814f3c8d 100644 --- a/homeassistant/components/tuya/climate.py +++ b/homeassistant/components/tuya/climate.py @@ -32,7 +32,7 @@ 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 @@ def __init__( # noqa: C901 # 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 @@ def __init__( # noqa: C901 ] # 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 @@ def __init__( # noqa: C901 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 @@ def hvac_mode(self) -> str: 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 diff --git a/homeassistant/components/tuya/const.py b/homeassistant/components/tuya/const.py index fa69b76695c321..02f8a8d356db57 100644 --- a/homeassistant/components/tuya/const.py +++ b/homeassistant/components/tuya/const.py @@ -4,6 +4,7 @@ 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 @@ ) DOMAIN = "tuya" +LOGGER = logging.getLogger(__package__) CONF_AUTH_TYPE = "auth_type" CONF_PROJECT_TYPE = "tuya_project_type" diff --git a/homeassistant/components/tuya/fan.py b/homeassistant/components/tuya/fan.py index 99d28f1b998fd0..4ebed3cf9e0b42 100644 --- a/homeassistant/components/tuya/fan.py +++ b/homeassistant/components/tuya/fan.py @@ -160,9 +160,9 @@ def preset_modes(self) -> list[str]: 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: diff --git a/homeassistant/components/tuya/light.py b/homeassistant/components/tuya/light.py index 16eda1cc32409b..0669dee86c457e 100644 --- a/homeassistant/components/tuya/light.py +++ b/homeassistant/components/tuya/light.py @@ -405,7 +405,7 @@ def __init__( 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 @@ def __init__( 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 diff --git a/homeassistant/components/tuya/sensor.py b/homeassistant/components/tuya/sensor.py index d6870d4b9bb6f6..07393b636e8d1d 100644 --- a/homeassistant/components/tuya/sensor.py +++ b/homeassistant/components/tuya/sensor.py @@ -727,15 +727,15 @@ def __init__( # 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. diff --git a/homeassistant/const.py b/homeassistant/const.py index f20d78f922406c..f9131e0a4f3c7c 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ 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) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 19b764ee32f076..e90fdf9db126bc 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -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 diff --git a/requirements_all.txt b/requirements_all.txt index 41318a26a287bc..47d2b09b186bb8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -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 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f2e191885c997e..003ece83507c6f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -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 diff --git a/tests/components/hue/test_light_v2.py b/tests/components/hue/test_light_v2.py index f9277fad528dcb..8b811ffe7c64f3 100644 --- a/tests/components/hue/test_light_v2.py +++ b/tests/components/hue/test_light_v2.py @@ -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" + ) diff --git a/tests/components/nuki/mock.py b/tests/components/nuki/mock.py index 30315915a73e6a..e85d1de3933aaa 100644 --- a/tests/components/nuki/mock.py +++ b/tests/components/nuki/mock.py @@ -7,6 +7,7 @@ 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) diff --git a/tests/components/nuki/test_config_flow.py b/tests/components/nuki/test_config_flow.py index fd7bfa2137b13c..634902e054ecba 100644 --- a/tests/components/nuki/test_config_flow.py +++ b/tests/components/nuki/test_config_flow.py @@ -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,