Skip to content

Commit ad4e545

Browse files
authored
Fix - only enable AlexaModeController if at least one mode is offered (home-assistant#148614)
1 parent 334d5f0 commit ad4e545

File tree

2 files changed

+183
-5
lines changed

2 files changed

+183
-5
lines changed

homeassistant/components/alexa/entities.py

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -505,8 +505,13 @@ def interfaces(self) -> Generator[AlexaCapability]:
505505
):
506506
yield AlexaThermostatController(self.hass, self.entity)
507507
yield AlexaTemperatureSensor(self.hass, self.entity)
508-
if self.entity.domain == water_heater.DOMAIN and (
509-
supported_features & water_heater.WaterHeaterEntityFeature.OPERATION_MODE
508+
if (
509+
self.entity.domain == water_heater.DOMAIN
510+
and (
511+
supported_features
512+
& water_heater.WaterHeaterEntityFeature.OPERATION_MODE
513+
)
514+
and self.entity.attributes.get(water_heater.ATTR_OPERATION_LIST)
510515
):
511516
yield AlexaModeController(
512517
self.entity,
@@ -634,7 +639,9 @@ def interfaces(self) -> Generator[AlexaCapability]:
634639
self.entity, instance=f"{fan.DOMAIN}.{fan.ATTR_OSCILLATING}"
635640
)
636641
force_range_controller = False
637-
if supported & fan.FanEntityFeature.PRESET_MODE:
642+
if supported & fan.FanEntityFeature.PRESET_MODE and self.entity.attributes.get(
643+
fan.ATTR_PRESET_MODES
644+
):
638645
yield AlexaModeController(
639646
self.entity, instance=f"{fan.DOMAIN}.{fan.ATTR_PRESET_MODE}"
640647
)
@@ -672,7 +679,11 @@ def interfaces(self) -> Generator[AlexaCapability]:
672679
yield AlexaPowerController(self.entity)
673680
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
674681
activities = self.entity.attributes.get(remote.ATTR_ACTIVITY_LIST) or []
675-
if activities and supported & remote.RemoteEntityFeature.ACTIVITY:
682+
if (
683+
activities
684+
and (supported & remote.RemoteEntityFeature.ACTIVITY)
685+
and self.entity.attributes.get(remote.ATTR_ACTIVITY_LIST)
686+
):
676687
yield AlexaModeController(
677688
self.entity, instance=f"{remote.DOMAIN}.{remote.ATTR_ACTIVITY}"
678689
)
@@ -692,7 +703,9 @@ def interfaces(self) -> Generator[AlexaCapability]:
692703
"""Yield the supported interfaces."""
693704
yield AlexaPowerController(self.entity)
694705
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
695-
if supported & humidifier.HumidifierEntityFeature.MODES:
706+
if (
707+
supported & humidifier.HumidifierEntityFeature.MODES
708+
) and self.entity.attributes.get(humidifier.ATTR_AVAILABLE_MODES):
696709
yield AlexaModeController(
697710
self.entity, instance=f"{humidifier.DOMAIN}.{humidifier.ATTR_MODE}"
698711
)

tests/components/alexa/test_entities.py

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import pytest
77

8+
from homeassistant.components import fan, humidifier, remote, water_heater
89
from homeassistant.components.alexa import smart_home
910
from homeassistant.const import EntityCategory, UnitOfTemperature, __version__
1011
from homeassistant.core import HomeAssistant
@@ -200,3 +201,167 @@ async def test_serialize_discovery_recovers(
200201
"Error serializing Alexa.PowerController discovery"
201202
f" for {hass.states.get('switch.bla')}"
202203
) in caplog.text
204+
205+
206+
@pytest.mark.parametrize(
207+
("domain", "state", "state_attributes", "mode_controller_exists"),
208+
[
209+
("switch", "on", {}, False),
210+
(
211+
"fan",
212+
"on",
213+
{
214+
"preset_modes": ["eco", "auto"],
215+
"preset_mode": "eco",
216+
"supported_features": fan.FanEntityFeature.PRESET_MODE.value,
217+
},
218+
True,
219+
),
220+
(
221+
"fan",
222+
"on",
223+
{
224+
"preset_modes": ["eco", "auto"],
225+
"preset_mode": None,
226+
"supported_features": fan.FanEntityFeature.PRESET_MODE.value,
227+
},
228+
True,
229+
),
230+
(
231+
"fan",
232+
"on",
233+
{
234+
"preset_modes": ["eco"],
235+
"preset_mode": None,
236+
"supported_features": fan.FanEntityFeature.PRESET_MODE.value,
237+
},
238+
True,
239+
),
240+
(
241+
"fan",
242+
"on",
243+
{
244+
"preset_modes": [],
245+
"preset_mode": None,
246+
"supported_features": fan.FanEntityFeature.PRESET_MODE.value,
247+
},
248+
False,
249+
),
250+
(
251+
"humidifier",
252+
"on",
253+
{
254+
"available_modes": ["auto", "manual"],
255+
"mode": "auto",
256+
"supported_features": humidifier.HumidifierEntityFeature.MODES.value,
257+
},
258+
True,
259+
),
260+
(
261+
"humidifier",
262+
"on",
263+
{
264+
"available_modes": ["auto"],
265+
"mode": None,
266+
"supported_features": humidifier.HumidifierEntityFeature.MODES.value,
267+
},
268+
True,
269+
),
270+
(
271+
"humidifier",
272+
"on",
273+
{
274+
"available_modes": [],
275+
"mode": None,
276+
"supported_features": humidifier.HumidifierEntityFeature.MODES.value,
277+
},
278+
False,
279+
),
280+
(
281+
"remote",
282+
"on",
283+
{
284+
"activity_list": ["tv", "dvd"],
285+
"current_activity": "tv",
286+
"supported_features": remote.RemoteEntityFeature.ACTIVITY.value,
287+
},
288+
True,
289+
),
290+
(
291+
"remote",
292+
"on",
293+
{
294+
"activity_list": ["tv"],
295+
"current_activity": None,
296+
"supported_features": remote.RemoteEntityFeature.ACTIVITY.value,
297+
},
298+
True,
299+
),
300+
(
301+
"remote",
302+
"on",
303+
{
304+
"activity_list": [],
305+
"current_activity": None,
306+
"supported_features": remote.RemoteEntityFeature.ACTIVITY.value,
307+
},
308+
False,
309+
),
310+
(
311+
"water_heater",
312+
"on",
313+
{
314+
"operation_list": ["on", "auto"],
315+
"operation_mode": "auto",
316+
"supported_features": water_heater.WaterHeaterEntityFeature.OPERATION_MODE.value,
317+
},
318+
True,
319+
),
320+
(
321+
"water_heater",
322+
"on",
323+
{
324+
"operation_list": ["on"],
325+
"operation_mode": None,
326+
"supported_features": water_heater.WaterHeaterEntityFeature.OPERATION_MODE.value,
327+
},
328+
True,
329+
),
330+
(
331+
"water_heater",
332+
"on",
333+
{
334+
"operation_list": [],
335+
"operation_mode": None,
336+
"supported_features": water_heater.WaterHeaterEntityFeature.OPERATION_MODE.value,
337+
},
338+
False,
339+
),
340+
],
341+
)
342+
async def test_mode_controller_is_omitted_if_no_modes_are_set(
343+
hass: HomeAssistant,
344+
domain: str,
345+
state: str,
346+
state_attributes: dict[str, Any],
347+
mode_controller_exists: bool,
348+
) -> None:
349+
"""Test we do not generate an invalid discovery with AlexaModeController during serialize discovery.
350+
351+
AlexModeControllers need at least 2 modes. If one mode is set, an extra mode will be added for compatibility.
352+
If no modes are offered, the mode controller should be omitted to prevent schema validations.
353+
"""
354+
request = get_new_request("Alexa.Discovery", "Discover")
355+
356+
hass.states.async_set(
357+
f"{domain}.bla", state, {"friendly_name": "Boop Woz"} | state_attributes
358+
)
359+
360+
msg = await smart_home.async_handle_message(hass, get_default_config(hass), request)
361+
msg = msg["event"]
362+
363+
interfaces = {
364+
ifc["interface"] for ifc in msg["payload"]["endpoints"][0]["capabilities"]
365+
}
366+
367+
assert ("Alexa.ModeController" in interfaces) is mode_controller_exists

0 commit comments

Comments
 (0)