diff --git a/homeassistant/components/broadlink/sensor.py b/homeassistant/components/broadlink/sensor.py index 0e42d8c438f..3f4a1e861b3 100644 --- a/homeassistant/components/broadlink/sensor.py +++ b/homeassistant/components/broadlink/sensor.py @@ -51,7 +51,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): sensors = [ BroadlinkSensor(device, monitored_condition) for monitored_condition in sensor_data - if sensor_data[monitored_condition] or device.api.type == "A1" + if sensor_data[monitored_condition] != 0 or device.api.type == "A1" ] async_add_entities(sensors) diff --git a/homeassistant/components/broadlink/updater.py b/homeassistant/components/broadlink/updater.py index 8401dba8c0d..a84eec07d68 100644 --- a/homeassistant/components/broadlink/updater.py +++ b/homeassistant/components/broadlink/updater.py @@ -117,11 +117,25 @@ class BroadlinkRMUpdateManager(BroadlinkUpdateManager): device = self.device if hasattr(device.api, "check_sensors"): - return await device.async_request(device.api.check_sensors) + data = await device.async_request(device.api.check_sensors) + return self.normalize(data, self.coordinator.data) await device.async_request(device.api.update) return {} + @staticmethod + def normalize(data, previous_data): + """Fix firmware issue. + + See https://github.com/home-assistant/core/issues/42100. + """ + if data["temperature"] == -7: + if previous_data is None or previous_data["temperature"] is None: + data["temperature"] = None + elif abs(previous_data["temperature"] - data["temperature"]) > 3: + data["temperature"] = previous_data["temperature"] + return data + class BroadlinkSP1UpdateManager(BroadlinkUpdateManager): """Manages updates for Broadlink SP1 devices.""" diff --git a/homeassistant/components/climacell/const.py b/homeassistant/components/climacell/const.py index 977a5089783..0352807138a 100644 --- a/homeassistant/components/climacell/const.py +++ b/homeassistant/components/climacell/const.py @@ -6,6 +6,7 @@ from pyclimacell.const import ( HealthConcernType, PollenIndex, PrimaryPollutantType, + V3PollenIndex, WeatherCode, ) @@ -307,8 +308,20 @@ CC_V3_SENSOR_TYPES = [ ATTR_FIELD: CC_V3_ATTR_CHINA_HEALTH_CONCERN, ATTR_NAME: "China MEP Health Concern", }, - {ATTR_FIELD: CC_V3_ATTR_POLLEN_TREE, ATTR_NAME: "Tree Pollen Index"}, - {ATTR_FIELD: CC_V3_ATTR_POLLEN_WEED, ATTR_NAME: "Weed Pollen Index"}, - {ATTR_FIELD: CC_V3_ATTR_POLLEN_GRASS, ATTR_NAME: "Grass Pollen Index"}, + { + ATTR_FIELD: CC_V3_ATTR_POLLEN_TREE, + ATTR_NAME: "Tree Pollen Index", + ATTR_VALUE_MAP: V3PollenIndex, + }, + { + ATTR_FIELD: CC_V3_ATTR_POLLEN_WEED, + ATTR_NAME: "Weed Pollen Index", + ATTR_VALUE_MAP: V3PollenIndex, + }, + { + ATTR_FIELD: CC_V3_ATTR_POLLEN_GRASS, + ATTR_NAME: "Grass Pollen Index", + ATTR_VALUE_MAP: V3PollenIndex, + }, {ATTR_FIELD: CC_V3_ATTR_FIRE_INDEX, ATTR_NAME: "Fire Index"}, ] diff --git a/homeassistant/components/climacell/manifest.json b/homeassistant/components/climacell/manifest.json index 89f6d7bf846..bb7dea841e4 100644 --- a/homeassistant/components/climacell/manifest.json +++ b/homeassistant/components/climacell/manifest.json @@ -3,7 +3,7 @@ "name": "ClimaCell", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/climacell", - "requirements": ["pyclimacell==0.18.0"], + "requirements": ["pyclimacell==0.18.2"], "codeowners": ["@raman325"], "iot_class": "cloud_polling" } diff --git a/homeassistant/components/climacell/sensor.py b/homeassistant/components/climacell/sensor.py index 50e051813c4..56812c9b2af 100644 --- a/homeassistant/components/climacell/sensor.py +++ b/homeassistant/components/climacell/sensor.py @@ -128,7 +128,7 @@ class BaseClimaCellSensorEntity(ClimaCellEntity, SensorEntity): ): return round(self._state * self.sensor_type[ATTR_METRIC_CONVERSION], 4) - if ATTR_VALUE_MAP in self.sensor_type: + if ATTR_VALUE_MAP in self.sensor_type and self._state is not None: return self.sensor_type[ATTR_VALUE_MAP](self._state).name.lower() return self._state diff --git a/homeassistant/components/demo/light.py b/homeassistant/components/demo/light.py index 3e949138b67..6680cd23874 100644 --- a/homeassistant/components/demo/light.py +++ b/homeassistant/components/demo/light.py @@ -1,4 +1,6 @@ """Demo light platform that implements lights.""" +from __future__ import annotations + import random from homeassistant.components.light import ( @@ -6,12 +8,13 @@ from homeassistant.components.light import ( ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_HS_COLOR, - ATTR_WHITE_VALUE, - SUPPORT_BRIGHTNESS, - SUPPORT_COLOR, - SUPPORT_COLOR_TEMP, + ATTR_RGBW_COLOR, + ATTR_RGBWW_COLOR, + COLOR_MODE_COLOR_TEMP, + COLOR_MODE_HS, + COLOR_MODE_RGBW, + COLOR_MODE_RGBWW, SUPPORT_EFFECT, - SUPPORT_WHITE_VALUE, LightEntity, ) @@ -23,9 +26,7 @@ LIGHT_EFFECT_LIST = ["rainbow", "none"] LIGHT_TEMPS = [240, 380] -SUPPORT_DEMO = ( - SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_COLOR | SUPPORT_WHITE_VALUE -) +SUPPORT_DEMO = {COLOR_MODE_HS, COLOR_MODE_COLOR_TEMP} async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): @@ -33,27 +34,43 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities( [ DemoLight( - unique_id="light_1", - name="Bed Light", - state=False, available=True, effect_list=LIGHT_EFFECT_LIST, effect=LIGHT_EFFECT_LIST[0], + name="Bed Light", + state=False, + unique_id="light_1", ), DemoLight( - unique_id="light_2", - name="Ceiling Lights", - state=True, available=True, ct=LIGHT_TEMPS[1], + name="Ceiling Lights", + state=True, + unique_id="light_2", ), DemoLight( - unique_id="light_3", - name="Kitchen Lights", - state=True, available=True, hs_color=LIGHT_COLORS[1], - ct=LIGHT_TEMPS[0], + name="Kitchen Lights", + state=True, + unique_id="light_3", + ), + DemoLight( + available=True, + ct=LIGHT_TEMPS[1], + name="Office RGBW Lights", + rgbw_color=(255, 0, 0, 255), + state=True, + supported_color_modes={COLOR_MODE_RGBW}, + unique_id="light_4", + ), + DemoLight( + available=True, + name="Living Room RGBWW Lights", + rgbww_color=(255, 0, 0, 255, 0), + state=True, + supported_color_modes={COLOR_MODE_RGBWW}, + unique_id="light_5", ), ] ) @@ -73,26 +90,39 @@ class DemoLight(LightEntity): name, state, available=False, - hs_color=None, - ct=None, brightness=180, - white=200, + ct=None, effect_list=None, effect=None, + hs_color=None, + rgbw_color=None, + rgbww_color=None, + supported_color_modes=None, ): """Initialize the light.""" - self._unique_id = unique_id - self._name = name - self._state = state - self._hs_color = hs_color - self._ct = ct or random.choice(LIGHT_TEMPS) - self._brightness = brightness - self._white = white - self._features = SUPPORT_DEMO - self._effect_list = effect_list - self._effect = effect self._available = True - self._color_mode = "ct" if ct is not None and hs_color is None else "hs" + self._brightness = brightness + self._ct = ct or random.choice(LIGHT_TEMPS) + self._effect = effect + self._effect_list = effect_list + self._features = 0 + self._hs_color = hs_color + self._name = name + self._rgbw_color = rgbw_color + self._rgbww_color = rgbww_color + self._state = state + self._unique_id = unique_id + if hs_color: + self._color_mode = COLOR_MODE_HS + elif rgbw_color: + self._color_mode = COLOR_MODE_RGBW + elif rgbww_color: + self._color_mode = COLOR_MODE_RGBWW + else: + self._color_mode = COLOR_MODE_COLOR_TEMP + if not supported_color_modes: + supported_color_modes = SUPPORT_DEMO + self._color_modes = supported_color_modes if self._effect_list is not None: self._features |= SUPPORT_EFFECT @@ -134,24 +164,30 @@ class DemoLight(LightEntity): """Return the brightness of this light between 0..255.""" return self._brightness + @property + def color_mode(self) -> str | None: + """Return the color mode of the light.""" + return self._color_mode + @property def hs_color(self) -> tuple: """Return the hs color value.""" - if self._color_mode == "hs": - return self._hs_color - return None + return self._hs_color + + @property + def rgbw_color(self) -> tuple: + """Return the rgbw color value.""" + return self._rgbw_color + + @property + def rgbww_color(self) -> tuple: + """Return the rgbww color value.""" + return self._rgbww_color @property def color_temp(self) -> int: """Return the CT color temperature.""" - if self._color_mode == "ct": - return self._ct - return None - - @property - def white_value(self) -> int: - """Return the white value of this light between 0..255.""" - return self._white + return self._ct @property def effect_list(self) -> list: @@ -173,24 +209,34 @@ class DemoLight(LightEntity): """Flag supported features.""" return self._features + @property + def supported_color_modes(self) -> set | None: + """Flag supported color modes.""" + return self._color_modes + async def async_turn_on(self, **kwargs) -> None: """Turn the light on.""" self._state = True + if ATTR_RGBW_COLOR in kwargs: + self._color_mode = COLOR_MODE_RGBW + self._rgbw_color = kwargs[ATTR_RGBW_COLOR] + + if ATTR_RGBWW_COLOR in kwargs: + self._color_mode = COLOR_MODE_RGBWW + self._rgbww_color = kwargs[ATTR_RGBWW_COLOR] + if ATTR_HS_COLOR in kwargs: - self._color_mode = "hs" + self._color_mode = COLOR_MODE_HS self._hs_color = kwargs[ATTR_HS_COLOR] if ATTR_COLOR_TEMP in kwargs: - self._color_mode = "ct" + self._color_mode = COLOR_MODE_COLOR_TEMP self._ct = kwargs[ATTR_COLOR_TEMP] if ATTR_BRIGHTNESS in kwargs: self._brightness = kwargs[ATTR_BRIGHTNESS] - if ATTR_WHITE_VALUE in kwargs: - self._white = kwargs[ATTR_WHITE_VALUE] - if ATTR_EFFECT in kwargs: self._effect = kwargs[ATTR_EFFECT] diff --git a/homeassistant/components/denonavr/__init__.py b/homeassistant/components/denonavr/__init__.py index 76baf73c3e5..818c005b1cd 100644 --- a/homeassistant/components/denonavr/__init__.py +++ b/homeassistant/components/denonavr/__init__.py @@ -42,7 +42,6 @@ async def async_setup_entry( entry.options.get(CONF_ZONE2, DEFAULT_ZONE2), entry.options.get(CONF_ZONE3, DEFAULT_ZONE3), lambda: get_async_client(hass), - entry.state, ) try: await connect_denonavr.async_connect_receiver() diff --git a/homeassistant/components/denonavr/manifest.json b/homeassistant/components/denonavr/manifest.json index b3f45330c94..123eac5d2bf 100644 --- a/homeassistant/components/denonavr/manifest.json +++ b/homeassistant/components/denonavr/manifest.json @@ -3,7 +3,7 @@ "name": "Denon AVR Network Receivers", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/denonavr", - "requirements": ["denonavr==0.10.5"], + "requirements": ["denonavr==0.10.6"], "codeowners": ["@scarface-4711", "@starkillerOG"], "ssdp": [ { diff --git a/homeassistant/components/denonavr/media_player.py b/homeassistant/components/denonavr/media_player.py index 14520f0ddaf..e745c89f5c8 100644 --- a/homeassistant/components/denonavr/media_player.py +++ b/homeassistant/components/denonavr/media_player.py @@ -246,7 +246,6 @@ class DenonDevice(MediaPlayerEntity): "manufacturer": self._config_entry.data[CONF_MANUFACTURER], "name": self._config_entry.title, "model": f"{self._config_entry.data[CONF_MODEL]}-{self._config_entry.data[CONF_TYPE]}", - "serial_number": self._config_entry.data[CONF_SERIAL_NUMBER], } return device_info diff --git a/homeassistant/components/denonavr/receiver.py b/homeassistant/components/denonavr/receiver.py index 8b50373799b..c5d4661b1a8 100644 --- a/homeassistant/components/denonavr/receiver.py +++ b/homeassistant/components/denonavr/receiver.py @@ -20,7 +20,6 @@ class ConnectDenonAVR: zone2: bool, zone3: bool, async_client_getter: Callable, - entry_state: str | None = None, ): """Initialize the class.""" self._async_client_getter = async_client_getter @@ -28,7 +27,6 @@ class ConnectDenonAVR: self._host = host self._show_all_inputs = show_all_inputs self._timeout = timeout - self._entry_state = entry_state self._zones = {} if zone2: diff --git a/homeassistant/components/group/light.py b/homeassistant/components/group/light.py index 26cc8e1c11c..84c218b5d72 100644 --- a/homeassistant/components/group/light.py +++ b/homeassistant/components/group/light.py @@ -12,6 +12,7 @@ import voluptuous as vol from homeassistant.components import light from homeassistant.components.light import ( ATTR_BRIGHTNESS, + ATTR_COLOR_MODE, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_EFFECT_LIST, @@ -19,16 +20,20 @@ from homeassistant.components.light import ( ATTR_HS_COLOR, ATTR_MAX_MIREDS, ATTR_MIN_MIREDS, + ATTR_RGB_COLOR, + ATTR_RGBW_COLOR, + ATTR_RGBWW_COLOR, + ATTR_SUPPORTED_COLOR_MODES, ATTR_TRANSITION, ATTR_WHITE_VALUE, + ATTR_XY_COLOR, PLATFORM_SCHEMA, - SUPPORT_BRIGHTNESS, - SUPPORT_COLOR, - SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, SUPPORT_FLASH, SUPPORT_TRANSITION, SUPPORT_WHITE_VALUE, + color_supported, + color_temp_supported, ) from homeassistant.const import ( ATTR_ENTITY_ID, @@ -59,13 +64,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) SUPPORT_GROUP_LIGHT = ( - SUPPORT_BRIGHTNESS - | SUPPORT_COLOR_TEMP - | SUPPORT_EFFECT - | SUPPORT_FLASH - | SUPPORT_COLOR - | SUPPORT_TRANSITION - | SUPPORT_WHITE_VALUE + SUPPORT_EFFECT | SUPPORT_FLASH | SUPPORT_TRANSITION | SUPPORT_WHITE_VALUE ) @@ -89,13 +88,19 @@ class LightGroup(GroupEntity, light.LightEntity): self._available = False self._icon = "mdi:lightbulb-group" self._brightness: int | None = None + self._color_mode: str | None = None self._hs_color: tuple[float, float] | None = None + self._rgb_color: tuple[int, int, int] | None = None + self._rgbw_color: tuple[int, int, int, int] | None = None + self._rgbww_color: tuple[int, int, int, int, int] | None = None + self._xy_color: tuple[float, float] | None = None self._color_temp: int | None = None self._min_mireds: int = 154 self._max_mireds: int = 500 self._white_value: int | None = None self._effect_list: list[str] | None = None self._effect: str | None = None + self._supported_color_modes: set[str] | None = None self._supported_features: int = 0 async def async_added_to_hass(self) -> None: @@ -143,11 +148,36 @@ class LightGroup(GroupEntity, light.LightEntity): """Return the brightness of this light group between 0..255.""" return self._brightness + @property + def color_mode(self) -> str | None: + """Return the color mode of the light.""" + return self._color_mode + @property def hs_color(self) -> tuple[float, float] | None: """Return the HS color value [float, float].""" return self._hs_color + @property + def rgb_color(self) -> tuple[int, int, int] | None: + """Return the rgb color value [int, int, int].""" + return self._rgb_color + + @property + def rgbw_color(self) -> tuple[int, int, int, int] | None: + """Return the rgbw color value [int, int, int, int].""" + return self._rgbw_color + + @property + def rgbww_color(self) -> tuple[int, int, int, int, int] | None: + """Return the rgbww color value [int, int, int, int, int].""" + return self._rgbww_color + + @property + def xy_color(self) -> tuple[float, float] | None: + """Return the xy color value [float, float].""" + return self._xy_color + @property def color_temp(self) -> int | None: """Return the CT color value in mireds.""" @@ -178,6 +208,11 @@ class LightGroup(GroupEntity, light.LightEntity): """Return the current effect.""" return self._effect + @property + def supported_color_modes(self) -> set | None: + """Flag supported color modes.""" + return self._supported_color_modes + @property def supported_features(self) -> int: """Flag supported features.""" @@ -204,6 +239,18 @@ class LightGroup(GroupEntity, light.LightEntity): if ATTR_HS_COLOR in kwargs: data[ATTR_HS_COLOR] = kwargs[ATTR_HS_COLOR] + if ATTR_RGB_COLOR in kwargs: + data[ATTR_RGB_COLOR] = kwargs[ATTR_RGB_COLOR] + + if ATTR_RGBW_COLOR in kwargs: + data[ATTR_RGBW_COLOR] = kwargs[ATTR_RGBW_COLOR] + + if ATTR_RGBWW_COLOR in kwargs: + data[ATTR_RGBWW_COLOR] = kwargs[ATTR_RGBWW_COLOR] + + if ATTR_XY_COLOR in kwargs: + data[ATTR_XY_COLOR] = kwargs[ATTR_XY_COLOR] + if ATTR_COLOR_TEMP in kwargs: data[ATTR_COLOR_TEMP] = kwargs[ATTR_COLOR_TEMP] @@ -215,11 +262,9 @@ class LightGroup(GroupEntity, light.LightEntity): state = self.hass.states.get(entity_id) if not state: continue - support = state.attributes.get(ATTR_SUPPORTED_FEATURES) + support = state.attributes.get(ATTR_SUPPORTED_COLOR_MODES) # Only pass color temperature to supported entity_ids - if bool(support & SUPPORT_COLOR) and not bool( - support & SUPPORT_COLOR_TEMP - ): + if color_supported(support) and not color_temp_supported(support): emulate_color_temp_entity_ids.append(entity_id) updated_entities.remove(entity_id) data[ATTR_ENTITY_ID] = updated_entities @@ -300,6 +345,16 @@ class LightGroup(GroupEntity, light.LightEntity): self._brightness = _reduce_attribute(on_states, ATTR_BRIGHTNESS) self._hs_color = _reduce_attribute(on_states, ATTR_HS_COLOR, reduce=_mean_tuple) + self._rgb_color = _reduce_attribute( + on_states, ATTR_RGB_COLOR, reduce=_mean_tuple + ) + self._rgbw_color = _reduce_attribute( + on_states, ATTR_RGBW_COLOR, reduce=_mean_tuple + ) + self._rgbww_color = _reduce_attribute( + on_states, ATTR_RGBWW_COLOR, reduce=_mean_tuple + ) + self._xy_color = _reduce_attribute(on_states, ATTR_XY_COLOR, reduce=_mean_tuple) self._white_value = _reduce_attribute(on_states, ATTR_WHITE_VALUE) @@ -324,6 +379,21 @@ class LightGroup(GroupEntity, light.LightEntity): effects_count = Counter(itertools.chain(all_effects)) self._effect = effects_count.most_common(1)[0][0] + self._color_mode = None + all_color_modes = list(_find_state_attributes(on_states, ATTR_COLOR_MODE)) + if all_color_modes: + # Report the most common color mode. + color_mode_count = Counter(itertools.chain(all_color_modes)) + self._color_mode = color_mode_count.most_common(1)[0][0] + + self._supported_color_modes = None + all_supported_color_modes = list( + _find_state_attributes(states, ATTR_SUPPORTED_COLOR_MODES) + ) + if all_supported_color_modes: + # Merge all color modes. + self._supported_color_modes = set().union(*all_supported_color_modes) + self._supported_features = 0 for support in _find_state_attributes(states, ATTR_SUPPORTED_FEATURES): # Merge supported features by emulating support for every feature diff --git a/homeassistant/components/group/services.yaml b/homeassistant/components/group/services.yaml index aac3e9aad59..f1e64a60022 100644 --- a/homeassistant/components/group/services.yaml +++ b/homeassistant/components/group/services.yaml @@ -34,7 +34,7 @@ set: object: add_entities: name: Add Entities - description: List of members they will change on group listening. + description: List of members that will change on group listening. example: domain.entity_id1, domain.entity_id2 selector: object: @@ -55,5 +55,4 @@ remove: required: true example: "test_group" selector: - entity: - domain: group + object: diff --git a/homeassistant/components/hue/binary_sensor.py b/homeassistant/components/hue/binary_sensor.py index e408e995ad4..d5c6953700d 100644 --- a/homeassistant/components/hue/binary_sensor.py +++ b/homeassistant/components/hue/binary_sensor.py @@ -1,5 +1,4 @@ """Hue binary sensor entities.""" - from aiohue.sensors import TYPE_ZLL_PRESENCE from homeassistant.components.binary_sensor import ( @@ -15,9 +14,14 @@ PRESENCE_NAME_FORMAT = "{} motion" async def async_setup_entry(hass, config_entry, async_add_entities): """Defer binary sensor setup to the shared sensor module.""" - await hass.data[HUE_DOMAIN][ - config_entry.entry_id - ].sensor_manager.async_register_component("binary_sensor", async_add_entities) + bridge = hass.data[HUE_DOMAIN][config_entry.entry_id] + + if not bridge.sensor_manager: + return + + await bridge.sensor_manager.async_register_component( + "binary_sensor", async_add_entities + ) class HuePresence(GenericZLLSensor, BinarySensorEntity): diff --git a/homeassistant/components/hue/bridge.py b/homeassistant/components/hue/bridge.py index 698ad9e18e3..776ebbeb1f6 100644 --- a/homeassistant/components/hue/bridge.py +++ b/homeassistant/components/hue/bridge.py @@ -102,7 +102,8 @@ class HueBridge: return False self.api = bridge - self.sensor_manager = SensorManager(self) + if bridge.sensors is not None: + self.sensor_manager = SensorManager(self) hass.config_entries.async_setup_platforms(self.config_entry, PLATFORMS) @@ -178,6 +179,10 @@ class HueBridge: async def hue_activate_scene(self, data, skip_reload=False, hide_warnings=False): """Service to call directly into bridge to set scenes.""" + if self.api.scenes is None: + _LOGGER.warning("Hub %s does not support scenes", self.api.host) + return + group_name = data[ATTR_GROUP_NAME] scene_name = data[ATTR_SCENE_NAME] transition = data.get(ATTR_TRANSITION) diff --git a/homeassistant/components/hue/manifest.json b/homeassistant/components/hue/manifest.json index b86bcd61790..de00d31f2c7 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==2.1.0"], + "requirements": ["aiohue==2.3.0"], "ssdp": [ { "manufacturer": "Royal Philips Electronics", diff --git a/homeassistant/components/hue/sensor.py b/homeassistant/components/hue/sensor.py index 6ac8d134327..3cd3b002f98 100644 --- a/homeassistant/components/hue/sensor.py +++ b/homeassistant/components/hue/sensor.py @@ -26,9 +26,12 @@ TEMPERATURE_NAME_FORMAT = "{} temperature" async def async_setup_entry(hass, config_entry, async_add_entities): """Defer sensor setup to the shared sensor module.""" - await hass.data[HUE_DOMAIN][ - config_entry.entry_id - ].sensor_manager.async_register_component("sensor", async_add_entities) + bridge = hass.data[HUE_DOMAIN][config_entry.entry_id] + + if not bridge.sensor_manager: + return + + await bridge.sensor_manager.async_register_component("sensor", async_add_entities) class GenericHueGaugeSensorEntity(GenericZLLSensor, SensorEntity): diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index 97aa2468145..0bc68702467 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -343,7 +343,7 @@ async def async_setup(hass, config): # noqa: C901 rgb_color = params.pop(ATTR_RGB_COLOR) if COLOR_MODE_RGBW in supported_color_modes: params[ATTR_RGBW_COLOR] = color_util.color_rgb_to_rgbw(*rgb_color) - if COLOR_MODE_RGBWW in supported_color_modes: + elif COLOR_MODE_RGBWW in supported_color_modes: params[ATTR_RGBWW_COLOR] = color_util.color_rgb_to_rgbww( *rgb_color, light.min_mireds, light.max_mireds ) diff --git a/homeassistant/components/mqtt/fan.py b/homeassistant/components/mqtt/fan.py index bdbe3412539..b4718499d64 100644 --- a/homeassistant/components/mqtt/fan.py +++ b/homeassistant/components/mqtt/fan.py @@ -344,6 +344,9 @@ class MqttFan(MqttEntity, FanEntity): def state_received(msg): """Handle new received MQTT message.""" payload = self._value_templates[CONF_STATE](msg.payload) + if not payload: + _LOGGER.debug("Ignoring empty state from '%s'", msg.topic) + return if payload == self._payload["STATE_ON"]: self._state = True elif payload == self._payload["STATE_OFF"]: @@ -362,22 +365,27 @@ class MqttFan(MqttEntity, FanEntity): def percentage_received(msg): """Handle new received MQTT message for the percentage.""" numeric_val_str = self._value_templates[ATTR_PERCENTAGE](msg.payload) + if not numeric_val_str: + _LOGGER.debug("Ignoring empty speed from '%s'", msg.topic) + return try: percentage = ranged_value_to_percentage( self._speed_range, int(numeric_val_str) ) except ValueError: _LOGGER.warning( - "'%s' received on topic %s is not a valid speed within the speed range", + "'%s' received on topic %s. '%s' is not a valid speed within the speed range", msg.payload, msg.topic, + numeric_val_str, ) return if percentage < 0 or percentage > 100: _LOGGER.warning( - "'%s' received on topic %s is not a valid speed within the speed range", + "'%s' received on topic %s. '%s' is not a valid speed within the speed range", msg.payload, msg.topic, + numeric_val_str, ) return self._percentage = percentage @@ -396,11 +404,15 @@ class MqttFan(MqttEntity, FanEntity): def preset_mode_received(msg): """Handle new received MQTT message for preset mode.""" preset_mode = self._value_templates[ATTR_PRESET_MODE](msg.payload) + if not preset_mode: + _LOGGER.debug("Ignoring empty preset_mode from '%s'", msg.topic) + return if preset_mode not in self.preset_modes: _LOGGER.warning( - "'%s' received on topic %s is not a valid preset mode", + "'%s' received on topic %s. '%s' is not a valid preset mode", msg.payload, msg.topic, + preset_mode, ) return @@ -436,9 +448,10 @@ class MqttFan(MqttEntity, FanEntity): self._speed = speed else: _LOGGER.warning( - "'%s' received on topic %s is not a valid speed", + "'%s' received on topic %s. '%s' is not a valid speed", msg.payload, msg.topic, + speed, ) return @@ -464,6 +477,9 @@ class MqttFan(MqttEntity, FanEntity): def oscillation_received(msg): """Handle new received MQTT message for the oscillation.""" payload = self._value_templates[ATTR_OSCILLATING](msg.payload) + if not payload: + _LOGGER.debug("Ignoring empty oscillation from '%s'", msg.topic) + return if payload == self._payload["OSCILLATE_ON_PAYLOAD"]: self._oscillation = True elif payload == self._payload["OSCILLATE_OFF_PAYLOAD"]: diff --git a/homeassistant/components/mysensors/__init__.py b/homeassistant/components/mysensors/__init__.py index 812e6bf1670..9d23cfd24b6 100644 --- a/homeassistant/components/mysensors/__init__.py +++ b/homeassistant/components/mysensors/__init__.py @@ -58,18 +58,23 @@ DEFAULT_TCP_PORT = 5003 DEFAULT_VERSION = "1.4" +def set_default_persistence_file(value: dict) -> dict: + """Set default persistence file.""" + for idx, gateway in enumerate(value): + fil = gateway.get(CONF_PERSISTENCE_FILE) + if fil is not None: + continue + new_name = f"mysensors{idx + 1}.pickle" + gateway[CONF_PERSISTENCE_FILE] = new_name + + return value + + def has_all_unique_files(value): """Validate that all persistence files are unique and set if any is set.""" - persistence_files = [gateway.get(CONF_PERSISTENCE_FILE) for gateway in value] - if None in persistence_files and any( - name is not None for name in persistence_files - ): - raise vol.Invalid( - "persistence file name of all devices must be set if any is set" - ) - if not all(name is None for name in persistence_files): - schema = vol.Schema(vol.Unique()) - schema(persistence_files) + persistence_files = [gateway[CONF_PERSISTENCE_FILE] for gateway in value] + schema = vol.Schema(vol.Unique()) + schema(persistence_files) return value @@ -128,7 +133,10 @@ CONFIG_SCHEMA = vol.Schema( deprecated(CONF_PERSISTENCE), { vol.Required(CONF_GATEWAYS): vol.All( - cv.ensure_list, has_all_unique_files, [GATEWAY_SCHEMA] + cv.ensure_list, + set_default_persistence_file, + has_all_unique_files, + [GATEWAY_SCHEMA], ), vol.Optional(CONF_RETAIN, default=True): cv.boolean, vol.Optional(CONF_VERSION, default=DEFAULT_VERSION): cv.string, @@ -159,7 +167,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: CONF_TOPIC_IN_PREFIX: gw.get(CONF_TOPIC_IN_PREFIX, ""), CONF_RETAIN: config[CONF_RETAIN], CONF_VERSION: config[CONF_VERSION], - CONF_PERSISTENCE_FILE: gw.get(CONF_PERSISTENCE_FILE) + CONF_PERSISTENCE_FILE: gw[CONF_PERSISTENCE_FILE] # nodes config ignored at this time. renaming nodes can now be done from the frontend. } for gw in config[CONF_GATEWAYS] diff --git a/homeassistant/components/mysensors/config_flow.py b/homeassistant/components/mysensors/config_flow.py index 59dff4829de..847408abcc5 100644 --- a/homeassistant/components/mysensors/config_flow.py +++ b/homeassistant/components/mysensors/config_flow.py @@ -324,7 +324,9 @@ class MySensorsConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): except vol.Invalid: errors[CONF_PERSISTENCE_FILE] = "invalid_persistence_file" else: - real_persistence_path = self._normalize_persistence_file( + real_persistence_path = user_input[ + CONF_PERSISTENCE_FILE + ] = self._normalize_persistence_file( user_input[CONF_PERSISTENCE_FILE] ) for other_entry in self.hass.config_entries.async_entries(DOMAIN): diff --git a/homeassistant/components/netatmo/climate.py b/homeassistant/components/netatmo/climate.py index 9993b4efac2..3b36e3089a2 100644 --- a/homeassistant/components/netatmo/climate.py +++ b/homeassistant/components/netatmo/climate.py @@ -651,8 +651,5 @@ def get_all_home_ids(home_data: pyatmo.HomeData) -> list[str]: return [ home_data.homes[home_id]["id"] for home_id in home_data.homes - if ( - "therm_schedules" in home_data.homes[home_id] - and "modules" in home_data.homes[home_id] - ) + if "modules" in home_data.homes[home_id] ] diff --git a/homeassistant/components/nuki/__init__.py b/homeassistant/components/nuki/__init__.py index f937bddf623..ea224612d82 100644 --- a/homeassistant/components/nuki/__init__.py +++ b/homeassistant/components/nuki/__init__.py @@ -10,7 +10,7 @@ from requests.exceptions import RequestException from homeassistant import exceptions from homeassistant.config_entries import SOURCE_IMPORT -from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TOKEN +from homeassistant.const import CONF_HOST, CONF_PLATFORM, CONF_PORT, CONF_TOKEN from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -22,6 +22,7 @@ from .const import ( DATA_COORDINATOR, DATA_LOCKS, DATA_OPENERS, + DEFAULT_PORT, DEFAULT_TIMEOUT, DOMAIN, ERROR_STATES, @@ -59,11 +60,18 @@ async def async_setup(hass, config): continue for conf in confs: - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=conf + if CONF_PLATFORM in conf and conf[CONF_PLATFORM] == DOMAIN: + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data={ + CONF_HOST: conf[CONF_HOST], + CONF_PORT: conf.get(CONF_PORT, DEFAULT_PORT), + CONF_TOKEN: conf[CONF_TOKEN], + }, + ) ) - ) return True diff --git a/homeassistant/components/onewire/__init__.py b/homeassistant/components/onewire/__init__.py index 4bf0382a92c..fbcc5a5fe04 100644 --- a/homeassistant/components/onewire/__init__.py +++ b/homeassistant/components/onewire/__init__.py @@ -23,7 +23,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry): except CannotConnect as exc: raise ConfigEntryNotReady() from exc - hass.data[DOMAIN][config_entry.unique_id] = onewirehub + hass.data[DOMAIN][config_entry.entry_id] = onewirehub async def cleanup_registry() -> None: # Get registries @@ -71,5 +71,5 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry): config_entry, PLATFORMS ) if unload_ok: - hass.data[DOMAIN].pop(config_entry.unique_id) + hass.data[DOMAIN].pop(config_entry.entry_id) return unload_ok diff --git a/homeassistant/components/onewire/binary_sensor.py b/homeassistant/components/onewire/binary_sensor.py index 86b584c998c..4e25ba431c3 100644 --- a/homeassistant/components/onewire/binary_sensor.py +++ b/homeassistant/components/onewire/binary_sensor.py @@ -81,7 +81,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Set up 1-Wire platform.""" # Only OWServer implementation works with binary sensors if config_entry.data[CONF_TYPE] == CONF_TYPE_OWSERVER: - onewirehub = hass.data[DOMAIN][config_entry.unique_id] + onewirehub = hass.data[DOMAIN][config_entry.entry_id] entities = await hass.async_add_executor_job(get_entities, onewirehub) async_add_entities(entities, True) diff --git a/homeassistant/components/onewire/sensor.py b/homeassistant/components/onewire/sensor.py index b3a5be0a1ca..c39862f9bab 100644 --- a/homeassistant/components/onewire/sensor.py +++ b/homeassistant/components/onewire/sensor.py @@ -256,7 +256,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async def async_setup_entry(hass, config_entry, async_add_entities): """Set up 1-Wire platform.""" - onewirehub = hass.data[DOMAIN][config_entry.unique_id] + onewirehub = hass.data[DOMAIN][config_entry.entry_id] entities = await hass.async_add_executor_job( get_entities, onewirehub, config_entry.data ) diff --git a/homeassistant/components/onewire/switch.py b/homeassistant/components/onewire/switch.py index da1ed01a980..1753800fbf0 100644 --- a/homeassistant/components/onewire/switch.py +++ b/homeassistant/components/onewire/switch.py @@ -144,7 +144,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Set up 1-Wire platform.""" # Only OWServer implementation works with switches if config_entry.data[CONF_TYPE] == CONF_TYPE_OWSERVER: - onewirehub = hass.data[DOMAIN][config_entry.unique_id] + onewirehub = hass.data[DOMAIN][config_entry.entry_id] entities = await hass.async_add_executor_job(get_entities, onewirehub) async_add_entities(entities, True) diff --git a/homeassistant/components/recorder/manifest.json b/homeassistant/components/recorder/manifest.json index a79a79fbc4a..6a3f6ae6b54 100644 --- a/homeassistant/components/recorder/manifest.json +++ b/homeassistant/components/recorder/manifest.json @@ -2,7 +2,7 @@ "domain": "recorder", "name": "Recorder", "documentation": "https://www.home-assistant.io/integrations/recorder", - "requirements": ["sqlalchemy==1.4.11"], + "requirements": ["sqlalchemy==1.4.13"], "codeowners": [], "quality_scale": "internal", "iot_class": "local_push" diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py index 80784ce51b7..4f9b10abb8c 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -213,11 +213,14 @@ async def async_setup_entry(hass, config_entry): # noqa: C901 _async_save_refresh_token(hass, config_entry, api.refresh_token) - simplisafe = hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id] = SimpliSafe( - hass, api, config_entry - ) - await simplisafe.async_init() + simplisafe = SimpliSafe(hass, api, config_entry) + try: + await simplisafe.async_init() + except SimplipyError as err: + raise ConfigEntryNotReady from err + + hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id] = simplisafe hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) @callback diff --git a/homeassistant/components/sql/manifest.json b/homeassistant/components/sql/manifest.json index 1716664c129..e9805040648 100644 --- a/homeassistant/components/sql/manifest.json +++ b/homeassistant/components/sql/manifest.json @@ -2,7 +2,7 @@ "domain": "sql", "name": "SQL", "documentation": "https://www.home-assistant.io/integrations/sql", - "requirements": ["sqlalchemy==1.4.11"], + "requirements": ["sqlalchemy==1.4.13"], "codeowners": ["@dgomes"], "iot_class": "local_polling" } diff --git a/homeassistant/components/tesla/__init__.py b/homeassistant/components/tesla/__init__.py index 2b0373dba33..54e3bab3f44 100644 --- a/homeassistant/components/tesla/__init__.py +++ b/homeassistant/components/tesla/__init__.py @@ -23,7 +23,7 @@ from homeassistant.const import ( HTTP_UNAUTHORIZED, ) from homeassistant.core import callback -from homeassistant.exceptions import ConfigEntryAuthFailed +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import config_validation as cv from homeassistant.helpers.httpx_client import SERVER_SOFTWARE, USER_AGENT from homeassistant.helpers.update_coordinator import ( @@ -170,6 +170,9 @@ async def async_setup_entry(hass, config_entry): except IncompleteCredentials as ex: await async_client.aclose() raise ConfigEntryAuthFailed from ex + except httpx.ConnectTimeout as ex: + await async_client.aclose() + raise ConfigEntryNotReady from ex except TeslaException as ex: await async_client.aclose() if ex.code == HTTP_UNAUTHORIZED: diff --git a/homeassistant/components/xiaomi_miio/manifest.json b/homeassistant/components/xiaomi_miio/manifest.json index 6566270041a..939e30edda8 100644 --- a/homeassistant/components/xiaomi_miio/manifest.json +++ b/homeassistant/components/xiaomi_miio/manifest.json @@ -3,7 +3,7 @@ "name": "Xiaomi Miio", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/xiaomi_miio", - "requirements": ["construct==2.10.56", "python-miio==0.5.5"], + "requirements": ["construct==2.10.56", "python-miio==0.5.6"], "codeowners": ["@rytilahti", "@syssi", "@starkillerOG"], "zeroconf": ["_miio._udp.local."], "iot_class": "local_polling" diff --git a/homeassistant/components/zha/api.py b/homeassistant/components/zha/api.py index 2b41deaab6b..053162010e8 100644 --- a/homeassistant/components/zha/api.py +++ b/homeassistant/components/zha/api.py @@ -61,7 +61,7 @@ from .core.const import ( ) from .core.group import GroupMember from .core.helpers import ( - async_input_cluster_exists, + async_cluster_exists, async_is_bindable_target, convert_install_code, get_matched_clusters, @@ -897,7 +897,7 @@ async def websocket_get_configuration(hass, connection, msg): data = {"schemas": {}, "data": {}} for section, schema in ZHA_CONFIG_SCHEMAS.items(): - if section == ZHA_ALARM_OPTIONS and not async_input_cluster_exists( + if section == ZHA_ALARM_OPTIONS and not async_cluster_exists( hass, IasAce.cluster_id ): continue diff --git a/homeassistant/components/zha/core/helpers.py b/homeassistant/components/zha/core/helpers.py index 84088148a8e..34359c19420 100644 --- a/homeassistant/components/zha/core/helpers.py +++ b/homeassistant/components/zha/core/helpers.py @@ -139,14 +139,17 @@ def async_get_zha_config_value(config_entry, section, config_key, default): ) -def async_input_cluster_exists(hass, cluster_id): +def async_cluster_exists(hass, cluster_id): """Determine if a device containing the specified in cluster is paired.""" zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] zha_devices = zha_gateway.devices.values() for zha_device in zha_devices: clusters_by_endpoint = zha_device.async_get_clusters() for clusters in clusters_by_endpoint.values(): - if cluster_id in clusters[CLUSTER_TYPE_IN]: + if ( + cluster_id in clusters[CLUSTER_TYPE_IN] + or cluster_id in clusters[CLUSTER_TYPE_OUT] + ): return True return False diff --git a/homeassistant/components/zha/core/registries.py b/homeassistant/components/zha/core/registries.py index 42f09d5323f..5fe7f806355 100644 --- a/homeassistant/components/zha/core/registries.py +++ b/homeassistant/components/zha/core/registries.py @@ -83,7 +83,8 @@ SINGLE_INPUT_CLUSTER_DEVICE_CLASS = { } SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS = { - zcl.clusters.general.OnOff.cluster_id: BINARY_SENSOR + zcl.clusters.general.OnOff.cluster_id: BINARY_SENSOR, + zcl.clusters.security.IasAce.cluster_id: ALARM, } BINDABLE_CLUSTERS = SetRegistry() diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index 3fd443e5643..81600ec6c16 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -25,7 +25,7 @@ from homeassistant.components.websocket_api.const import ( ERR_NOT_SUPPORTED, ERR_UNKNOWN_ERROR, ) -from homeassistant.config_entries import ConfigEntry +from homeassistant.config_entries import ENTRY_STATE_LOADED, ConfigEntry from homeassistant.const import CONF_URL from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv @@ -44,6 +44,7 @@ from .helpers import async_enable_statistics, update_data_collection_preference # general API constants ID = "id" ENTRY_ID = "entry_id" +ERR_NOT_LOADED = "not_loaded" NODE_ID = "node_id" COMMAND_CLASS_ID = "command_class_id" TYPE = "type" @@ -83,6 +84,13 @@ def async_get_entry(orig_func: Callable) -> Callable: msg[ID], ERR_NOT_FOUND, f"Config entry {entry_id} not found" ) return + + if entry.state != ENTRY_STATE_LOADED: + connection.send_error( + msg[ID], ERR_NOT_LOADED, f"Config entry {entry_id} not loaded" + ) + return + client = hass.data[DOMAIN][entry_id][DATA_CLIENT] await orig_func(hass, connection, msg, entry, client) @@ -137,17 +145,20 @@ def async_register_api(hass: HomeAssistant) -> None: hass.http.register_view(DumpView) # type: ignore -@websocket_api.require_admin +@websocket_api.require_admin # type: ignore +@websocket_api.async_response @websocket_api.websocket_command( {vol.Required(TYPE): "zwave_js/network_status", vol.Required(ENTRY_ID): str} ) -@callback -def websocket_network_status( - hass: HomeAssistant, connection: ActiveConnection, msg: dict +@async_get_entry +async def websocket_network_status( + hass: HomeAssistant, + connection: ActiveConnection, + msg: dict, + entry: ConfigEntry, + client: Client, ) -> None: """Get the status of the Z-Wave JS network.""" - entry_id = msg[ENTRY_ID] - client = hass.data[DOMAIN][entry_id][DATA_CLIENT] data = { "client": { "ws_server_url": client.ws_server_url, @@ -166,6 +177,7 @@ def websocket_network_status( ) +@websocket_api.async_response # type: ignore @websocket_api.websocket_command( { vol.Required(TYPE): "zwave_js/node_status", @@ -173,20 +185,14 @@ def websocket_network_status( vol.Required(NODE_ID): int, } ) -@callback -def websocket_node_status( - hass: HomeAssistant, connection: ActiveConnection, msg: dict +@async_get_node +async def websocket_node_status( + hass: HomeAssistant, + connection: ActiveConnection, + msg: dict, + node: Node, ) -> None: """Get the status of a Z-Wave JS node.""" - entry_id = msg[ENTRY_ID] - client = hass.data[DOMAIN][entry_id][DATA_CLIENT] - node_id = msg[NODE_ID] - node = client.driver.controller.nodes.get(node_id) - - if node is None: - connection.send_error(msg[ID], ERR_NOT_FOUND, f"Node {node_id} not found") - return - data = { "node_id": node.node_id, "is_routing": node.is_routing, @@ -537,7 +543,8 @@ async def websocket_set_config_parameter( ) -@websocket_api.require_admin +@websocket_api.require_admin # type: ignore +@websocket_api.async_response @websocket_api.websocket_command( { vol.Required(TYPE): "zwave_js/get_config_parameters", @@ -545,20 +552,11 @@ async def websocket_set_config_parameter( vol.Required(NODE_ID): int, } ) -@callback -def websocket_get_config_parameters( - hass: HomeAssistant, connection: ActiveConnection, msg: dict +@async_get_node +async def websocket_get_config_parameters( + hass: HomeAssistant, connection: ActiveConnection, msg: dict, node: Node ) -> None: """Get a list of configuration parameters for a Z-Wave node.""" - entry_id = msg[ENTRY_ID] - node_id = msg[NODE_ID] - client = hass.data[DOMAIN][entry_id][DATA_CLIENT] - node = client.driver.controller.nodes.get(node_id) - - if node is None: - connection.send_error(msg[ID], ERR_NOT_FOUND, f"Node {node_id} not found") - return - values = node.get_configuration_values() result = {} for value_id, zwave_value in values.items(): diff --git a/homeassistant/const.py b/homeassistant/const.py index f7ffd77bcbf..56fe2b36e57 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 2021 MINOR_VERSION = 5 -PATCH_VERSION = "0" +PATCH_VERSION = "1" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 8, 0) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 67c8436f67a..5e4d3e2c2ff 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -30,7 +30,7 @@ pyyaml==5.4.1 requests==2.25.1 ruamel.yaml==0.15.100 scapy==2.4.5 -sqlalchemy==1.4.11 +sqlalchemy==1.4.13 voluptuous-serialize==2.4.0 voluptuous==0.12.1 yarl==1.6.3 diff --git a/requirements_all.txt b/requirements_all.txt index 6a55af80d8f..862119fd519 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -182,7 +182,7 @@ aiohomekit==0.2.61 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==2.1.0 +aiohue==2.3.0 # homeassistant.components.imap aioimaplib==0.7.15 @@ -479,7 +479,7 @@ defusedxml==0.6.0 deluge-client==1.7.1 # homeassistant.components.denonavr -denonavr==0.10.5 +denonavr==0.10.6 # homeassistant.components.devolo_home_control devolo-home-control-api==0.17.3 @@ -1322,7 +1322,7 @@ pychromecast==9.1.2 pycketcasts==1.0.0 # homeassistant.components.climacell -pyclimacell==0.18.0 +pyclimacell==0.18.2 # homeassistant.components.cmus pycmus==0.1.1 @@ -1819,7 +1819,7 @@ python-juicenet==1.0.1 # python-lirc==1.2.3 # homeassistant.components.xiaomi_miio -python-miio==0.5.5 +python-miio==0.5.6 # homeassistant.components.mpd python-mpd2==3.0.4 @@ -2142,7 +2142,7 @@ spotipy==2.18.0 # homeassistant.components.recorder # homeassistant.components.sql -sqlalchemy==1.4.11 +sqlalchemy==1.4.13 # homeassistant.components.srp_energy srpenergy==1.3.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9cc4476781f..15f6ce2aa97 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -119,7 +119,7 @@ aiohomekit==0.2.61 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==2.1.0 +aiohue==2.3.0 # homeassistant.components.apache_kafka aiokafka==0.6.0 @@ -264,7 +264,7 @@ debugpy==1.2.1 defusedxml==0.6.0 # homeassistant.components.denonavr -denonavr==0.10.5 +denonavr==0.10.6 # homeassistant.components.devolo_home_control devolo-home-control-api==0.17.3 @@ -720,7 +720,7 @@ pycfdns==1.2.1 pychromecast==9.1.2 # homeassistant.components.climacell -pyclimacell==0.18.0 +pyclimacell==0.18.2 # homeassistant.components.comfoconnect pycomfoconnect==0.4 @@ -983,7 +983,7 @@ python-izone==1.1.4 python-juicenet==1.0.1 # homeassistant.components.xiaomi_miio -python-miio==0.5.5 +python-miio==0.5.6 # homeassistant.components.nest python-nest==4.1.0 @@ -1144,7 +1144,7 @@ spotipy==2.18.0 # homeassistant.components.recorder # homeassistant.components.sql -sqlalchemy==1.4.11 +sqlalchemy==1.4.13 # homeassistant.components.srp_energy srpenergy==1.3.2 diff --git a/tests/components/broadlink/test_device.py b/tests/components/broadlink/test_device.py index df22bcaffcb..8e53fd74c1c 100644 --- a/tests/components/broadlink/test_device.py +++ b/tests/components/broadlink/test_device.py @@ -144,7 +144,10 @@ async def test_device_setup_update_authorization_error(hass): """Test we handle an authorization error in the update step.""" device = get_device("Office") mock_api = device.get_mock_api() - mock_api.check_sensors.side_effect = (blke.AuthorizationError(), None) + mock_api.check_sensors.side_effect = ( + blke.AuthorizationError(), + {"temperature": 30}, + ) with patch.object( hass.config_entries, "async_forward_entry_setup" diff --git a/tests/components/broadlink/test_sensors.py b/tests/components/broadlink/test_sensors.py index de0cd88f288..e5d31705a4f 100644 --- a/tests/components/broadlink/test_sensors.py +++ b/tests/components/broadlink/test_sensors.py @@ -143,6 +143,38 @@ async def test_rm_pro_sensor_update(hass): assert sensors_and_states == {(f"{device.name} Temperature", "25.8")} +async def test_rm_pro_filter_crazy_temperature(hass): + """Test we filter a crazy temperature variation. + + Firmware issue. See https://github.com/home-assistant/core/issues/42100. + """ + device = get_device("Office") + mock_api = device.get_mock_api() + mock_api.check_sensors.return_value = {"temperature": 22.9} + + device_registry = mock_device_registry(hass) + entity_registry = mock_registry(hass) + + mock_api, mock_entry = await device.setup_entry(hass, mock_api=mock_api) + + device_entry = device_registry.async_get_device({(DOMAIN, mock_entry.unique_id)}) + entries = async_entries_for_device(entity_registry, device_entry.id) + sensors = {entry for entry in entries if entry.domain == SENSOR_DOMAIN} + assert len(sensors) == 1 + + mock_api.check_sensors.return_value = {"temperature": -7} + await hass.helpers.entity_component.async_update_entity( + next(iter(sensors)).entity_id + ) + assert mock_api.check_sensors.call_count == 2 + + sensors_and_states = { + (sensor.original_name, hass.states.get(sensor.entity_id).state) + for sensor in sensors + } + assert sensors_and_states == {(f"{device.name} Temperature", "22.9")} + + async def test_rm_mini3_no_sensor(hass): """Test we do not set up sensors for RM mini 3.""" device = get_device("Entrance") diff --git a/tests/components/climacell/test_sensor.py b/tests/components/climacell/test_sensor.py index 7757fe208d3..44fc163848b 100644 --- a/tests/components/climacell/test_sensor.py +++ b/tests/components/climacell/test_sensor.py @@ -119,9 +119,9 @@ async def test_v3_sensor( check_sensor_state(hass, EPA_HEALTH_CONCERN, "Good") check_sensor_state(hass, EPA_PRIMARY_POLLUTANT, "pm25") check_sensor_state(hass, FIRE_INDEX, "9") - check_sensor_state(hass, GRASS_POLLEN, "0") - check_sensor_state(hass, WEED_POLLEN, "0") - check_sensor_state(hass, TREE_POLLEN, "0") + check_sensor_state(hass, GRASS_POLLEN, "minimal_to_none") + check_sensor_state(hass, WEED_POLLEN, "minimal_to_none") + check_sensor_state(hass, TREE_POLLEN, "minimal_to_none") async def test_v4_sensor( diff --git a/tests/components/demo/test_light.py b/tests/components/demo/test_light.py index 4e7f58811d9..7633cbe5ccf 100644 --- a/tests/components/demo/test_light.py +++ b/tests/components/demo/test_light.py @@ -11,7 +11,6 @@ from homeassistant.components.light import ( ATTR_MAX_MIREDS, ATTR_MIN_MIREDS, ATTR_RGB_COLOR, - ATTR_WHITE_VALUE, ATTR_XY_COLOR, DOMAIN as LIGHT_DOMAIN, SERVICE_TURN_OFF, @@ -54,13 +53,11 @@ async def test_state_attributes(hass): { ATTR_ENTITY_ID: ENTITY_LIGHT, ATTR_RGB_COLOR: (251, 253, 255), - ATTR_WHITE_VALUE: 254, }, blocking=True, ) state = hass.states.get(ENTITY_LIGHT) - assert state.attributes.get(ATTR_WHITE_VALUE) == 254 assert state.attributes.get(ATTR_RGB_COLOR) == (250, 252, 255) assert state.attributes.get(ATTR_XY_COLOR) == (0.319, 0.326) diff --git a/tests/components/emulated_hue/test_hue_api.py b/tests/components/emulated_hue/test_hue_api.py index c0adad38c9d..cb0b1f39365 100644 --- a/tests/components/emulated_hue/test_hue_api.py +++ b/tests/components/emulated_hue/test_hue_api.py @@ -91,6 +91,8 @@ ENTITY_IDS_BY_NUMBER = { "22": "scene.light_on", "23": "scene.light_off", "24": "media_player.kitchen", + "25": "light.office_rgbw_lights", + "26": "light.living_room_rgbww_lights", } ENTITY_NUMBERS_BY_ID = {v: k for k, v in ENTITY_IDS_BY_NUMBER.items()} diff --git a/tests/components/google_assistant/__init__.py b/tests/components/google_assistant/__init__.py index 0fe89d0fa7b..123ca120243 100644 --- a/tests/components/google_assistant/__init__.py +++ b/tests/components/google_assistant/__init__.py @@ -380,4 +380,26 @@ DEMO_DEVICES = [ "type": "action.devices.types.SECURITYSYSTEM", "willReportState": False, }, + { + "id": "light.living_room_rgbww_lights", + "name": {"name": "Living Room RGBWW Lights"}, + "traits": [ + "action.devices.traits.OnOff", + "action.devices.traits.Brightness", + "action.devices.traits.ColorSetting", + ], + "type": "action.devices.types.LIGHT", + "willReportState": False, + }, + { + "id": "light.office_rgbw_lights", + "name": {"name": "Office RGBW Lights"}, + "traits": [ + "action.devices.traits.OnOff", + "action.devices.traits.Brightness", + "action.devices.traits.ColorSetting", + ], + "type": "action.devices.types.LIGHT", + "willReportState": False, + }, ] diff --git a/tests/components/group/test_light.py b/tests/components/group/test_light.py index 136da458f66..c9b861a46a9 100644 --- a/tests/components/group/test_light.py +++ b/tests/components/group/test_light.py @@ -8,6 +8,7 @@ from homeassistant.components.group import DOMAIN, SERVICE_RELOAD import homeassistant.components.group.light as group from homeassistant.components.light import ( ATTR_BRIGHTNESS, + ATTR_COLOR_MODE, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_EFFECT_LIST, @@ -16,13 +17,24 @@ from homeassistant.components.light import ( ATTR_MAX_MIREDS, ATTR_MIN_MIREDS, ATTR_RGB_COLOR, + ATTR_RGBW_COLOR, + ATTR_RGBWW_COLOR, + ATTR_SUPPORTED_COLOR_MODES, ATTR_TRANSITION, ATTR_WHITE_VALUE, ATTR_XY_COLOR, + COLOR_MODE_BRIGHTNESS, + COLOR_MODE_COLOR_TEMP, + COLOR_MODE_HS, + COLOR_MODE_RGBW, + COLOR_MODE_RGBWW, DOMAIN as LIGHT_DOMAIN, SERVICE_TOGGLE, SERVICE_TURN_OFF, SERVICE_TURN_ON, + SUPPORT_BRIGHTNESS, + SUPPORT_COLOR, + SUPPORT_COLOR_TEMP, ) from homeassistant.const import ( ATTR_ENTITY_ID, @@ -104,85 +116,281 @@ async def test_state_reporting(hass): async def test_brightness(hass): """Test brightness reporting.""" - await async_setup_component( + platform = getattr(hass.components, "test.light") + platform.init(empty=True) + + platform.ENTITIES.append(platform.MockLight("test1", STATE_ON)) + platform.ENTITIES.append(platform.MockLight("test2", STATE_OFF)) + + entity0 = platform.ENTITIES[0] + entity0.supported_color_modes = {COLOR_MODE_BRIGHTNESS} + entity0.color_mode = COLOR_MODE_BRIGHTNESS + entity0.brightness = 255 + + entity1 = platform.ENTITIES[1] + entity1.supported_features = SUPPORT_BRIGHTNESS + + assert await async_setup_component( hass, LIGHT_DOMAIN, { - LIGHT_DOMAIN: { - "platform": DOMAIN, - "entities": ["light.test1", "light.test2"], - } + LIGHT_DOMAIN: [ + {"platform": "test"}, + { + "platform": DOMAIN, + "entities": ["light.test1", "light.test2"], + }, + ] }, ) await hass.async_block_till_done() await hass.async_start() await hass.async_block_till_done() - hass.states.async_set( - "light.test1", STATE_ON, {ATTR_BRIGHTNESS: 255, ATTR_SUPPORTED_FEATURES: 1} - ) - await hass.async_block_till_done() state = hass.states.get("light.light_group") assert state.state == STATE_ON - assert state.attributes[ATTR_SUPPORTED_FEATURES] == 1 assert state.attributes[ATTR_BRIGHTNESS] == 255 + assert state.attributes[ATTR_COLOR_MODE] == "brightness" + assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0 + assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == ["brightness"] - hass.states.async_set( - "light.test2", STATE_ON, {ATTR_BRIGHTNESS: 100, ATTR_SUPPORTED_FEATURES: 1} + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": [entity1.entity_id], ATTR_BRIGHTNESS: 100}, + blocking=True, ) await hass.async_block_till_done() state = hass.states.get("light.light_group") assert state.state == STATE_ON assert state.attributes[ATTR_BRIGHTNESS] == 177 + assert state.attributes[ATTR_COLOR_MODE] == "brightness" + assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0 + assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == ["brightness"] - hass.states.async_set( - "light.test1", STATE_OFF, {ATTR_BRIGHTNESS: 255, ATTR_SUPPORTED_FEATURES: 1} + await hass.services.async_call( + "light", + "turn_off", + {"entity_id": [entity0.entity_id]}, + blocking=True, ) await hass.async_block_till_done() state = hass.states.get("light.light_group") assert state.state == STATE_ON - assert state.attributes[ATTR_SUPPORTED_FEATURES] == 1 assert state.attributes[ATTR_BRIGHTNESS] == 100 + assert state.attributes[ATTR_COLOR_MODE] == "brightness" + assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0 + assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == ["brightness"] -async def test_color(hass): - """Test RGB reporting.""" - await async_setup_component( +async def test_color_hs(hass): + """Test hs color reporting.""" + platform = getattr(hass.components, "test.light") + platform.init(empty=True) + + platform.ENTITIES.append(platform.MockLight("test1", STATE_ON)) + platform.ENTITIES.append(platform.MockLight("test2", STATE_OFF)) + + entity0 = platform.ENTITIES[0] + entity0.supported_color_modes = {COLOR_MODE_HS} + entity0.color_mode = COLOR_MODE_HS + entity0.brightness = 255 + entity0.hs_color = (0, 100) + + entity1 = platform.ENTITIES[1] + entity1.supported_features = SUPPORT_COLOR + + assert await async_setup_component( hass, LIGHT_DOMAIN, { - LIGHT_DOMAIN: { - "platform": DOMAIN, - "entities": ["light.test1", "light.test2"], - } + LIGHT_DOMAIN: [ + {"platform": "test"}, + { + "platform": DOMAIN, + "entities": ["light.test1", "light.test2"], + }, + ] }, ) await hass.async_block_till_done() await hass.async_start() await hass.async_block_till_done() - hass.states.async_set( - "light.test1", STATE_ON, {ATTR_HS_COLOR: (0, 100), ATTR_SUPPORTED_FEATURES: 16} - ) - await hass.async_block_till_done() state = hass.states.get("light.light_group") assert state.state == STATE_ON - assert state.attributes[ATTR_SUPPORTED_FEATURES] == 16 + assert state.attributes[ATTR_COLOR_MODE] == "hs" assert state.attributes[ATTR_HS_COLOR] == (0, 100) + assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == ["hs"] + assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0 - hass.states.async_set( - "light.test2", STATE_ON, {ATTR_HS_COLOR: (0, 50), ATTR_SUPPORTED_FEATURES: 16} + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": [entity1.entity_id], ATTR_HS_COLOR: (0, 50)}, + blocking=True, ) await hass.async_block_till_done() state = hass.states.get("light.light_group") + assert state.attributes[ATTR_COLOR_MODE] == "hs" assert state.attributes[ATTR_HS_COLOR] == (0, 75) + assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == ["hs"] + assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0 - hass.states.async_set( - "light.test1", STATE_OFF, {ATTR_HS_COLOR: (0, 0), ATTR_SUPPORTED_FEATURES: 16} + await hass.services.async_call( + "light", + "turn_off", + {"entity_id": [entity0.entity_id]}, + blocking=True, ) await hass.async_block_till_done() state = hass.states.get("light.light_group") + assert state.attributes[ATTR_COLOR_MODE] == "hs" assert state.attributes[ATTR_HS_COLOR] == (0, 50) + assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == ["hs"] + assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0 + + +async def test_color_rgbw(hass): + """Test rgbw color reporting.""" + platform = getattr(hass.components, "test.light") + platform.init(empty=True) + + platform.ENTITIES.append(platform.MockLight("test1", STATE_ON)) + platform.ENTITIES.append(platform.MockLight("test2", STATE_OFF)) + + entity0 = platform.ENTITIES[0] + entity0.supported_color_modes = {COLOR_MODE_RGBW} + entity0.color_mode = COLOR_MODE_RGBW + entity0.brightness = 255 + entity0.rgbw_color = (0, 64, 128, 255) + + entity1 = platform.ENTITIES[1] + entity1.supported_color_modes = {COLOR_MODE_RGBW} + entity1.color_mode = COLOR_MODE_RGBW + entity1.brightness = 255 + entity1.rgbw_color = (255, 128, 64, 0) + + assert await async_setup_component( + hass, + LIGHT_DOMAIN, + { + LIGHT_DOMAIN: [ + {"platform": "test"}, + { + "platform": DOMAIN, + "entities": ["light.test1", "light.test2"], + }, + ] + }, + ) + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + state = hass.states.get("light.light_group") + assert state.state == STATE_ON + assert state.attributes[ATTR_COLOR_MODE] == "rgbw" + assert state.attributes[ATTR_RGBW_COLOR] == (0, 64, 128, 255) + assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == ["rgbw"] + assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0 + + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": [entity1.entity_id]}, + blocking=True, + ) + await hass.async_block_till_done() + state = hass.states.get("light.light_group") + assert state.attributes[ATTR_COLOR_MODE] == "rgbw" + assert state.attributes[ATTR_RGBW_COLOR] == (127, 96, 96, 127) + assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == ["rgbw"] + assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0 + + await hass.services.async_call( + "light", + "turn_off", + {"entity_id": [entity0.entity_id]}, + blocking=True, + ) + await hass.async_block_till_done() + state = hass.states.get("light.light_group") + assert state.attributes[ATTR_COLOR_MODE] == "rgbw" + assert state.attributes[ATTR_RGBW_COLOR] == (255, 128, 64, 0) + assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == ["rgbw"] + assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0 + + +async def test_color_rgbww(hass): + """Test rgbww color reporting.""" + platform = getattr(hass.components, "test.light") + platform.init(empty=True) + + platform.ENTITIES.append(platform.MockLight("test1", STATE_ON)) + platform.ENTITIES.append(platform.MockLight("test2", STATE_OFF)) + + entity0 = platform.ENTITIES[0] + entity0.supported_color_modes = {COLOR_MODE_RGBWW} + entity0.color_mode = COLOR_MODE_RGBWW + entity0.brightness = 255 + entity0.rgbww_color = (0, 32, 64, 128, 255) + + entity1 = platform.ENTITIES[1] + entity1.supported_color_modes = {COLOR_MODE_RGBWW} + entity1.color_mode = COLOR_MODE_RGBWW + entity1.brightness = 255 + entity1.rgbww_color = (255, 128, 64, 32, 0) + + assert await async_setup_component( + hass, + LIGHT_DOMAIN, + { + LIGHT_DOMAIN: [ + {"platform": "test"}, + { + "platform": DOMAIN, + "entities": ["light.test1", "light.test2"], + }, + ] + }, + ) + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + state = hass.states.get("light.light_group") + assert state.state == STATE_ON + assert state.attributes[ATTR_COLOR_MODE] == "rgbww" + assert state.attributes[ATTR_RGBWW_COLOR] == (0, 32, 64, 128, 255) + assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == ["rgbww"] + assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0 + + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": [entity1.entity_id]}, + blocking=True, + ) + await hass.async_block_till_done() + state = hass.states.get("light.light_group") + assert state.attributes[ATTR_COLOR_MODE] == "rgbww" + assert state.attributes[ATTR_RGBWW_COLOR] == (127, 80, 64, 80, 127) + assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == ["rgbww"] + assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0 + + await hass.services.async_call( + "light", + "turn_off", + {"entity_id": [entity0.entity_id]}, + blocking=True, + ) + await hass.async_block_till_done() + state = hass.states.get("light.light_group") + assert state.attributes[ATTR_COLOR_MODE] == "rgbww" + assert state.attributes[ATTR_RGBWW_COLOR] == (255, 128, 64, 32, 0) + assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == ["rgbww"] + assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0 async def test_white_value(hass): @@ -206,6 +414,7 @@ async def test_white_value(hass): ) await hass.async_block_till_done() state = hass.states.get("light.light_group") + assert state.attributes[ATTR_SUPPORTED_FEATURES] == 128 assert state.attributes[ATTR_WHITE_VALUE] == 255 hass.states.async_set( @@ -213,6 +422,7 @@ async def test_white_value(hass): ) await hass.async_block_till_done() state = hass.states.get("light.light_group") + assert state.attributes[ATTR_SUPPORTED_FEATURES] == 128 assert state.attributes[ATTR_WHITE_VALUE] == 177 hass.states.async_set( @@ -220,62 +430,36 @@ async def test_white_value(hass): ) await hass.async_block_till_done() state = hass.states.get("light.light_group") + assert state.attributes[ATTR_SUPPORTED_FEATURES] == 128 assert state.attributes[ATTR_WHITE_VALUE] == 100 async def test_color_temp(hass): """Test color temp reporting.""" - await async_setup_component( - hass, - LIGHT_DOMAIN, - { - LIGHT_DOMAIN: { - "platform": DOMAIN, - "entities": ["light.test1", "light.test2"], - } - }, - ) - await hass.async_block_till_done() - await hass.async_start() - await hass.async_block_till_done() + platform = getattr(hass.components, "test.light") + platform.init(empty=True) - hass.states.async_set( - "light.test1", STATE_ON, {"color_temp": 2, ATTR_SUPPORTED_FEATURES: 2} - ) - await hass.async_block_till_done() - state = hass.states.get("light.light_group") - assert state.attributes[ATTR_COLOR_TEMP] == 2 + platform.ENTITIES.append(platform.MockLight("test1", STATE_ON)) + platform.ENTITIES.append(platform.MockLight("test2", STATE_OFF)) - hass.states.async_set( - "light.test2", STATE_ON, {"color_temp": 1000, ATTR_SUPPORTED_FEATURES: 2} - ) - await hass.async_block_till_done() - state = hass.states.get("light.light_group") - assert state.attributes[ATTR_COLOR_TEMP] == 501 + entity0 = platform.ENTITIES[0] + entity0.supported_color_modes = {COLOR_MODE_COLOR_TEMP} + entity0.color_mode = COLOR_MODE_COLOR_TEMP + entity0.brightness = 255 + entity0.color_temp = 2 - hass.states.async_set( - "light.test1", STATE_OFF, {"color_temp": 2, ATTR_SUPPORTED_FEATURES: 2} - ) - await hass.async_block_till_done() - state = hass.states.get("light.light_group") - assert state.attributes[ATTR_COLOR_TEMP] == 1000 + entity1 = platform.ENTITIES[1] + entity1.supported_features = SUPPORT_COLOR_TEMP - -async def test_emulated_color_temp_group(hass): - """Test emulated color temperature in a group.""" - await async_setup_component( + assert await async_setup_component( hass, LIGHT_DOMAIN, { LIGHT_DOMAIN: [ - {"platform": "demo"}, + {"platform": "test"}, { "platform": DOMAIN, - "entities": [ - "light.bed_light", - "light.ceiling_lights", - "light.kitchen_lights", - ], + "entities": ["light.test1", "light.test2"], }, ] }, @@ -284,13 +468,78 @@ async def test_emulated_color_temp_group(hass): await hass.async_start() await hass.async_block_till_done() - hass.states.async_set("light.bed_light", STATE_ON, {ATTR_SUPPORTED_FEATURES: 2}) - hass.states.async_set( - "light.ceiling_lights", STATE_ON, {ATTR_SUPPORTED_FEATURES: 63} + state = hass.states.get("light.light_group") + assert state.attributes[ATTR_COLOR_MODE] == "color_temp" + assert state.attributes[ATTR_COLOR_TEMP] == 2 + assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0 + assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == ["color_temp"] + + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": [entity1.entity_id], ATTR_COLOR_TEMP: 1000}, + blocking=True, ) - hass.states.async_set( - "light.kitchen_lights", STATE_ON, {ATTR_SUPPORTED_FEATURES: 61} + await hass.async_block_till_done() + state = hass.states.get("light.light_group") + assert state.attributes[ATTR_COLOR_MODE] == "color_temp" + assert state.attributes[ATTR_COLOR_TEMP] == 501 + assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0 + assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == ["color_temp"] + + await hass.services.async_call( + "light", + "turn_off", + {"entity_id": [entity0.entity_id]}, + blocking=True, ) + await hass.async_block_till_done() + state = hass.states.get("light.light_group") + assert state.attributes[ATTR_COLOR_MODE] == "color_temp" + assert state.attributes[ATTR_COLOR_TEMP] == 1000 + assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0 + assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == ["color_temp"] + + +async def test_emulated_color_temp_group(hass): + """Test emulated color temperature in a group.""" + platform = getattr(hass.components, "test.light") + platform.init(empty=True) + + platform.ENTITIES.append(platform.MockLight("test1", STATE_ON)) + platform.ENTITIES.append(platform.MockLight("test2", STATE_OFF)) + platform.ENTITIES.append(platform.MockLight("test3", STATE_OFF)) + + entity0 = platform.ENTITIES[0] + entity0.supported_color_modes = {COLOR_MODE_COLOR_TEMP} + entity0.color_mode = COLOR_MODE_COLOR_TEMP + + entity1 = platform.ENTITIES[1] + entity1.supported_color_modes = {COLOR_MODE_COLOR_TEMP, COLOR_MODE_HS} + entity1.color_mode = COLOR_MODE_COLOR_TEMP + + entity2 = platform.ENTITIES[2] + entity2.supported_color_modes = {COLOR_MODE_HS} + entity2.color_mode = COLOR_MODE_HS + + assert await async_setup_component( + hass, + LIGHT_DOMAIN, + { + LIGHT_DOMAIN: [ + {"platform": "test"}, + { + "platform": DOMAIN, + "entities": ["light.test1", "light.test2", "light.test3"], + }, + ] + }, + ) + + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + await hass.async_block_till_done() await hass.services.async_call( LIGHT_DOMAIN, @@ -300,61 +549,82 @@ async def test_emulated_color_temp_group(hass): ) await hass.async_block_till_done() - state = hass.states.get("light.bed_light") + state = hass.states.get("light.test1") assert state.state == STATE_ON assert state.attributes[ATTR_COLOR_TEMP] == 200 assert ATTR_HS_COLOR not in state.attributes.keys() - state = hass.states.get("light.ceiling_lights") + state = hass.states.get("light.test2") assert state.state == STATE_ON assert state.attributes[ATTR_COLOR_TEMP] == 200 assert ATTR_HS_COLOR not in state.attributes.keys() - state = hass.states.get("light.kitchen_lights") + state = hass.states.get("light.test3") assert state.state == STATE_ON assert state.attributes[ATTR_HS_COLOR] == (27.001, 19.243) async def test_min_max_mireds(hass): - """Test min/max mireds reporting.""" - await async_setup_component( + """Test min/max mireds reporting. + + min/max mireds is reported both when light is on and off + """ + platform = getattr(hass.components, "test.light") + platform.init(empty=True) + + platform.ENTITIES.append(platform.MockLight("test1", STATE_ON)) + platform.ENTITIES.append(platform.MockLight("test2", STATE_OFF)) + + entity0 = platform.ENTITIES[0] + entity0.supported_color_modes = {COLOR_MODE_COLOR_TEMP} + entity0.color_mode = COLOR_MODE_COLOR_TEMP + entity0.color_temp = 2 + entity0.min_mireds = 2 + entity0.max_mireds = 5 + + entity1 = platform.ENTITIES[1] + entity1.supported_features = SUPPORT_COLOR_TEMP + entity1.min_mireds = 1 + entity1.max_mireds = 1234567890 + + assert await async_setup_component( hass, LIGHT_DOMAIN, { - LIGHT_DOMAIN: { - "platform": DOMAIN, - "entities": ["light.test1", "light.test2"], - } + LIGHT_DOMAIN: [ + {"platform": "test"}, + { + "platform": DOMAIN, + "entities": ["light.test1", "light.test2"], + }, + ] }, ) await hass.async_block_till_done() await hass.async_start() await hass.async_block_till_done() - hass.states.async_set( - "light.test1", - STATE_ON, - {ATTR_MIN_MIREDS: 2, ATTR_MAX_MIREDS: 5, ATTR_SUPPORTED_FEATURES: 2}, - ) await hass.async_block_till_done() state = hass.states.get("light.light_group") - assert state.attributes[ATTR_MIN_MIREDS] == 2 - assert state.attributes[ATTR_MAX_MIREDS] == 5 - - hass.states.async_set( - "light.test2", - STATE_ON, - {ATTR_MIN_MIREDS: 7, ATTR_MAX_MIREDS: 1234567890, ATTR_SUPPORTED_FEATURES: 2}, - ) - await hass.async_block_till_done() - state = hass.states.get("light.light_group") - assert state.attributes[ATTR_MIN_MIREDS] == 2 + assert state.attributes[ATTR_MIN_MIREDS] == 1 assert state.attributes[ATTR_MAX_MIREDS] == 1234567890 - hass.states.async_set( - "light.test1", - STATE_OFF, - {ATTR_MIN_MIREDS: 1, ATTR_MAX_MIREDS: 2, ATTR_SUPPORTED_FEATURES: 2}, + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": [entity0.entity_id]}, + blocking=True, + ) + await hass.async_block_till_done() + state = hass.states.get("light.light_group") + assert state.attributes[ATTR_MIN_MIREDS] == 1 + assert state.attributes[ATTR_MAX_MIREDS] == 1234567890 + + await hass.services.async_call( + "light", + "turn_off", + {"entity_id": [entity0.entity_id]}, + blocking=True, ) await hass.async_block_till_done() state = hass.states.get("light.light_group") @@ -465,6 +735,123 @@ async def test_effect(hass): assert state.attributes[ATTR_EFFECT] == "Random" +async def test_supported_color_modes(hass): + """Test supported_color_modes reporting.""" + platform = getattr(hass.components, "test.light") + platform.init(empty=True) + + platform.ENTITIES.append(platform.MockLight("test1", STATE_ON)) + platform.ENTITIES.append(platform.MockLight("test2", STATE_OFF)) + platform.ENTITIES.append(platform.MockLight("test3", STATE_OFF)) + + entity0 = platform.ENTITIES[0] + entity0.supported_color_modes = {COLOR_MODE_COLOR_TEMP, COLOR_MODE_HS} + + entity1 = platform.ENTITIES[1] + entity1.supported_color_modes = {COLOR_MODE_RGBW, COLOR_MODE_RGBWW} + + entity2 = platform.ENTITIES[2] + entity2.supported_features = SUPPORT_BRIGHTNESS + + assert await async_setup_component( + hass, + LIGHT_DOMAIN, + { + LIGHT_DOMAIN: [ + {"platform": "test"}, + { + "platform": DOMAIN, + "entities": ["light.test1", "light.test2", "light.test3"], + }, + ] + }, + ) + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + state = hass.states.get("light.light_group") + assert set(state.attributes[ATTR_SUPPORTED_COLOR_MODES]) == { + "brightness", + "color_temp", + "hs", + "rgbw", + "rgbww", + } + + +async def test_color_mode(hass): + """Test color_mode reporting.""" + platform = getattr(hass.components, "test.light") + platform.init(empty=True) + + platform.ENTITIES.append(platform.MockLight("test1", STATE_ON)) + platform.ENTITIES.append(platform.MockLight("test2", STATE_OFF)) + platform.ENTITIES.append(platform.MockLight("test3", STATE_OFF)) + + entity0 = platform.ENTITIES[0] + entity0.supported_color_modes = {COLOR_MODE_COLOR_TEMP, COLOR_MODE_HS} + entity0.color_mode = COLOR_MODE_COLOR_TEMP + + entity1 = platform.ENTITIES[1] + entity1.supported_color_modes = {COLOR_MODE_COLOR_TEMP, COLOR_MODE_HS} + entity1.color_mode = COLOR_MODE_COLOR_TEMP + + entity2 = platform.ENTITIES[2] + entity2.supported_color_modes = {COLOR_MODE_COLOR_TEMP, COLOR_MODE_HS} + entity2.color_mode = COLOR_MODE_HS + + assert await async_setup_component( + hass, + LIGHT_DOMAIN, + { + LIGHT_DOMAIN: [ + {"platform": "test"}, + { + "platform": DOMAIN, + "entities": ["light.test1", "light.test2", "light.test3"], + }, + ] + }, + ) + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + state = hass.states.get("light.light_group") + assert state.attributes[ATTR_COLOR_MODE] == COLOR_MODE_COLOR_TEMP + + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": [entity1.entity_id]}, + blocking=True, + ) + await hass.async_block_till_done() + state = hass.states.get("light.light_group") + assert state.attributes[ATTR_COLOR_MODE] == COLOR_MODE_COLOR_TEMP + + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": [entity2.entity_id]}, + blocking=True, + ) + await hass.async_block_till_done() + state = hass.states.get("light.light_group") + assert state.attributes[ATTR_COLOR_MODE] == COLOR_MODE_COLOR_TEMP + + await hass.services.async_call( + "light", + "turn_off", + {"entity_id": [entity0.entity_id, entity1.entity_id]}, + blocking=True, + ) + await hass.async_block_till_done() + state = hass.states.get("light.light_group") + assert state.attributes[ATTR_COLOR_MODE] == COLOR_MODE_HS + + async def test_supported_features(hass): """Test supported features reporting.""" await async_setup_component( @@ -486,20 +873,26 @@ async def test_supported_features(hass): state = hass.states.get("light.light_group") assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0 + # SUPPORT_COLOR_TEMP = 2 + # SUPPORT_COLOR_TEMP = 2 will be blocked in favour of COLOR_MODE_COLOR_TEMP hass.states.async_set("light.test2", STATE_ON, {ATTR_SUPPORTED_FEATURES: 2}) await hass.async_block_till_done() state = hass.states.get("light.light_group") - assert state.attributes[ATTR_SUPPORTED_FEATURES] == 2 + assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0 + # SUPPORT_TRANSITION | SUPPORT_FLASH | SUPPORT_BRIGHTNESS = 41 + # SUPPORT_BRIGHTNESS = 1 will be translated to COLOR_MODE_BRIGHTNESS hass.states.async_set("light.test1", STATE_OFF, {ATTR_SUPPORTED_FEATURES: 41}) await hass.async_block_till_done() state = hass.states.get("light.light_group") - assert state.attributes[ATTR_SUPPORTED_FEATURES] == 43 + # SUPPORT_TRANSITION | SUPPORT_FLASH = 40 + assert state.attributes[ATTR_SUPPORTED_FEATURES] == 40 + # Test that unknown feature 256 is blocked hass.states.async_set("light.test2", STATE_OFF, {ATTR_SUPPORTED_FEATURES: 256}) await hass.async_block_till_done() state = hass.states.get("light.light_group") - assert state.attributes[ATTR_SUPPORTED_FEATURES] == 41 + assert state.attributes[ATTR_SUPPORTED_FEATURES] == 40 async def test_service_calls(hass): @@ -629,8 +1022,6 @@ async def test_invalid_service_calls(hass): } await grouped_light.async_turn_on(**data) data[ATTR_ENTITY_ID] = ["light.test1", "light.test2"] - data.pop(ATTR_RGB_COLOR) - data.pop(ATTR_XY_COLOR) mock_call.assert_called_once_with( LIGHT_DOMAIN, SERVICE_TURN_ON, data, blocking=True, context=None ) diff --git a/tests/components/light/test_init.py b/tests/components/light/test_init.py index 752de867542..ef781b56a56 100644 --- a/tests/components/light/test_init.py +++ b/tests/components/light/test_init.py @@ -748,9 +748,9 @@ async def test_light_brightness_step(hass): ) _, data = entity0.last_call("turn_on") - assert data["brightness"] == 126 # 100 + (255 * 0.10) + assert data["brightness"] == 116 # 90 + (255 * 0.10) _, data = entity1.last_call("turn_on") - assert data["brightness"] == 76 # 50 + (255 * 0.10) + assert data["brightness"] == 66 # 40 + (255 * 0.10) async def test_light_brightness_pct_conversion(hass): diff --git a/tests/components/mqtt/test_fan.py b/tests/components/mqtt/test_fan.py index bfa1f387bcd..ee12a7ce03c 100644 --- a/tests/components/mqtt/test_fan.py +++ b/tests/components/mqtt/test_fan.py @@ -408,6 +408,10 @@ async def test_controlling_state_via_topic_and_json_message(hass, mqtt_mock, cap state = hass.states.get("fan.test") assert state.attributes.get(fan.ATTR_PERCENTAGE) == 100 + async_fire_mqtt_message(hass, "percentage-state-topic", '{"otherval": 100}') + assert "Ignoring empty speed from" in caplog.text + caplog.clear() + async_fire_mqtt_message(hass, "preset-mode-state-topic", '{"val": "low"}') assert "not a valid preset mode" in caplog.text caplog.clear() @@ -424,6 +428,99 @@ async def test_controlling_state_via_topic_and_json_message(hass, mqtt_mock, cap state = hass.states.get("fan.test") assert state.attributes.get("preset_mode") == "silent" + async_fire_mqtt_message(hass, "preset-mode-state-topic", '{"otherval": 100}') + assert "Ignoring empty preset_mode from" in caplog.text + caplog.clear() + + +async def test_controlling_state_via_topic_and_json_message_shared_topic( + hass, mqtt_mock, caplog +): + """Test the controlling state via topic and JSON message using a shared topic.""" + assert await async_setup_component( + hass, + fan.DOMAIN, + { + fan.DOMAIN: { + "platform": "mqtt", + "name": "test", + "state_topic": "shared-state-topic", + "command_topic": "command-topic", + "oscillation_state_topic": "shared-state-topic", + "oscillation_command_topic": "oscillation-command-topic", + "percentage_state_topic": "shared-state-topic", + "percentage_command_topic": "percentage-command-topic", + "preset_mode_state_topic": "shared-state-topic", + "preset_mode_command_topic": "preset-mode-command-topic", + "preset_modes": [ + "auto", + "smart", + "whoosh", + "eco", + "breeze", + "silent", + ], + "state_value_template": "{{ value_json.state }}", + "oscillation_value_template": "{{ value_json.oscillation }}", + "percentage_value_template": "{{ value_json.percentage }}", + "preset_mode_value_template": "{{ value_json.preset_mode }}", + "speed_range_min": 1, + "speed_range_max": 100, + } + }, + ) + await hass.async_block_till_done() + + state = hass.states.get("fan.test") + assert state.state == STATE_OFF + assert not state.attributes.get(ATTR_ASSUMED_STATE) + + async_fire_mqtt_message( + hass, + "shared-state-topic", + '{"state":"ON","preset_mode":"eco","oscillation":"oscillate_on","percentage": 50}', + ) + state = hass.states.get("fan.test") + assert state.state == STATE_ON + assert state.attributes.get("oscillating") is True + assert state.attributes.get(fan.ATTR_PERCENTAGE) == 50 + assert state.attributes.get("preset_mode") == "eco" + + async_fire_mqtt_message( + hass, + "shared-state-topic", + '{"state":"ON","preset_mode":"auto","oscillation":"oscillate_off","percentage": 10}', + ) + state = hass.states.get("fan.test") + assert state.state == STATE_ON + assert state.attributes.get("oscillating") is False + assert state.attributes.get(fan.ATTR_PERCENTAGE) == 10 + assert state.attributes.get("preset_mode") == "auto" + + async_fire_mqtt_message( + hass, + "shared-state-topic", + '{"state":"OFF","preset_mode":"auto","oscillation":"oscillate_off","percentage": 0}', + ) + state = hass.states.get("fan.test") + assert state.state == STATE_OFF + assert state.attributes.get("oscillating") is False + assert state.attributes.get(fan.ATTR_PERCENTAGE) == 0 + assert state.attributes.get("preset_mode") == "auto" + + async_fire_mqtt_message( + hass, + "shared-state-topic", + '{"percentage": 100}', + ) + state = hass.states.get("fan.test") + assert state.attributes.get(fan.ATTR_PERCENTAGE) == 100 + assert state.attributes.get("preset_mode") == "auto" + assert "Ignoring empty preset_mode from" in caplog.text + assert "Ignoring empty state from" in caplog.text + assert "Ignoring empty oscillation from" in caplog.text + caplog.clear() + async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock, caplog): """Test optimistic mode without state topic.""" diff --git a/tests/components/mysensors/test_config_flow.py b/tests/components/mysensors/test_config_flow.py index 66900066cd1..161d00e44b3 100644 --- a/tests/components/mysensors/test_config_flow.py +++ b/tests/components/mysensors/test_config_flow.py @@ -380,6 +380,9 @@ async def test_config_invalid( with patch( "homeassistant.components.mysensors.config_flow.try_connect", return_value=True + ), patch( + "homeassistant.components.mysensors.gateway.socket.getaddrinfo", + side_effect=OSError, ), patch( "homeassistant.components.mysensors.async_setup", return_value=True ) as mock_setup, patch( diff --git a/tests/components/mysensors/test_init.py b/tests/components/mysensors/test_init.py index 4fb51d6c17a..05e3df76285 100644 --- a/tests/components/mysensors/test_init.py +++ b/tests/components/mysensors/test_init.py @@ -32,7 +32,7 @@ from homeassistant.setup import async_setup_component @pytest.mark.parametrize( - "config, expected_calls, expected_to_succeed, expected_config_flow_user_input", + "config, expected_calls, expected_to_succeed, expected_config_entry_data", [ ( { @@ -52,13 +52,19 @@ from homeassistant.setup import async_setup_component }, 1, True, - { - CONF_GATEWAY_TYPE: CONF_GATEWAY_TYPE_SERIAL, - CONF_DEVICE: "COM5", - CONF_PERSISTENCE_FILE: "bla.json", - CONF_BAUD_RATE: 57600, - CONF_VERSION: "2.3", - }, + [ + { + CONF_GATEWAY_TYPE: CONF_GATEWAY_TYPE_SERIAL, + CONF_DEVICE: "COM5", + CONF_PERSISTENCE_FILE: "bla.json", + CONF_BAUD_RATE: 57600, + CONF_VERSION: "2.3", + CONF_TCP_PORT: 5003, + CONF_TOPIC_IN_PREFIX: "", + CONF_TOPIC_OUT_PREFIX: "", + CONF_RETAIN: True, + } + ], ), ( { @@ -78,13 +84,19 @@ from homeassistant.setup import async_setup_component }, 1, True, - { - CONF_GATEWAY_TYPE: CONF_GATEWAY_TYPE_TCP, - CONF_DEVICE: "127.0.0.1", - CONF_PERSISTENCE_FILE: "blub.pickle", - CONF_TCP_PORT: 343, - CONF_VERSION: "2.4", - }, + [ + { + CONF_GATEWAY_TYPE: CONF_GATEWAY_TYPE_TCP, + CONF_DEVICE: "127.0.0.1", + CONF_PERSISTENCE_FILE: "blub.pickle", + CONF_TCP_PORT: 343, + CONF_VERSION: "2.4", + CONF_BAUD_RATE: 115200, + CONF_TOPIC_IN_PREFIX: "", + CONF_TOPIC_OUT_PREFIX: "", + CONF_RETAIN: False, + } + ], ), ( { @@ -100,12 +112,19 @@ from homeassistant.setup import async_setup_component }, 1, True, - { - CONF_GATEWAY_TYPE: CONF_GATEWAY_TYPE_TCP, - CONF_DEVICE: "127.0.0.1", - CONF_TCP_PORT: 5003, - CONF_VERSION: DEFAULT_VERSION, - }, + [ + { + CONF_GATEWAY_TYPE: CONF_GATEWAY_TYPE_TCP, + CONF_DEVICE: "127.0.0.1", + CONF_TCP_PORT: 5003, + CONF_VERSION: DEFAULT_VERSION, + CONF_BAUD_RATE: 115200, + CONF_TOPIC_IN_PREFIX: "", + CONF_TOPIC_OUT_PREFIX: "", + CONF_RETAIN: False, + CONF_PERSISTENCE_FILE: "mysensors1.pickle", + } + ], ), ( { @@ -125,13 +144,19 @@ from homeassistant.setup import async_setup_component }, 1, True, - { - CONF_GATEWAY_TYPE: CONF_GATEWAY_TYPE_MQTT, - CONF_DEVICE: "mqtt", - CONF_VERSION: DEFAULT_VERSION, - CONF_TOPIC_OUT_PREFIX: "outtopic", - CONF_TOPIC_IN_PREFIX: "intopic", - }, + [ + { + CONF_GATEWAY_TYPE: CONF_GATEWAY_TYPE_MQTT, + CONF_DEVICE: "mqtt", + CONF_VERSION: DEFAULT_VERSION, + CONF_BAUD_RATE: 115200, + CONF_TCP_PORT: 5003, + CONF_TOPIC_OUT_PREFIX: "outtopic", + CONF_TOPIC_IN_PREFIX: "intopic", + CONF_RETAIN: False, + CONF_PERSISTENCE_FILE: "mysensors1.pickle", + } + ], ), ( { @@ -149,7 +174,7 @@ from homeassistant.setup import async_setup_component }, 0, True, - {}, + [{}], ), ( { @@ -177,7 +202,30 @@ from homeassistant.setup import async_setup_component }, 2, True, - {}, + [ + { + CONF_DEVICE: "mqtt", + CONF_PERSISTENCE_FILE: "bla.json", + CONF_TOPIC_OUT_PREFIX: "out", + CONF_TOPIC_IN_PREFIX: "in", + CONF_BAUD_RATE: 115200, + CONF_TCP_PORT: 5003, + CONF_VERSION: "2.4", + CONF_RETAIN: False, + CONF_GATEWAY_TYPE: CONF_GATEWAY_TYPE_MQTT, + }, + { + CONF_DEVICE: "COM6", + CONF_PERSISTENCE_FILE: "bla2.json", + CONF_TOPIC_OUT_PREFIX: "", + CONF_TOPIC_IN_PREFIX: "", + CONF_BAUD_RATE: 115200, + CONF_TCP_PORT: 5003, + CONF_VERSION: "2.4", + CONF_RETAIN: False, + CONF_GATEWAY_TYPE: CONF_GATEWAY_TYPE_SERIAL, + }, + ], ), ( { @@ -203,7 +251,7 @@ from homeassistant.setup import async_setup_component }, 0, False, - {}, + [{}], ), ( { @@ -223,7 +271,47 @@ from homeassistant.setup import async_setup_component }, 0, True, - {}, + [{}], + ), + ( + { + DOMAIN: { + CONF_GATEWAYS: [ + { + CONF_DEVICE: "COM1", + }, + { + CONF_DEVICE: "COM2", + }, + ], + } + }, + 2, + True, + [ + { + CONF_DEVICE: "COM1", + CONF_PERSISTENCE_FILE: "mysensors1.pickle", + CONF_TOPIC_OUT_PREFIX: "", + CONF_TOPIC_IN_PREFIX: "", + CONF_BAUD_RATE: 115200, + CONF_TCP_PORT: 5003, + CONF_VERSION: "1.4", + CONF_RETAIN: True, + CONF_GATEWAY_TYPE: CONF_GATEWAY_TYPE_SERIAL, + }, + { + CONF_DEVICE: "COM2", + CONF_PERSISTENCE_FILE: "mysensors2.pickle", + CONF_TOPIC_OUT_PREFIX: "", + CONF_TOPIC_IN_PREFIX: "", + CONF_BAUD_RATE: 115200, + CONF_TCP_PORT: 5003, + CONF_VERSION: "1.4", + CONF_RETAIN: True, + CONF_GATEWAY_TYPE: CONF_GATEWAY_TYPE_SERIAL, + }, + ], ), ], ) @@ -233,7 +321,7 @@ async def test_import( config: ConfigType, expected_calls: int, expected_to_succeed: bool, - expected_config_flow_user_input: dict[str, Any], + expected_config_entry_data: list[dict[str, Any]], ) -> None: """Test importing a gateway.""" await async_setup_component(hass, "persistent_notification", {}) @@ -249,8 +337,13 @@ async def test_import( assert len(mock_setup_entry.mock_calls) == expected_calls - if expected_calls > 0: - config_flow_user_input = mock_setup_entry.mock_calls[0][1][1].data - for key, value in expected_config_flow_user_input.items(): - assert key in config_flow_user_input - assert config_flow_user_input[key] == value + for idx in range(expected_calls): + config_entry = mock_setup_entry.mock_calls[idx][1][1] + expected_persistence_file = expected_config_entry_data[idx].pop( + CONF_PERSISTENCE_FILE + ) + expected_persistence_path = hass.config.path(expected_persistence_file) + config_entry_data = dict(config_entry.data) + persistence_path = config_entry_data.pop(CONF_PERSISTENCE_FILE) + assert persistence_path == expected_persistence_path + assert config_entry_data == expected_config_entry_data[idx] diff --git a/tests/components/onewire/test_init.py b/tests/components/onewire/test_init.py index 21e512a2229..44e8d15bff6 100644 --- a/tests/components/onewire/test_init.py +++ b/tests/components/onewire/test_init.py @@ -36,7 +36,6 @@ async def test_owserver_connect_failure(hass): CONF_HOST: "1.2.3.4", CONF_PORT: "1234", }, - unique_id=f"{CONF_TYPE_OWSERVER}:1.2.3.4:1234", connection_class=CONN_CLASS_LOCAL_POLL, options={}, entry_id="2", @@ -65,7 +64,6 @@ async def test_failed_owserver_listing(hass): CONF_HOST: "1.2.3.4", CONF_PORT: "1234", }, - unique_id=f"{CONF_TYPE_OWSERVER}:1.2.3.4:1234", connection_class=CONN_CLASS_LOCAL_POLL, options={}, entry_id="2", diff --git a/tests/components/zwave_js/test_api.py b/tests/components/zwave_js/test_api.py index 0a88a8e02ff..57961ee89e4 100644 --- a/tests/components/zwave_js/test_api.py +++ b/tests/components/zwave_js/test_api.py @@ -12,6 +12,7 @@ from homeassistant.components.zwave_js.api import ( CONFIG, ENABLED, ENTRY_ID, + ERR_NOT_LOADED, FILENAME, FORCE_CONSOLE, ID, @@ -31,8 +32,8 @@ from homeassistant.components.zwave_js.const import ( from homeassistant.helpers import device_registry as dr -async def test_websocket_api(hass, integration, multisensor_6, hass_ws_client): - """Test the network and node status websocket commands.""" +async def test_network_status(hass, integration, hass_ws_client): + """Test the network status websocket command.""" entry = integration ws_client = await hass_ws_client(hass) @@ -45,6 +46,24 @@ async def test_websocket_api(hass, integration, multisensor_6, hass_ws_client): assert result["client"]["ws_server_url"] == "ws://test:3000/zjs" assert result["client"]["server_version"] == "1.0.0" + # Test sending command with not loaded entry fails + await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + await ws_client.send_json( + {ID: 3, TYPE: "zwave_js/network_status", ENTRY_ID: entry.entry_id} + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == ERR_NOT_LOADED + + +async def test_node_status(hass, integration, multisensor_6, hass_ws_client): + """Test the node status websocket command.""" + entry = integration + ws_client = await hass_ws_client(hass) + node = multisensor_6 await ws_client.send_json( { @@ -63,33 +82,10 @@ async def test_websocket_api(hass, integration, multisensor_6, hass_ws_client): assert not result["is_secure"] assert result["status"] == 1 - # Test getting configuration parameter values - await ws_client.send_json( - { - ID: 4, - TYPE: "zwave_js/get_config_parameters", - ENTRY_ID: entry.entry_id, - NODE_ID: node.node_id, - } - ) - msg = await ws_client.receive_json() - result = msg["result"] - - assert len(result) == 61 - key = "52-112-0-2" - assert result[key]["property"] == 2 - assert result[key]["property_key"] is None - assert result[key]["metadata"]["type"] == "number" - assert result[key]["configuration_value_type"] == "enumerated" - assert result[key]["metadata"]["states"] - - key = "52-112-0-201-255" - assert result[key]["property_key"] == 255 - # Test getting non-existent node fails await ws_client.send_json( { - ID: 5, + ID: 4, TYPE: "zwave_js/node_status", ENTRY_ID: entry.entry_id, NODE_ID: 99999, @@ -99,18 +95,22 @@ async def test_websocket_api(hass, integration, multisensor_6, hass_ws_client): assert not msg["success"] assert msg["error"]["code"] == ERR_NOT_FOUND - # Test getting non-existent node config params fails + # Test sending command with not loaded entry fails + await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + await ws_client.send_json( { - ID: 6, - TYPE: "zwave_js/get_config_parameters", + ID: 5, + TYPE: "zwave_js/node_status", ENTRY_ID: entry.entry_id, - NODE_ID: 99999, + NODE_ID: node.node_id, } ) msg = await ws_client.receive_json() + assert not msg["success"] - assert msg["error"]["code"] == ERR_NOT_FOUND + assert msg["error"]["code"] == ERR_NOT_LOADED async def test_add_node( @@ -145,6 +145,29 @@ async def test_add_node( client.driver.receive_event(nortek_thermostat_added_event) msg = await ws_client.receive_json() assert msg["event"]["event"] == "node added" + node_details = { + "node_id": 53, + "status": 0, + "ready": False, + } + assert msg["event"]["node"] == node_details + + msg = await ws_client.receive_json() + assert msg["event"]["event"] == "device registered" + # Check the keys of the device item + assert list(msg["event"]["device"]) == ["name", "id"] + + # Test sending command with not loaded entry fails + await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + await ws_client.send_json( + {ID: 4, TYPE: "zwave_js/add_node", ENTRY_ID: entry.entry_id} + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == ERR_NOT_LOADED async def test_cancel_inclusion_exclusion(hass, integration, client, hass_ws_client): @@ -168,6 +191,26 @@ async def test_cancel_inclusion_exclusion(hass, integration, client, hass_ws_cli msg = await ws_client.receive_json() assert msg["success"] + # Test sending command with not loaded entry fails + await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + await ws_client.send_json( + {ID: 6, TYPE: "zwave_js/stop_inclusion", ENTRY_ID: entry.entry_id} + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == ERR_NOT_LOADED + + await ws_client.send_json( + {ID: 7, TYPE: "zwave_js/stop_exclusion", ENTRY_ID: entry.entry_id} + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == ERR_NOT_LOADED + async def test_remove_node( hass, @@ -226,6 +269,18 @@ async def test_remove_node( ) assert device is None + # Test sending command with not loaded entry fails + await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + await ws_client.send_json( + {ID: 4, TYPE: "zwave_js/remove_node", ENTRY_ID: entry.entry_id} + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == ERR_NOT_LOADED + async def test_refresh_node_info( hass, client, integration, hass_ws_client, multisensor_6 @@ -295,6 +350,36 @@ async def test_refresh_node_info( client.async_send_command_no_wait.reset_mock() + # Test getting non-existent node fails + await ws_client.send_json( + { + ID: 2, + TYPE: "zwave_js/refresh_node_info", + ENTRY_ID: entry.entry_id, + NODE_ID: 9999, + } + ) + msg = await ws_client.receive_json() + assert not msg["success"] + assert msg["error"]["code"] == ERR_NOT_FOUND + + # Test sending command with not loaded entry fails + await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + await ws_client.send_json( + { + ID: 3, + TYPE: "zwave_js/refresh_node_info", + ENTRY_ID: entry.entry_id, + NODE_ID: 52, + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == ERR_NOT_LOADED + async def test_refresh_node_values( hass, client, integration, hass_ws_client, multisensor_6 @@ -391,6 +476,38 @@ async def test_refresh_node_cc_values( assert not msg["success"] assert msg["error"]["code"] == ERR_NOT_FOUND + # Test getting non-existent node fails + await ws_client.send_json( + { + ID: 3, + TYPE: "zwave_js/refresh_node_cc_values", + ENTRY_ID: entry.entry_id, + NODE_ID: 9999, + COMMAND_CLASS_ID: 112, + } + ) + msg = await ws_client.receive_json() + assert not msg["success"] + assert msg["error"]["code"] == ERR_NOT_FOUND + + # Test sending command with not loaded entry fails + await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + await ws_client.send_json( + { + ID: 4, + TYPE: "zwave_js/refresh_node_cc_values", + ENTRY_ID: entry.entry_id, + NODE_ID: 52, + COMMAND_CLASS_ID: 112, + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == ERR_NOT_LOADED + async def test_set_config_parameter( hass, client, hass_ws_client, multisensor_6, integration @@ -510,6 +627,103 @@ async def test_set_config_parameter( assert msg["error"]["code"] == "unknown_error" assert msg["error"]["message"] == "test" + # Test getting non-existent node fails + await ws_client.send_json( + { + ID: 5, + TYPE: "zwave_js/set_config_parameter", + ENTRY_ID: entry.entry_id, + NODE_ID: 9999, + PROPERTY: 102, + PROPERTY_KEY: 1, + VALUE: 1, + } + ) + msg = await ws_client.receive_json() + assert not msg["success"] + assert msg["error"]["code"] == ERR_NOT_FOUND + + # Test sending command with not loaded entry fails + await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + await ws_client.send_json( + { + ID: 6, + TYPE: "zwave_js/set_config_parameter", + ENTRY_ID: entry.entry_id, + NODE_ID: 52, + PROPERTY: 102, + PROPERTY_KEY: 1, + VALUE: 1, + } + ) + + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == ERR_NOT_LOADED + + +async def test_get_config_parameters(hass, integration, multisensor_6, hass_ws_client): + """Test the get config parameters websocket command.""" + entry = integration + ws_client = await hass_ws_client(hass) + node = multisensor_6 + + # Test getting configuration parameter values + await ws_client.send_json( + { + ID: 4, + TYPE: "zwave_js/get_config_parameters", + ENTRY_ID: entry.entry_id, + NODE_ID: node.node_id, + } + ) + msg = await ws_client.receive_json() + result = msg["result"] + + assert len(result) == 61 + key = "52-112-0-2" + assert result[key]["property"] == 2 + assert result[key]["property_key"] is None + assert result[key]["metadata"]["type"] == "number" + assert result[key]["configuration_value_type"] == "enumerated" + assert result[key]["metadata"]["states"] + + key = "52-112-0-201-255" + assert result[key]["property_key"] == 255 + + # Test getting non-existent node config params fails + await ws_client.send_json( + { + ID: 5, + TYPE: "zwave_js/get_config_parameters", + ENTRY_ID: entry.entry_id, + NODE_ID: 99999, + } + ) + msg = await ws_client.receive_json() + assert not msg["success"] + assert msg["error"]["code"] == ERR_NOT_FOUND + + # Test sending command with not loaded entry fails + await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + await ws_client.send_json( + { + ID: 6, + TYPE: "zwave_js/get_config_parameters", + ENTRY_ID: entry.entry_id, + NODE_ID: node.node_id, + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == ERR_NOT_LOADED + async def test_dump_view(integration, hass_client): """Test the HTTP dump view.""" @@ -571,6 +785,18 @@ async def test_subscribe_logs(hass, integration, client, hass_ws_client): "timestamp": "time", } + # Test sending command with not loaded entry fails + await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + await ws_client.send_json( + {ID: 2, TYPE: "zwave_js/subscribe_logs", ENTRY_ID: entry.entry_id} + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == ERR_NOT_LOADED + async def test_update_log_config(hass, client, integration, hass_ws_client): """Test that the update_log_config WS API call works and that schema validation works.""" @@ -691,6 +917,23 @@ async def test_update_log_config(hass, client, integration, hass_ws_client): and "must be provided if logging to file" in msg["error"]["message"] ) + # Test sending command with not loaded entry fails + await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + await ws_client.send_json( + { + ID: 7, + TYPE: "zwave_js/update_log_config", + ENTRY_ID: entry.entry_id, + CONFIG: {LEVEL: "Error"}, + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == ERR_NOT_LOADED + async def test_get_log_config(hass, client, integration, hass_ws_client): """Test that the get_log_config WS API call works.""" @@ -726,6 +969,22 @@ async def test_get_log_config(hass, client, integration, hass_ws_client): assert log_config["filename"] == "/test.txt" assert log_config["force_console"] is False + # Test sending command with not loaded entry fails + await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + await ws_client.send_json( + { + ID: 2, + TYPE: "zwave_js/get_log_config", + ENTRY_ID: entry.entry_id, + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == ERR_NOT_LOADED + async def test_data_collection(hass, client, integration, hass_ws_client): """Test that the data collection WS API commands work.""" @@ -794,3 +1053,32 @@ async def test_data_collection(hass, client, integration, hass_ws_client): assert not entry.data[CONF_DATA_COLLECTION_OPTED_IN] client.async_send_command.reset_mock() + + # Test sending command with not loaded entry fails + await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + await ws_client.send_json( + { + ID: 4, + TYPE: "zwave_js/data_collection_status", + ENTRY_ID: entry.entry_id, + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == ERR_NOT_LOADED + + await ws_client.send_json( + { + ID: 5, + TYPE: "zwave_js/update_data_collection_preference", + ENTRY_ID: entry.entry_id, + OPTED_IN: True, + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == ERR_NOT_LOADED diff --git a/tests/testing_config/custom_components/test/light.py b/tests/testing_config/custom_components/test/light.py index 84008d90c27..88ce04bdc92 100644 --- a/tests/testing_config/custom_components/test/light.py +++ b/tests/testing_config/custom_components/test/light.py @@ -36,18 +36,33 @@ async def async_setup_platform( class MockLight(MockToggleEntity, LightEntity): """Mock light class.""" - brightness = None + color_mode = None + max_mireds = 500 + min_mireds = 153 supported_color_modes = None supported_features = 0 - color_mode = None - + brightness = None + color_temp = None hs_color = None - xy_color = None rgb_color = None rgbw_color = None rgbww_color = None - - color_temp = None - + xy_color = None white_value = None + + def turn_on(self, **kwargs): + """Turn the entity on.""" + super().turn_on(**kwargs) + for key, value in kwargs.items(): + if key in [ + "brightness", + "hs_color", + "xy_color", + "rgb_color", + "rgbw_color", + "rgbww_color", + "color_temp", + "white_value", + ]: + setattr(self, key, value)