diff --git a/homeassistant/components/flux_led/manifest.json b/homeassistant/components/flux_led/manifest.json index 9b55c9f4549..9f1eaa5d4a4 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 a2c7e49f6e6..c1d833ac169 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 7a69637fbb1..832592f3f1b 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 add3336764d..7ef91f684fe 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 @@ 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: diff --git a/homeassistant/components/hue/v2/helpers.py b/homeassistant/components/hue/v2/helpers.py index 307e7c55e03..97fdbe6160a 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 496507aff4d..1d45293012c 100644 --- a/homeassistant/components/hue/v2/hue_event.py +++ b/homeassistant/components/hue/v2/hue_event.py @@ -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) diff --git a/homeassistant/components/hue/v2/light.py b/homeassistant/components/hue/v2/light.py index d6578c8ef9a..42444fd9ad0 100644 --- a/homeassistant/components/hue/v2/light.py +++ b/homeassistant/components/hue/v2/light.py @@ -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, + ) diff --git a/homeassistant/components/nuki/__init__.py b/homeassistant/components/nuki/__init__.py index 99def8d4117..09f36661c5d 100644 --- a/homeassistant/components/nuki/__init__.py +++ b/homeassistant/components/nuki/__init__.py @@ -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, diff --git a/homeassistant/components/nuki/config_flow.py b/homeassistant/components/nuki/config_flow.py index bd2c5a0d750..54796d53890 100644 --- a/homeassistant/components/nuki/config_flow.py +++ b/homeassistant/components/nuki/config_flow.py @@ -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( diff --git a/homeassistant/components/nuki/helpers.py b/homeassistant/components/nuki/helpers.py new file mode 100644 index 00000000000..3deedf9d8db --- /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 5972d755e11..7d9a963b26c 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 ffc0d08a1b4..385af8224a6 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -482,7 +482,7 @@ class SonosSpeaker: 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 ad98523258a..ce3130ceaf2 100644 --- a/homeassistant/components/sonos/switch.py +++ b/homeassistant/components/sonos/switch.py @@ -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 + ) diff --git a/homeassistant/components/tuya/climate.py b/homeassistant/components/tuya/climate.py index cb70fc5515a..35e6f5814f3 100644 --- a/homeassistant/components/tuya/climate.py +++ b/homeassistant/components/tuya/climate.py @@ -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 diff --git a/homeassistant/components/tuya/const.py b/homeassistant/components/tuya/const.py index fa69b76695c..02f8a8d356d 100644 --- a/homeassistant/components/tuya/const.py +++ b/homeassistant/components/tuya/const.py @@ -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" diff --git a/homeassistant/components/tuya/fan.py b/homeassistant/components/tuya/fan.py index 99d28f1b998..4ebed3cf9e0 100644 --- a/homeassistant/components/tuya/fan.py +++ b/homeassistant/components/tuya/fan.py @@ -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: diff --git a/homeassistant/components/tuya/light.py b/homeassistant/components/tuya/light.py index 16eda1cc324..0669dee86c4 100644 --- a/homeassistant/components/tuya/light.py +++ b/homeassistant/components/tuya/light.py @@ -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 diff --git a/homeassistant/components/tuya/sensor.py b/homeassistant/components/tuya/sensor.py index d6870d4b9bb..07393b636e8 100644 --- a/homeassistant/components/tuya/sensor.py +++ b/homeassistant/components/tuya/sensor.py @@ -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. diff --git a/homeassistant/const.py b/homeassistant/const.py index f20d78f9224..f9131e0a4f3 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -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) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 19b764ee32f..e90fdf9db12 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 41318a26a28..47d2b09b186 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 f2e191885c9..003ece83507 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 f9277fad528..8b811ffe7c6 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 30315915a73..e85d1de3933 100644 --- a/tests/components/nuki/mock.py +++ b/tests/components/nuki/mock.py @@ -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) diff --git a/tests/components/nuki/test_config_flow.py b/tests/components/nuki/test_config_flow.py index fd7bfa2137b..634902e054e 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,