mirror of https://github.com/home-assistant/core
4159 lines
128 KiB
Python
4159 lines
128 KiB
Python
"""Tests for the Google Assistant traits."""
|
|
|
|
from datetime import datetime, timedelta
|
|
from typing import Any
|
|
from unittest.mock import ANY, patch
|
|
|
|
from freezegun.api import FrozenDateTimeFactory
|
|
import pytest
|
|
|
|
from homeassistant.components import (
|
|
alarm_control_panel,
|
|
binary_sensor,
|
|
button,
|
|
camera,
|
|
climate,
|
|
cover,
|
|
event,
|
|
fan,
|
|
group,
|
|
humidifier,
|
|
input_boolean,
|
|
input_button,
|
|
input_select,
|
|
light,
|
|
lock,
|
|
media_player,
|
|
scene,
|
|
script,
|
|
select,
|
|
sensor,
|
|
switch,
|
|
vacuum,
|
|
valve,
|
|
water_heater,
|
|
)
|
|
from homeassistant.components.alarm_control_panel import (
|
|
AlarmControlPanelEntityFeature,
|
|
AlarmControlPanelState,
|
|
)
|
|
from homeassistant.components.camera import CameraEntityFeature
|
|
from homeassistant.components.climate import ClimateEntityFeature
|
|
from homeassistant.components.cover import CoverEntityFeature
|
|
from homeassistant.components.fan import FanEntityFeature
|
|
from homeassistant.components.google_assistant import const, error, helpers, trait
|
|
from homeassistant.components.google_assistant.error import SmartHomeError
|
|
from homeassistant.components.humidifier import HumidifierEntityFeature
|
|
from homeassistant.components.light import LightEntityFeature
|
|
from homeassistant.components.lock import LockEntityFeature
|
|
from homeassistant.components.media_player import (
|
|
SERVICE_PLAY_MEDIA,
|
|
MediaPlayerEntityFeature,
|
|
MediaType,
|
|
)
|
|
from homeassistant.components.vacuum import VacuumEntityFeature
|
|
from homeassistant.components.valve import ValveEntityFeature
|
|
from homeassistant.components.water_heater import WaterHeaterEntityFeature
|
|
from homeassistant.const import (
|
|
ATTR_ASSUMED_STATE,
|
|
ATTR_BATTERY_LEVEL,
|
|
ATTR_DEVICE_CLASS,
|
|
ATTR_ENTITY_ID,
|
|
ATTR_MODE,
|
|
ATTR_SUPPORTED_FEATURES,
|
|
ATTR_TEMPERATURE,
|
|
EVENT_CALL_SERVICE,
|
|
SERVICE_TURN_OFF,
|
|
SERVICE_TURN_ON,
|
|
STATE_IDLE,
|
|
STATE_OFF,
|
|
STATE_ON,
|
|
STATE_PAUSED,
|
|
STATE_PLAYING,
|
|
STATE_STANDBY,
|
|
STATE_UNAVAILABLE,
|
|
STATE_UNKNOWN,
|
|
UnitOfTemperature,
|
|
)
|
|
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant, State
|
|
from homeassistant.core_config import async_process_ha_core_config
|
|
from homeassistant.util import color, dt as dt_util
|
|
from homeassistant.util.unit_conversion import TemperatureConverter
|
|
|
|
from . import BASIC_CONFIG, MockConfig
|
|
|
|
from tests.common import async_capture_events, async_mock_service
|
|
|
|
REQ_ID = "ff36a3cc-ec34-11e6-b1a0-64510650abcf"
|
|
|
|
BASIC_DATA = helpers.RequestData(
|
|
BASIC_CONFIG, "test-agent", const.SOURCE_CLOUD, REQ_ID, None
|
|
)
|
|
|
|
PIN_CONFIG = MockConfig(secure_devices_pin="1234")
|
|
|
|
PIN_DATA = helpers.RequestData(
|
|
PIN_CONFIG, "test-agent", const.SOURCE_CLOUD, REQ_ID, None
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"supported_color_modes", [["brightness"], ["hs"], ["color_temp"]]
|
|
)
|
|
async def test_brightness_light(hass: HomeAssistant, supported_color_modes) -> None:
|
|
"""Test brightness trait support for light domain."""
|
|
assert helpers.get_google_type(light.DOMAIN, None) is not None
|
|
assert trait.BrightnessTrait.supported(
|
|
light.DOMAIN, 0, None, {"supported_color_modes": supported_color_modes}
|
|
)
|
|
|
|
trt = trait.BrightnessTrait(
|
|
hass,
|
|
State("light.bla", light.STATE_ON, {light.ATTR_BRIGHTNESS: 243}),
|
|
BASIC_CONFIG,
|
|
)
|
|
|
|
assert trt.sync_attributes() == {}
|
|
|
|
assert trt.query_attributes() == {"brightness": 95}
|
|
|
|
events = async_capture_events(hass, EVENT_CALL_SERVICE)
|
|
|
|
calls = async_mock_service(hass, light.DOMAIN, light.SERVICE_TURN_ON)
|
|
await trt.execute(
|
|
trait.COMMAND_BRIGHTNESS_ABSOLUTE, BASIC_DATA, {"brightness": 50}, {}
|
|
)
|
|
await hass.async_block_till_done()
|
|
|
|
assert len(calls) == 1
|
|
assert calls[0].data == {ATTR_ENTITY_ID: "light.bla", light.ATTR_BRIGHTNESS_PCT: 50}
|
|
|
|
assert len(events) == 1
|
|
assert events[0].data == {
|
|
"domain": "light",
|
|
"service": "turn_on",
|
|
"service_data": {"brightness_pct": 50, "entity_id": "light.bla"},
|
|
}
|
|
|
|
|
|
async def test_camera_stream(hass: HomeAssistant) -> None:
|
|
"""Test camera stream trait support for camera domain."""
|
|
await async_process_ha_core_config(
|
|
hass,
|
|
{"external_url": "https://example.com"},
|
|
)
|
|
assert helpers.get_google_type(camera.DOMAIN, None) is not None
|
|
assert trait.CameraStreamTrait.supported(
|
|
camera.DOMAIN, CameraEntityFeature.STREAM, None, None
|
|
)
|
|
|
|
trt = trait.CameraStreamTrait(
|
|
hass, State("camera.bla", camera.STATE_IDLE, {}), BASIC_CONFIG
|
|
)
|
|
|
|
assert trt.sync_attributes() == {
|
|
"cameraStreamSupportedProtocols": ["hls"],
|
|
"cameraStreamNeedAuthToken": False,
|
|
"cameraStreamNeedDrmEncryption": False,
|
|
}
|
|
|
|
assert trt.query_attributes() == {}
|
|
|
|
with patch(
|
|
"homeassistant.components.camera.async_request_stream",
|
|
return_value="/api/streams/bla",
|
|
):
|
|
await trt.execute(trait.COMMAND_GET_CAMERA_STREAM, BASIC_DATA, {}, {})
|
|
|
|
assert trt.query_attributes() == {
|
|
"cameraStreamAccessUrl": "https://example.com/api/streams/bla",
|
|
"cameraStreamReceiverAppId": "B45F4572",
|
|
}
|
|
|
|
|
|
async def test_onoff_group(hass: HomeAssistant) -> None:
|
|
"""Test OnOff trait support for group domain."""
|
|
assert helpers.get_google_type(group.DOMAIN, None) is not None
|
|
assert trait.OnOffTrait.supported(group.DOMAIN, 0, None, None)
|
|
|
|
trt_on = trait.OnOffTrait(hass, State("group.bla", STATE_ON), BASIC_CONFIG)
|
|
|
|
assert trt_on.sync_attributes() == {}
|
|
|
|
assert trt_on.query_attributes() == {"on": True}
|
|
|
|
trt_off = trait.OnOffTrait(hass, State("group.bla", STATE_OFF), BASIC_CONFIG)
|
|
|
|
assert trt_off.query_attributes() == {"on": False}
|
|
|
|
on_calls = async_mock_service(hass, HOMEASSISTANT_DOMAIN, SERVICE_TURN_ON)
|
|
await trt_on.execute(trait.COMMAND_ON_OFF, BASIC_DATA, {"on": True}, {})
|
|
assert len(on_calls) == 1
|
|
assert on_calls[0].data == {ATTR_ENTITY_ID: "group.bla"}
|
|
|
|
off_calls = async_mock_service(hass, HOMEASSISTANT_DOMAIN, SERVICE_TURN_OFF)
|
|
await trt_on.execute(trait.COMMAND_ON_OFF, BASIC_DATA, {"on": False}, {})
|
|
assert len(off_calls) == 1
|
|
assert off_calls[0].data == {ATTR_ENTITY_ID: "group.bla"}
|
|
|
|
|
|
async def test_onoff_input_boolean(hass: HomeAssistant) -> None:
|
|
"""Test OnOff trait support for input_boolean domain."""
|
|
assert helpers.get_google_type(input_boolean.DOMAIN, None) is not None
|
|
assert trait.OnOffTrait.supported(input_boolean.DOMAIN, 0, None, None)
|
|
|
|
trt_on = trait.OnOffTrait(hass, State("input_boolean.bla", STATE_ON), BASIC_CONFIG)
|
|
|
|
assert trt_on.sync_attributes() == {}
|
|
|
|
assert trt_on.query_attributes() == {"on": True}
|
|
|
|
trt_off = trait.OnOffTrait(
|
|
hass, State("input_boolean.bla", STATE_OFF), BASIC_CONFIG
|
|
)
|
|
|
|
assert trt_off.query_attributes() == {"on": False}
|
|
|
|
on_calls = async_mock_service(hass, input_boolean.DOMAIN, SERVICE_TURN_ON)
|
|
await trt_on.execute(trait.COMMAND_ON_OFF, BASIC_DATA, {"on": True}, {})
|
|
assert len(on_calls) == 1
|
|
assert on_calls[0].data == {ATTR_ENTITY_ID: "input_boolean.bla"}
|
|
|
|
off_calls = async_mock_service(hass, input_boolean.DOMAIN, SERVICE_TURN_OFF)
|
|
await trt_on.execute(trait.COMMAND_ON_OFF, BASIC_DATA, {"on": False}, {})
|
|
assert len(off_calls) == 1
|
|
assert off_calls[0].data == {ATTR_ENTITY_ID: "input_boolean.bla"}
|
|
|
|
|
|
@pytest.mark.freeze_time("2023-08-01T00:02:57+00:00")
|
|
async def test_doorbell_event(hass: HomeAssistant) -> None:
|
|
"""Test doorbell event trait support for event domain."""
|
|
assert trait.ObjectDetection.supported(event.DOMAIN, 0, "doorbell", None)
|
|
|
|
state = State(
|
|
"event.bla",
|
|
"2023-08-01T00:02:57+00:00",
|
|
attributes={"device_class": "doorbell"},
|
|
)
|
|
trt_od = trait.ObjectDetection(hass, state, BASIC_CONFIG)
|
|
|
|
assert not trt_od.sync_attributes()
|
|
assert trt_od.sync_options() == {"notificationSupportedByAgent": True}
|
|
assert not trt_od.query_attributes()
|
|
time_stamp = datetime.fromisoformat(state.state)
|
|
assert trt_od.query_notifications() == {
|
|
"ObjectDetection": {
|
|
"objects": {
|
|
"unclassified": 1,
|
|
},
|
|
"priority": 0,
|
|
"detectionTimestamp": int(time_stamp.timestamp() * 1000),
|
|
}
|
|
}
|
|
|
|
# Test that stale notifications (older than 30 s) are dropped
|
|
state = State(
|
|
"event.bla",
|
|
"2023-08-01T00:02:22+00:00",
|
|
attributes={"device_class": "doorbell"},
|
|
)
|
|
trt_od = trait.ObjectDetection(hass, state, BASIC_CONFIG)
|
|
assert trt_od.query_notifications() is None
|
|
|
|
|
|
async def test_onoff_switch(hass: HomeAssistant) -> None:
|
|
"""Test OnOff trait support for switch domain."""
|
|
assert helpers.get_google_type(switch.DOMAIN, None) is not None
|
|
assert trait.OnOffTrait.supported(switch.DOMAIN, 0, None, None)
|
|
|
|
trt_on = trait.OnOffTrait(hass, State("switch.bla", STATE_ON), BASIC_CONFIG)
|
|
|
|
assert trt_on.sync_attributes() == {}
|
|
|
|
assert trt_on.query_attributes() == {"on": True}
|
|
|
|
trt_off = trait.OnOffTrait(hass, State("switch.bla", STATE_OFF), BASIC_CONFIG)
|
|
|
|
assert trt_off.query_attributes() == {"on": False}
|
|
|
|
trt_assumed = trait.OnOffTrait(
|
|
hass, State("switch.bla", STATE_OFF, {"assumed_state": True}), BASIC_CONFIG
|
|
)
|
|
assert trt_assumed.sync_attributes() == {"commandOnlyOnOff": True}
|
|
|
|
on_calls = async_mock_service(hass, switch.DOMAIN, SERVICE_TURN_ON)
|
|
await trt_on.execute(trait.COMMAND_ON_OFF, BASIC_DATA, {"on": True}, {})
|
|
assert len(on_calls) == 1
|
|
assert on_calls[0].data == {ATTR_ENTITY_ID: "switch.bla"}
|
|
|
|
off_calls = async_mock_service(hass, switch.DOMAIN, SERVICE_TURN_OFF)
|
|
await trt_on.execute(trait.COMMAND_ON_OFF, BASIC_DATA, {"on": False}, {})
|
|
assert len(off_calls) == 1
|
|
assert off_calls[0].data == {ATTR_ENTITY_ID: "switch.bla"}
|
|
|
|
|
|
async def test_onoff_fan(hass: HomeAssistant) -> None:
|
|
"""Test OnOff trait support for fan domain."""
|
|
assert helpers.get_google_type(fan.DOMAIN, None) is not None
|
|
assert trait.OnOffTrait.supported(fan.DOMAIN, 0, None, None)
|
|
|
|
trt_on = trait.OnOffTrait(hass, State("fan.bla", STATE_ON), BASIC_CONFIG)
|
|
|
|
assert trt_on.sync_attributes() == {}
|
|
|
|
assert trt_on.query_attributes() == {"on": True}
|
|
|
|
trt_off = trait.OnOffTrait(hass, State("fan.bla", STATE_OFF), BASIC_CONFIG)
|
|
assert trt_off.query_attributes() == {"on": False}
|
|
|
|
on_calls = async_mock_service(hass, fan.DOMAIN, SERVICE_TURN_ON)
|
|
await trt_on.execute(trait.COMMAND_ON_OFF, BASIC_DATA, {"on": True}, {})
|
|
assert len(on_calls) == 1
|
|
assert on_calls[0].data == {ATTR_ENTITY_ID: "fan.bla"}
|
|
|
|
off_calls = async_mock_service(hass, fan.DOMAIN, SERVICE_TURN_OFF)
|
|
await trt_on.execute(trait.COMMAND_ON_OFF, BASIC_DATA, {"on": False}, {})
|
|
assert len(off_calls) == 1
|
|
assert off_calls[0].data == {ATTR_ENTITY_ID: "fan.bla"}
|
|
|
|
|
|
async def test_onoff_light(hass: HomeAssistant) -> None:
|
|
"""Test OnOff trait support for light domain."""
|
|
assert helpers.get_google_type(light.DOMAIN, None) is not None
|
|
assert trait.OnOffTrait.supported(light.DOMAIN, 0, None, None)
|
|
|
|
trt_on = trait.OnOffTrait(hass, State("light.bla", STATE_ON), BASIC_CONFIG)
|
|
|
|
assert trt_on.sync_attributes() == {}
|
|
|
|
assert trt_on.query_attributes() == {"on": True}
|
|
|
|
trt_off = trait.OnOffTrait(hass, State("light.bla", STATE_OFF), BASIC_CONFIG)
|
|
|
|
assert trt_off.query_attributes() == {"on": False}
|
|
|
|
on_calls = async_mock_service(hass, light.DOMAIN, SERVICE_TURN_ON)
|
|
await trt_on.execute(trait.COMMAND_ON_OFF, BASIC_DATA, {"on": True}, {})
|
|
assert len(on_calls) == 1
|
|
assert on_calls[0].data == {ATTR_ENTITY_ID: "light.bla"}
|
|
|
|
off_calls = async_mock_service(hass, light.DOMAIN, SERVICE_TURN_OFF)
|
|
await trt_on.execute(trait.COMMAND_ON_OFF, BASIC_DATA, {"on": False}, {})
|
|
assert len(off_calls) == 1
|
|
assert off_calls[0].data == {ATTR_ENTITY_ID: "light.bla"}
|
|
|
|
|
|
async def test_onoff_media_player(hass: HomeAssistant) -> None:
|
|
"""Test OnOff trait support for media_player domain."""
|
|
assert helpers.get_google_type(media_player.DOMAIN, None) is not None
|
|
assert trait.OnOffTrait.supported(media_player.DOMAIN, 0, None, None)
|
|
|
|
trt_on = trait.OnOffTrait(hass, State("media_player.bla", STATE_ON), BASIC_CONFIG)
|
|
|
|
assert trt_on.sync_attributes() == {}
|
|
|
|
assert trt_on.query_attributes() == {"on": True}
|
|
|
|
trt_off = trait.OnOffTrait(hass, State("media_player.bla", STATE_OFF), BASIC_CONFIG)
|
|
|
|
assert trt_off.query_attributes() == {"on": False}
|
|
|
|
on_calls = async_mock_service(hass, media_player.DOMAIN, SERVICE_TURN_ON)
|
|
await trt_on.execute(trait.COMMAND_ON_OFF, BASIC_DATA, {"on": True}, {})
|
|
assert len(on_calls) == 1
|
|
assert on_calls[0].data == {ATTR_ENTITY_ID: "media_player.bla"}
|
|
|
|
off_calls = async_mock_service(hass, media_player.DOMAIN, SERVICE_TURN_OFF)
|
|
|
|
await trt_on.execute(trait.COMMAND_ON_OFF, BASIC_DATA, {"on": False}, {})
|
|
assert len(off_calls) == 1
|
|
assert off_calls[0].data == {ATTR_ENTITY_ID: "media_player.bla"}
|
|
|
|
|
|
async def test_onoff_humidifier(hass: HomeAssistant) -> None:
|
|
"""Test OnOff trait support for humidifier domain."""
|
|
assert helpers.get_google_type(humidifier.DOMAIN, None) is not None
|
|
assert trait.OnOffTrait.supported(humidifier.DOMAIN, 0, None, None)
|
|
|
|
trt_on = trait.OnOffTrait(hass, State("humidifier.bla", STATE_ON), BASIC_CONFIG)
|
|
|
|
assert trt_on.sync_attributes() == {}
|
|
|
|
assert trt_on.query_attributes() == {"on": True}
|
|
|
|
trt_off = trait.OnOffTrait(hass, State("humidifier.bla", STATE_OFF), BASIC_CONFIG)
|
|
|
|
assert trt_off.query_attributes() == {"on": False}
|
|
|
|
on_calls = async_mock_service(hass, humidifier.DOMAIN, SERVICE_TURN_ON)
|
|
await trt_on.execute(trait.COMMAND_ON_OFF, BASIC_DATA, {"on": True}, {})
|
|
assert len(on_calls) == 1
|
|
assert on_calls[0].data == {ATTR_ENTITY_ID: "humidifier.bla"}
|
|
|
|
off_calls = async_mock_service(hass, humidifier.DOMAIN, SERVICE_TURN_OFF)
|
|
|
|
await trt_on.execute(trait.COMMAND_ON_OFF, BASIC_DATA, {"on": False}, {})
|
|
assert len(off_calls) == 1
|
|
assert off_calls[0].data == {ATTR_ENTITY_ID: "humidifier.bla"}
|
|
|
|
|
|
async def test_onoff_water_heater(hass: HomeAssistant) -> None:
|
|
"""Test OnOff trait support for water_heater domain."""
|
|
assert helpers.get_google_type(water_heater.DOMAIN, None) is not None
|
|
assert trait.OnOffTrait.supported(
|
|
water_heater.DOMAIN, WaterHeaterEntityFeature.ON_OFF, None, None
|
|
)
|
|
|
|
trt_on = trait.OnOffTrait(hass, State("water_heater.bla", STATE_ON), BASIC_CONFIG)
|
|
|
|
assert trt_on.sync_attributes() == {}
|
|
|
|
assert trt_on.query_attributes() == {"on": True}
|
|
|
|
trt_off = trait.OnOffTrait(hass, State("water_heater.bla", STATE_OFF), BASIC_CONFIG)
|
|
|
|
assert trt_off.query_attributes() == {"on": False}
|
|
|
|
on_calls = async_mock_service(hass, water_heater.DOMAIN, SERVICE_TURN_ON)
|
|
await trt_on.execute(trait.COMMAND_ON_OFF, BASIC_DATA, {"on": True}, {})
|
|
assert len(on_calls) == 1
|
|
assert on_calls[0].data == {ATTR_ENTITY_ID: "water_heater.bla"}
|
|
|
|
off_calls = async_mock_service(hass, water_heater.DOMAIN, SERVICE_TURN_OFF)
|
|
|
|
await trt_on.execute(trait.COMMAND_ON_OFF, BASIC_DATA, {"on": False}, {})
|
|
assert len(off_calls) == 1
|
|
assert off_calls[0].data == {ATTR_ENTITY_ID: "water_heater.bla"}
|
|
|
|
|
|
async def test_dock_vacuum(hass: HomeAssistant) -> None:
|
|
"""Test dock trait support for vacuum domain."""
|
|
assert helpers.get_google_type(vacuum.DOMAIN, None) is not None
|
|
assert trait.DockTrait.supported(vacuum.DOMAIN, 0, None, None)
|
|
|
|
trt = trait.DockTrait(hass, State("vacuum.bla", vacuum.STATE_IDLE), BASIC_CONFIG)
|
|
|
|
assert trt.sync_attributes() == {}
|
|
|
|
assert trt.query_attributes() == {"isDocked": False}
|
|
|
|
calls = async_mock_service(hass, vacuum.DOMAIN, vacuum.SERVICE_RETURN_TO_BASE)
|
|
await trt.execute(trait.COMMAND_DOCK, BASIC_DATA, {}, {})
|
|
assert len(calls) == 1
|
|
assert calls[0].data == {ATTR_ENTITY_ID: "vacuum.bla"}
|
|
|
|
|
|
async def test_locate_vacuum(hass: HomeAssistant) -> None:
|
|
"""Test locate trait support for vacuum domain."""
|
|
assert helpers.get_google_type(vacuum.DOMAIN, None) is not None
|
|
assert trait.LocatorTrait.supported(
|
|
vacuum.DOMAIN, VacuumEntityFeature.LOCATE, None, None
|
|
)
|
|
|
|
trt = trait.LocatorTrait(
|
|
hass,
|
|
State(
|
|
"vacuum.bla",
|
|
vacuum.STATE_IDLE,
|
|
{ATTR_SUPPORTED_FEATURES: VacuumEntityFeature.LOCATE},
|
|
),
|
|
BASIC_CONFIG,
|
|
)
|
|
|
|
assert trt.sync_attributes() == {}
|
|
|
|
assert trt.query_attributes() == {}
|
|
|
|
calls = async_mock_service(hass, vacuum.DOMAIN, vacuum.SERVICE_LOCATE)
|
|
await trt.execute(trait.COMMAND_LOCATE, BASIC_DATA, {"silence": False}, {})
|
|
assert len(calls) == 1
|
|
assert calls[0].data == {ATTR_ENTITY_ID: "vacuum.bla"}
|
|
|
|
with pytest.raises(helpers.SmartHomeError) as err:
|
|
await trt.execute(trait.COMMAND_LOCATE, BASIC_DATA, {"silence": True}, {})
|
|
assert err.value.code == const.ERR_FUNCTION_NOT_SUPPORTED
|
|
|
|
|
|
async def test_energystorage_vacuum(hass: HomeAssistant) -> None:
|
|
"""Test EnergyStorage trait support for vacuum domain."""
|
|
assert helpers.get_google_type(vacuum.DOMAIN, None) is not None
|
|
assert trait.EnergyStorageTrait.supported(
|
|
vacuum.DOMAIN, VacuumEntityFeature.BATTERY, None, None
|
|
)
|
|
|
|
trt = trait.EnergyStorageTrait(
|
|
hass,
|
|
State(
|
|
"vacuum.bla",
|
|
vacuum.STATE_DOCKED,
|
|
{
|
|
ATTR_SUPPORTED_FEATURES: VacuumEntityFeature.BATTERY,
|
|
ATTR_BATTERY_LEVEL: 100,
|
|
},
|
|
),
|
|
BASIC_CONFIG,
|
|
)
|
|
|
|
assert trt.sync_attributes() == {
|
|
"isRechargeable": True,
|
|
"queryOnlyEnergyStorage": True,
|
|
}
|
|
|
|
assert trt.query_attributes() == {
|
|
"descriptiveCapacityRemaining": "FULL",
|
|
"capacityRemaining": [{"rawValue": 100, "unit": "PERCENTAGE"}],
|
|
"capacityUntilFull": [{"rawValue": 0, "unit": "PERCENTAGE"}],
|
|
"isCharging": True,
|
|
"isPluggedIn": True,
|
|
}
|
|
|
|
trt = trait.EnergyStorageTrait(
|
|
hass,
|
|
State(
|
|
"vacuum.bla",
|
|
vacuum.STATE_CLEANING,
|
|
{
|
|
ATTR_SUPPORTED_FEATURES: VacuumEntityFeature.BATTERY,
|
|
ATTR_BATTERY_LEVEL: 20,
|
|
},
|
|
),
|
|
BASIC_CONFIG,
|
|
)
|
|
|
|
assert trt.sync_attributes() == {
|
|
"isRechargeable": True,
|
|
"queryOnlyEnergyStorage": True,
|
|
}
|
|
|
|
assert trt.query_attributes() == {
|
|
"descriptiveCapacityRemaining": "CRITICALLY_LOW",
|
|
"capacityRemaining": [{"rawValue": 20, "unit": "PERCENTAGE"}],
|
|
"capacityUntilFull": [{"rawValue": 80, "unit": "PERCENTAGE"}],
|
|
"isCharging": False,
|
|
"isPluggedIn": False,
|
|
}
|
|
|
|
with pytest.raises(helpers.SmartHomeError) as err:
|
|
await trt.execute(trait.COMMAND_CHARGE, BASIC_DATA, {"charge": True}, {})
|
|
assert err.value.code == const.ERR_FUNCTION_NOT_SUPPORTED
|
|
|
|
with pytest.raises(helpers.SmartHomeError) as err:
|
|
await trt.execute(trait.COMMAND_CHARGE, BASIC_DATA, {"charge": False}, {})
|
|
assert err.value.code == const.ERR_FUNCTION_NOT_SUPPORTED
|
|
|
|
|
|
async def test_startstop_vacuum(hass: HomeAssistant) -> None:
|
|
"""Test startStop trait support for vacuum domain."""
|
|
assert helpers.get_google_type(vacuum.DOMAIN, None) is not None
|
|
assert trait.StartStopTrait.supported(vacuum.DOMAIN, 0, None, None)
|
|
|
|
trt = trait.StartStopTrait(
|
|
hass,
|
|
State(
|
|
"vacuum.bla",
|
|
vacuum.STATE_PAUSED,
|
|
{ATTR_SUPPORTED_FEATURES: VacuumEntityFeature.PAUSE},
|
|
),
|
|
BASIC_CONFIG,
|
|
)
|
|
|
|
assert trt.sync_attributes() == {"pausable": True}
|
|
|
|
assert trt.query_attributes() == {"isRunning": False, "isPaused": True}
|
|
|
|
start_calls = async_mock_service(hass, vacuum.DOMAIN, vacuum.SERVICE_START)
|
|
await trt.execute(trait.COMMAND_START_STOP, BASIC_DATA, {"start": True}, {})
|
|
assert len(start_calls) == 1
|
|
assert start_calls[0].data == {ATTR_ENTITY_ID: "vacuum.bla"}
|
|
|
|
stop_calls = async_mock_service(hass, vacuum.DOMAIN, vacuum.SERVICE_STOP)
|
|
await trt.execute(trait.COMMAND_START_STOP, BASIC_DATA, {"start": False}, {})
|
|
assert len(stop_calls) == 1
|
|
assert stop_calls[0].data == {ATTR_ENTITY_ID: "vacuum.bla"}
|
|
|
|
pause_calls = async_mock_service(hass, vacuum.DOMAIN, vacuum.SERVICE_PAUSE)
|
|
await trt.execute(trait.COMMAND_PAUSE_UNPAUSE, BASIC_DATA, {"pause": True}, {})
|
|
assert len(pause_calls) == 1
|
|
assert pause_calls[0].data == {ATTR_ENTITY_ID: "vacuum.bla"}
|
|
|
|
unpause_calls = async_mock_service(hass, vacuum.DOMAIN, vacuum.SERVICE_START)
|
|
await trt.execute(trait.COMMAND_PAUSE_UNPAUSE, BASIC_DATA, {"pause": False}, {})
|
|
assert len(unpause_calls) == 1
|
|
assert unpause_calls[0].data == {ATTR_ENTITY_ID: "vacuum.bla"}
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
(
|
|
"domain",
|
|
"state_open",
|
|
"state_closed",
|
|
"state_opening",
|
|
"state_closing",
|
|
"supported_features",
|
|
"service_close",
|
|
"service_open",
|
|
"service_stop",
|
|
"service_toggle",
|
|
),
|
|
[
|
|
(
|
|
cover.DOMAIN,
|
|
cover.STATE_OPEN,
|
|
cover.STATE_CLOSED,
|
|
cover.STATE_OPENING,
|
|
cover.STATE_CLOSING,
|
|
CoverEntityFeature.STOP
|
|
| CoverEntityFeature.OPEN
|
|
| CoverEntityFeature.CLOSE,
|
|
cover.SERVICE_OPEN_COVER,
|
|
cover.SERVICE_CLOSE_COVER,
|
|
cover.SERVICE_STOP_COVER,
|
|
cover.SERVICE_TOGGLE,
|
|
),
|
|
(
|
|
valve.DOMAIN,
|
|
valve.ValveState.OPEN,
|
|
valve.ValveState.CLOSED,
|
|
valve.ValveState.OPENING,
|
|
valve.ValveState.CLOSING,
|
|
ValveEntityFeature.STOP
|
|
| ValveEntityFeature.OPEN
|
|
| ValveEntityFeature.CLOSE,
|
|
valve.SERVICE_OPEN_VALVE,
|
|
valve.SERVICE_CLOSE_VALVE,
|
|
valve.SERVICE_STOP_VALVE,
|
|
cover.SERVICE_TOGGLE,
|
|
),
|
|
],
|
|
)
|
|
async def test_startstop_cover_valve(
|
|
hass: HomeAssistant,
|
|
domain: str,
|
|
state_open: str,
|
|
state_closed: str,
|
|
state_opening: str,
|
|
state_closing: str,
|
|
supported_features: str,
|
|
service_open: str,
|
|
service_close: str,
|
|
service_stop: str,
|
|
service_toggle: str,
|
|
) -> None:
|
|
"""Test startStop trait support."""
|
|
assert helpers.get_google_type(domain, None) is not None
|
|
assert trait.StartStopTrait.supported(domain, supported_features, None, None)
|
|
|
|
state = State(
|
|
f"{domain}.bla",
|
|
state_closed,
|
|
{ATTR_SUPPORTED_FEATURES: supported_features},
|
|
)
|
|
|
|
trt = trait.StartStopTrait(
|
|
hass,
|
|
state,
|
|
BASIC_CONFIG,
|
|
)
|
|
|
|
assert trt.sync_attributes() == {}
|
|
|
|
for state_value in (state_closing, state_opening):
|
|
state.state = state_value
|
|
assert trt.query_attributes() == {"isRunning": True}
|
|
|
|
stop_calls = async_mock_service(hass, domain, service_stop)
|
|
open_calls = async_mock_service(hass, domain, service_open)
|
|
close_calls = async_mock_service(hass, domain, service_close)
|
|
toggle_calls = async_mock_service(hass, domain, service_toggle)
|
|
await trt.execute(trait.COMMAND_START_STOP, BASIC_DATA, {"start": False}, {})
|
|
assert len(stop_calls) == 1
|
|
assert stop_calls[0].data == {ATTR_ENTITY_ID: f"{domain}.bla"}
|
|
|
|
for state_value in (state_closed, state_open):
|
|
state.state = state_value
|
|
assert trt.query_attributes() == {"isRunning": False}
|
|
|
|
for state_value in (state_closing, state_opening):
|
|
state.state = state_value
|
|
assert trt.query_attributes() == {"isRunning": True}
|
|
|
|
state.state = state_open
|
|
with pytest.raises(
|
|
SmartHomeError, match=f"{domain.capitalize()} is already stopped"
|
|
):
|
|
await trt.execute(trait.COMMAND_START_STOP, BASIC_DATA, {"start": False}, {})
|
|
|
|
# Start triggers toggle open
|
|
state.state = state_closed
|
|
await trt.execute(trait.COMMAND_START_STOP, BASIC_DATA, {"start": True}, {})
|
|
assert len(open_calls) == 0
|
|
assert len(close_calls) == 0
|
|
assert len(toggle_calls) == 1
|
|
assert toggle_calls[0].data == {ATTR_ENTITY_ID: f"{domain}.bla"}
|
|
# Second start triggers toggle close
|
|
state.state = state_open
|
|
await trt.execute(trait.COMMAND_START_STOP, BASIC_DATA, {"start": True}, {})
|
|
assert len(open_calls) == 0
|
|
assert len(close_calls) == 0
|
|
assert len(toggle_calls) == 2
|
|
assert toggle_calls[1].data == {ATTR_ENTITY_ID: f"{domain}.bla"}
|
|
|
|
state.state = state_closed
|
|
with pytest.raises(
|
|
SmartHomeError,
|
|
match="Command action.devices.commands.PauseUnpause is not supported",
|
|
):
|
|
await trt.execute(trait.COMMAND_PAUSE_UNPAUSE, BASIC_DATA, {"start": True}, {})
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
(
|
|
"domain",
|
|
"state_open",
|
|
"state_closed",
|
|
"state_opening",
|
|
"state_closing",
|
|
"supported_features",
|
|
"service_close",
|
|
"service_open",
|
|
"service_stop",
|
|
"service_toggle",
|
|
),
|
|
[
|
|
(
|
|
cover.DOMAIN,
|
|
cover.STATE_OPEN,
|
|
cover.STATE_CLOSED,
|
|
cover.STATE_OPENING,
|
|
cover.STATE_CLOSING,
|
|
CoverEntityFeature.STOP
|
|
| CoverEntityFeature.OPEN
|
|
| CoverEntityFeature.CLOSE,
|
|
cover.SERVICE_OPEN_COVER,
|
|
cover.SERVICE_CLOSE_COVER,
|
|
cover.SERVICE_STOP_COVER,
|
|
cover.SERVICE_TOGGLE,
|
|
),
|
|
(
|
|
valve.DOMAIN,
|
|
valve.ValveState.OPEN,
|
|
valve.ValveState.CLOSED,
|
|
valve.ValveState.OPENING,
|
|
valve.ValveState.CLOSING,
|
|
ValveEntityFeature.STOP
|
|
| ValveEntityFeature.OPEN
|
|
| ValveEntityFeature.CLOSE,
|
|
valve.SERVICE_OPEN_VALVE,
|
|
valve.SERVICE_CLOSE_VALVE,
|
|
valve.SERVICE_STOP_VALVE,
|
|
cover.SERVICE_TOGGLE,
|
|
),
|
|
],
|
|
)
|
|
async def test_startstop_cover_valve_assumed(
|
|
hass: HomeAssistant,
|
|
domain: str,
|
|
state_open: str,
|
|
state_closed: str,
|
|
state_opening: str,
|
|
state_closing: str,
|
|
supported_features: str,
|
|
service_open: str,
|
|
service_close: str,
|
|
service_stop: str,
|
|
service_toggle: str,
|
|
) -> None:
|
|
"""Test startStop trait support for cover domain of assumed state."""
|
|
trt = trait.StartStopTrait(
|
|
hass,
|
|
State(
|
|
f"{domain}.bla",
|
|
state_closed,
|
|
{
|
|
ATTR_SUPPORTED_FEATURES: supported_features,
|
|
ATTR_ASSUMED_STATE: True,
|
|
},
|
|
),
|
|
BASIC_CONFIG,
|
|
)
|
|
|
|
stop_calls = async_mock_service(hass, domain, service_stop)
|
|
toggle_calls = async_mock_service(hass, domain, service_toggle)
|
|
await trt.execute(trait.COMMAND_START_STOP, BASIC_DATA, {"start": False}, {})
|
|
assert len(stop_calls) == 1
|
|
assert len(toggle_calls) == 0
|
|
assert stop_calls[0].data == {ATTR_ENTITY_ID: f"{domain}.bla"}
|
|
|
|
stop_calls.clear()
|
|
await trt.execute(trait.COMMAND_START_STOP, BASIC_DATA, {"start": True}, {})
|
|
assert len(stop_calls) == 0
|
|
assert len(toggle_calls) == 1
|
|
assert toggle_calls[0].data == {ATTR_ENTITY_ID: f"{domain}.bla"}
|
|
|
|
|
|
@pytest.mark.parametrize("supported_color_modes", [["hs"], ["rgb"], ["xy"]])
|
|
async def test_color_setting_color_light(
|
|
hass: HomeAssistant, supported_color_modes
|
|
) -> None:
|
|
"""Test ColorSpectrum trait support for light domain."""
|
|
assert helpers.get_google_type(light.DOMAIN, None) is not None
|
|
assert not trait.ColorSettingTrait.supported(light.DOMAIN, 0, None, {})
|
|
assert trait.ColorSettingTrait.supported(
|
|
light.DOMAIN, 0, None, {"supported_color_modes": supported_color_modes}
|
|
)
|
|
|
|
trt = trait.ColorSettingTrait(
|
|
hass,
|
|
State(
|
|
"light.bla",
|
|
STATE_ON,
|
|
{
|
|
light.ATTR_HS_COLOR: (20, 94),
|
|
light.ATTR_BRIGHTNESS: 200,
|
|
light.ATTR_COLOR_MODE: "hs",
|
|
"supported_color_modes": supported_color_modes,
|
|
},
|
|
),
|
|
BASIC_CONFIG,
|
|
)
|
|
|
|
assert trt.sync_attributes() == {"colorModel": "hsv"}
|
|
|
|
assert trt.query_attributes() == {
|
|
"color": {"spectrumHsv": {"hue": 20, "saturation": 0.94, "value": 200 / 255}}
|
|
}
|
|
|
|
assert trt.can_execute(
|
|
trait.COMMAND_COLOR_ABSOLUTE, {"color": {"spectrumRGB": 16715792}}
|
|
)
|
|
|
|
calls = async_mock_service(hass, light.DOMAIN, SERVICE_TURN_ON)
|
|
await trt.execute(
|
|
trait.COMMAND_COLOR_ABSOLUTE,
|
|
BASIC_DATA,
|
|
{"color": {"spectrumRGB": 1052927}},
|
|
{},
|
|
)
|
|
assert len(calls) == 1
|
|
assert calls[0].data == {
|
|
ATTR_ENTITY_ID: "light.bla",
|
|
light.ATTR_HS_COLOR: (240, 93.725),
|
|
}
|
|
|
|
await trt.execute(
|
|
trait.COMMAND_COLOR_ABSOLUTE,
|
|
BASIC_DATA,
|
|
{"color": {"spectrumHSV": {"hue": 100, "saturation": 0.50, "value": 0.20}}},
|
|
{},
|
|
)
|
|
assert len(calls) == 2
|
|
assert calls[1].data == {
|
|
ATTR_ENTITY_ID: "light.bla",
|
|
light.ATTR_HS_COLOR: [100, 50],
|
|
light.ATTR_BRIGHTNESS: 0.2 * 255,
|
|
}
|
|
|
|
|
|
async def test_color_setting_temperature_light(hass: HomeAssistant) -> None:
|
|
"""Test ColorTemperature trait support for light domain."""
|
|
assert helpers.get_google_type(light.DOMAIN, None) is not None
|
|
assert not trait.ColorSettingTrait.supported(light.DOMAIN, 0, None, {})
|
|
assert trait.ColorSettingTrait.supported(
|
|
light.DOMAIN, 0, None, {"supported_color_modes": ["color_temp"]}
|
|
)
|
|
|
|
trt = trait.ColorSettingTrait(
|
|
hass,
|
|
State(
|
|
"light.bla",
|
|
STATE_ON,
|
|
{
|
|
light.ATTR_MIN_MIREDS: 200,
|
|
light.ATTR_COLOR_MODE: "color_temp",
|
|
light.ATTR_COLOR_TEMP: 300,
|
|
light.ATTR_MAX_MIREDS: 500,
|
|
"supported_color_modes": ["color_temp"],
|
|
},
|
|
),
|
|
BASIC_CONFIG,
|
|
)
|
|
|
|
assert trt.sync_attributes() == {
|
|
"colorTemperatureRange": {"temperatureMinK": 2000, "temperatureMaxK": 5000}
|
|
}
|
|
|
|
assert trt.query_attributes() == {"color": {"temperatureK": 3333}}
|
|
|
|
assert trt.can_execute(
|
|
trait.COMMAND_COLOR_ABSOLUTE, {"color": {"temperature": 400}}
|
|
)
|
|
calls = async_mock_service(hass, light.DOMAIN, SERVICE_TURN_ON)
|
|
|
|
with pytest.raises(helpers.SmartHomeError) as err:
|
|
await trt.execute(
|
|
trait.COMMAND_COLOR_ABSOLUTE,
|
|
BASIC_DATA,
|
|
{"color": {"temperature": 5555}},
|
|
{},
|
|
)
|
|
assert err.value.code == const.ERR_VALUE_OUT_OF_RANGE
|
|
|
|
await trt.execute(
|
|
trait.COMMAND_COLOR_ABSOLUTE, BASIC_DATA, {"color": {"temperature": 2857}}, {}
|
|
)
|
|
assert len(calls) == 1
|
|
assert calls[0].data == {
|
|
ATTR_ENTITY_ID: "light.bla",
|
|
light.ATTR_COLOR_TEMP: color.color_temperature_kelvin_to_mired(2857),
|
|
}
|
|
|
|
|
|
async def test_color_light_temperature_light_bad_temp(hass: HomeAssistant) -> None:
|
|
"""Test ColorTemperature trait support for light domain."""
|
|
assert helpers.get_google_type(light.DOMAIN, None) is not None
|
|
assert not trait.ColorSettingTrait.supported(light.DOMAIN, 0, None, {})
|
|
assert trait.ColorSettingTrait.supported(
|
|
light.DOMAIN, 0, None, {"supported_color_modes": ["color_temp"]}
|
|
)
|
|
|
|
trt = trait.ColorSettingTrait(
|
|
hass,
|
|
State(
|
|
"light.bla",
|
|
STATE_ON,
|
|
{
|
|
light.ATTR_MIN_MIREDS: 200,
|
|
light.ATTR_COLOR_TEMP: 0,
|
|
light.ATTR_MAX_MIREDS: 500,
|
|
},
|
|
),
|
|
BASIC_CONFIG,
|
|
)
|
|
|
|
assert trt.query_attributes() == {}
|
|
|
|
|
|
async def test_light_modes(hass: HomeAssistant) -> None:
|
|
"""Test Light Mode trait."""
|
|
assert helpers.get_google_type(light.DOMAIN, None) is not None
|
|
assert trait.ModesTrait.supported(
|
|
light.DOMAIN, LightEntityFeature.EFFECT, None, None
|
|
)
|
|
|
|
trt = trait.ModesTrait(
|
|
hass,
|
|
State(
|
|
"light.living_room",
|
|
light.STATE_ON,
|
|
attributes={
|
|
light.ATTR_EFFECT_LIST: ["random", "colorloop"],
|
|
light.ATTR_EFFECT: "random",
|
|
},
|
|
),
|
|
BASIC_CONFIG,
|
|
)
|
|
|
|
attribs = trt.sync_attributes()
|
|
assert attribs == {
|
|
"availableModes": [
|
|
{
|
|
"name": "effect",
|
|
"name_values": [{"name_synonym": ["effect"], "lang": "en"}],
|
|
"settings": [
|
|
{
|
|
"setting_name": "random",
|
|
"setting_values": [
|
|
{"setting_synonym": ["random"], "lang": "en"}
|
|
],
|
|
},
|
|
{
|
|
"setting_name": "colorloop",
|
|
"setting_values": [
|
|
{"setting_synonym": ["colorloop"], "lang": "en"}
|
|
],
|
|
},
|
|
],
|
|
"ordered": False,
|
|
}
|
|
]
|
|
}
|
|
|
|
assert trt.query_attributes() == {
|
|
"currentModeSettings": {"effect": "random"},
|
|
"on": True,
|
|
}
|
|
|
|
assert trt.can_execute(
|
|
trait.COMMAND_SET_MODES,
|
|
params={"updateModeSettings": {"effect": "colorloop"}},
|
|
)
|
|
|
|
calls = async_mock_service(hass, light.DOMAIN, SERVICE_TURN_ON)
|
|
await trt.execute(
|
|
trait.COMMAND_SET_MODES,
|
|
BASIC_DATA,
|
|
{"updateModeSettings": {"effect": "colorloop"}},
|
|
{},
|
|
)
|
|
|
|
assert len(calls) == 1
|
|
assert calls[0].data == {
|
|
"entity_id": "light.living_room",
|
|
"effect": "colorloop",
|
|
}
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"component",
|
|
[button, input_button],
|
|
)
|
|
async def test_scene_button(hass: HomeAssistant, component) -> None:
|
|
"""Test Scene trait support for the (input) button domain."""
|
|
assert helpers.get_google_type(component.DOMAIN, None) is not None
|
|
assert trait.SceneTrait.supported(component.DOMAIN, 0, None, None)
|
|
|
|
trt = trait.SceneTrait(
|
|
hass, State(f"{component.DOMAIN}.bla", STATE_UNKNOWN), BASIC_CONFIG
|
|
)
|
|
assert trt.sync_attributes() == {}
|
|
assert trt.query_attributes() == {}
|
|
assert trt.can_execute(trait.COMMAND_ACTIVATE_SCENE, {})
|
|
|
|
calls = async_mock_service(hass, component.DOMAIN, component.SERVICE_PRESS)
|
|
await trt.execute(trait.COMMAND_ACTIVATE_SCENE, BASIC_DATA, {}, {})
|
|
|
|
# We don't wait till button press is done.
|
|
await hass.async_block_till_done()
|
|
|
|
assert len(calls) == 1
|
|
assert calls[0].data == {ATTR_ENTITY_ID: f"{component.DOMAIN}.bla"}
|
|
|
|
|
|
async def test_scene_scene(hass: HomeAssistant) -> None:
|
|
"""Test Scene trait support for scene domain."""
|
|
assert helpers.get_google_type(scene.DOMAIN, None) is not None
|
|
assert trait.SceneTrait.supported(scene.DOMAIN, 0, None, None)
|
|
|
|
trt = trait.SceneTrait(hass, State("scene.bla", STATE_UNKNOWN), BASIC_CONFIG)
|
|
assert trt.sync_attributes() == {}
|
|
assert trt.query_attributes() == {}
|
|
assert trt.can_execute(trait.COMMAND_ACTIVATE_SCENE, {})
|
|
|
|
calls = async_mock_service(hass, scene.DOMAIN, SERVICE_TURN_ON)
|
|
await trt.execute(trait.COMMAND_ACTIVATE_SCENE, BASIC_DATA, {}, {})
|
|
assert len(calls) == 1
|
|
assert calls[0].data == {ATTR_ENTITY_ID: "scene.bla"}
|
|
|
|
|
|
async def test_scene_script(hass: HomeAssistant) -> None:
|
|
"""Test Scene trait support for script domain."""
|
|
assert helpers.get_google_type(script.DOMAIN, None) is not None
|
|
assert trait.SceneTrait.supported(script.DOMAIN, 0, None, None)
|
|
|
|
trt = trait.SceneTrait(hass, State("script.bla", STATE_OFF), BASIC_CONFIG)
|
|
assert trt.sync_attributes() == {}
|
|
assert trt.query_attributes() == {}
|
|
assert trt.can_execute(trait.COMMAND_ACTIVATE_SCENE, {})
|
|
|
|
calls = async_mock_service(hass, script.DOMAIN, SERVICE_TURN_ON)
|
|
await trt.execute(trait.COMMAND_ACTIVATE_SCENE, BASIC_DATA, {}, {})
|
|
|
|
# We don't wait till script execution is done.
|
|
await hass.async_block_till_done()
|
|
|
|
assert len(calls) == 1
|
|
assert calls[0].data == {ATTR_ENTITY_ID: "script.bla"}
|
|
|
|
|
|
async def test_temperature_setting_climate_onoff(hass: HomeAssistant) -> None:
|
|
"""Test TemperatureSetting trait support for climate domain - range."""
|
|
assert helpers.get_google_type(climate.DOMAIN, None) is not None
|
|
assert trait.TemperatureSettingTrait.supported(climate.DOMAIN, 0, None, None)
|
|
|
|
hass.config.units.temperature_unit = UnitOfTemperature.FAHRENHEIT
|
|
|
|
trt = trait.TemperatureSettingTrait(
|
|
hass,
|
|
State(
|
|
"climate.bla",
|
|
climate.HVACMode.AUTO,
|
|
{
|
|
ATTR_SUPPORTED_FEATURES: ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
|
|
| ClimateEntityFeature.TURN_ON
|
|
| ClimateEntityFeature.TURN_OFF,
|
|
climate.ATTR_HVAC_MODES: [
|
|
climate.HVACMode.OFF,
|
|
climate.HVACMode.COOL,
|
|
climate.HVACMode.HEAT,
|
|
climate.HVACMode.HEAT_COOL,
|
|
],
|
|
climate.ATTR_MIN_TEMP: 45,
|
|
climate.ATTR_MAX_TEMP: 95,
|
|
},
|
|
),
|
|
BASIC_CONFIG,
|
|
)
|
|
assert trt.sync_attributes() == {
|
|
"availableThermostatModes": ["off", "cool", "heat", "heatcool", "on"],
|
|
"thermostatTemperatureRange": {
|
|
"minThresholdCelsius": 7,
|
|
"maxThresholdCelsius": 35,
|
|
},
|
|
"thermostatTemperatureUnit": "F",
|
|
}
|
|
assert trt.can_execute(trait.COMMAND_THERMOSTAT_SET_MODE, {})
|
|
|
|
calls = async_mock_service(hass, climate.DOMAIN, SERVICE_TURN_ON)
|
|
await trt.execute(
|
|
trait.COMMAND_THERMOSTAT_SET_MODE, BASIC_DATA, {"thermostatMode": "on"}, {}
|
|
)
|
|
assert len(calls) == 1
|
|
|
|
calls = async_mock_service(hass, climate.DOMAIN, SERVICE_TURN_OFF)
|
|
await trt.execute(
|
|
trait.COMMAND_THERMOSTAT_SET_MODE, BASIC_DATA, {"thermostatMode": "off"}, {}
|
|
)
|
|
assert len(calls) == 1
|
|
|
|
|
|
async def test_temperature_setting_climate_no_modes(hass: HomeAssistant) -> None:
|
|
"""Test TemperatureSetting trait support for climate domain not supporting any modes."""
|
|
assert helpers.get_google_type(climate.DOMAIN, None) is not None
|
|
assert trait.TemperatureSettingTrait.supported(climate.DOMAIN, 0, None, None)
|
|
|
|
hass.config.units.temperature_unit = UnitOfTemperature.CELSIUS
|
|
|
|
trt = trait.TemperatureSettingTrait(
|
|
hass,
|
|
State(
|
|
"climate.bla",
|
|
climate.HVACMode.AUTO,
|
|
{
|
|
climate.ATTR_HVAC_MODES: [],
|
|
climate.ATTR_MIN_TEMP: climate.DEFAULT_MIN_TEMP,
|
|
climate.ATTR_MAX_TEMP: climate.DEFAULT_MAX_TEMP,
|
|
},
|
|
),
|
|
BASIC_CONFIG,
|
|
)
|
|
assert trt.sync_attributes() == {
|
|
"availableThermostatModes": ["heat"],
|
|
"thermostatTemperatureRange": {
|
|
"minThresholdCelsius": climate.DEFAULT_MIN_TEMP,
|
|
"maxThresholdCelsius": climate.DEFAULT_MAX_TEMP,
|
|
},
|
|
"thermostatTemperatureUnit": "C",
|
|
}
|
|
|
|
|
|
async def test_temperature_setting_climate_range(hass: HomeAssistant) -> None:
|
|
"""Test TemperatureSetting trait support for climate domain - range."""
|
|
assert helpers.get_google_type(climate.DOMAIN, None) is not None
|
|
assert trait.TemperatureSettingTrait.supported(climate.DOMAIN, 0, None, None)
|
|
|
|
hass.config.units.temperature_unit = UnitOfTemperature.FAHRENHEIT
|
|
|
|
trt = trait.TemperatureSettingTrait(
|
|
hass,
|
|
State(
|
|
"climate.bla",
|
|
climate.HVACMode.AUTO,
|
|
{
|
|
climate.ATTR_CURRENT_TEMPERATURE: 70,
|
|
climate.ATTR_CURRENT_HUMIDITY: 25,
|
|
ATTR_SUPPORTED_FEATURES: ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
|
|
| ClimateEntityFeature.TURN_ON
|
|
| ClimateEntityFeature.TURN_OFF,
|
|
climate.ATTR_HVAC_MODES: [
|
|
STATE_OFF,
|
|
climate.HVACMode.COOL,
|
|
climate.HVACMode.HEAT,
|
|
climate.HVACMode.AUTO,
|
|
],
|
|
climate.ATTR_TARGET_TEMP_HIGH: 75,
|
|
climate.ATTR_TARGET_TEMP_LOW: 65,
|
|
climate.ATTR_MIN_TEMP: 50,
|
|
climate.ATTR_MAX_TEMP: 80,
|
|
},
|
|
),
|
|
BASIC_CONFIG,
|
|
)
|
|
assert trt.sync_attributes() == {
|
|
"availableThermostatModes": ["off", "cool", "heat", "auto", "on"],
|
|
"thermostatTemperatureRange": {
|
|
"minThresholdCelsius": 10,
|
|
"maxThresholdCelsius": 27,
|
|
},
|
|
"thermostatTemperatureUnit": "F",
|
|
}
|
|
assert trt.query_attributes() == {
|
|
"thermostatMode": "auto",
|
|
"thermostatTemperatureAmbient": 21.1,
|
|
"thermostatHumidityAmbient": 25,
|
|
"thermostatTemperatureSetpointLow": 18.3,
|
|
"thermostatTemperatureSetpointHigh": 23.9,
|
|
}
|
|
assert trt.can_execute(trait.COMMAND_THERMOSTAT_TEMPERATURE_SET_RANGE, {})
|
|
assert trt.can_execute(trait.COMMAND_THERMOSTAT_SET_MODE, {})
|
|
|
|
calls = async_mock_service(hass, climate.DOMAIN, climate.SERVICE_SET_TEMPERATURE)
|
|
await trt.execute(
|
|
trait.COMMAND_THERMOSTAT_TEMPERATURE_SET_RANGE,
|
|
BASIC_DATA,
|
|
{
|
|
"thermostatTemperatureSetpointHigh": 25,
|
|
"thermostatTemperatureSetpointLow": 20,
|
|
},
|
|
{},
|
|
)
|
|
assert len(calls) == 1
|
|
assert calls[0].data == {
|
|
ATTR_ENTITY_ID: "climate.bla",
|
|
climate.ATTR_TARGET_TEMP_HIGH: 77,
|
|
climate.ATTR_TARGET_TEMP_LOW: 68,
|
|
}
|
|
|
|
calls = async_mock_service(hass, climate.DOMAIN, climate.SERVICE_SET_HVAC_MODE)
|
|
await trt.execute(
|
|
trait.COMMAND_THERMOSTAT_SET_MODE, BASIC_DATA, {"thermostatMode": "cool"}, {}
|
|
)
|
|
assert len(calls) == 1
|
|
assert calls[0].data == {
|
|
ATTR_ENTITY_ID: "climate.bla",
|
|
climate.ATTR_HVAC_MODE: climate.HVACMode.COOL,
|
|
}
|
|
|
|
with pytest.raises(helpers.SmartHomeError) as err:
|
|
await trt.execute(
|
|
trait.COMMAND_THERMOSTAT_TEMPERATURE_SET_RANGE,
|
|
BASIC_DATA,
|
|
{
|
|
"thermostatTemperatureSetpointHigh": 26,
|
|
"thermostatTemperatureSetpointLow": -100,
|
|
},
|
|
{},
|
|
)
|
|
assert err.value.code == const.ERR_VALUE_OUT_OF_RANGE
|
|
|
|
with pytest.raises(helpers.SmartHomeError) as err:
|
|
await trt.execute(
|
|
trait.COMMAND_THERMOSTAT_TEMPERATURE_SET_RANGE,
|
|
BASIC_DATA,
|
|
{
|
|
"thermostatTemperatureSetpointHigh": 100,
|
|
"thermostatTemperatureSetpointLow": 18,
|
|
},
|
|
{},
|
|
)
|
|
assert err.value.code == const.ERR_VALUE_OUT_OF_RANGE
|
|
|
|
calls = async_mock_service(hass, climate.DOMAIN, climate.SERVICE_SET_TEMPERATURE)
|
|
await trt.execute(
|
|
trait.COMMAND_THERMOSTAT_TEMPERATURE_SETPOINT,
|
|
BASIC_DATA,
|
|
{"thermostatTemperatureSetpoint": 23.9},
|
|
{},
|
|
)
|
|
assert len(calls) == 1
|
|
assert calls[0].data == {
|
|
ATTR_ENTITY_ID: "climate.bla",
|
|
climate.ATTR_TEMPERATURE: 75,
|
|
}
|
|
hass.config.units.temperature_unit = UnitOfTemperature.CELSIUS
|
|
|
|
|
|
async def test_temperature_setting_climate_setpoint(hass: HomeAssistant) -> None:
|
|
"""Test TemperatureSetting trait support for climate domain - setpoint."""
|
|
assert helpers.get_google_type(climate.DOMAIN, None) is not None
|
|
assert trait.TemperatureSettingTrait.supported(climate.DOMAIN, 0, None, None)
|
|
|
|
hass.config.units.temperature_unit = UnitOfTemperature.CELSIUS
|
|
|
|
trt = trait.TemperatureSettingTrait(
|
|
hass,
|
|
State(
|
|
"climate.bla",
|
|
climate.HVACMode.COOL,
|
|
{
|
|
ATTR_SUPPORTED_FEATURES: ClimateEntityFeature.TARGET_TEMPERATURE
|
|
| ClimateEntityFeature.TURN_ON
|
|
| ClimateEntityFeature.TURN_OFF,
|
|
climate.ATTR_HVAC_MODES: [STATE_OFF, climate.HVACMode.COOL],
|
|
climate.ATTR_MIN_TEMP: 10,
|
|
climate.ATTR_MAX_TEMP: 30,
|
|
climate.ATTR_PRESET_MODE: climate.PRESET_ECO,
|
|
ATTR_TEMPERATURE: 18,
|
|
climate.ATTR_CURRENT_TEMPERATURE: 20,
|
|
},
|
|
),
|
|
BASIC_CONFIG,
|
|
)
|
|
assert trt.sync_attributes() == {
|
|
"availableThermostatModes": ["off", "cool", "on"],
|
|
"thermostatTemperatureRange": {
|
|
"minThresholdCelsius": 10,
|
|
"maxThresholdCelsius": 30,
|
|
},
|
|
"thermostatTemperatureUnit": "C",
|
|
}
|
|
assert trt.query_attributes() == {
|
|
"thermostatMode": "eco",
|
|
"thermostatTemperatureAmbient": 20,
|
|
"thermostatTemperatureSetpoint": 18,
|
|
}
|
|
assert trt.can_execute(trait.COMMAND_THERMOSTAT_TEMPERATURE_SETPOINT, {})
|
|
assert trt.can_execute(trait.COMMAND_THERMOSTAT_SET_MODE, {})
|
|
|
|
calls = async_mock_service(hass, climate.DOMAIN, climate.SERVICE_SET_TEMPERATURE)
|
|
with pytest.raises(helpers.SmartHomeError):
|
|
await trt.execute(
|
|
trait.COMMAND_THERMOSTAT_TEMPERATURE_SETPOINT,
|
|
BASIC_DATA,
|
|
{"thermostatTemperatureSetpoint": -100},
|
|
{},
|
|
)
|
|
|
|
await trt.execute(
|
|
trait.COMMAND_THERMOSTAT_TEMPERATURE_SETPOINT,
|
|
BASIC_DATA,
|
|
{"thermostatTemperatureSetpoint": 19},
|
|
{},
|
|
)
|
|
assert len(calls) == 1
|
|
assert calls[0].data == {ATTR_ENTITY_ID: "climate.bla", ATTR_TEMPERATURE: 19}
|
|
|
|
calls = async_mock_service(hass, climate.DOMAIN, climate.SERVICE_SET_PRESET_MODE)
|
|
await trt.execute(
|
|
trait.COMMAND_THERMOSTAT_SET_MODE,
|
|
BASIC_DATA,
|
|
{"thermostatMode": "eco"},
|
|
{},
|
|
)
|
|
assert len(calls) == 1
|
|
assert calls[0].data == {
|
|
ATTR_ENTITY_ID: "climate.bla",
|
|
climate.ATTR_PRESET_MODE: "eco",
|
|
}
|
|
|
|
calls = async_mock_service(hass, climate.DOMAIN, climate.SERVICE_SET_TEMPERATURE)
|
|
await trt.execute(
|
|
trait.COMMAND_THERMOSTAT_TEMPERATURE_SET_RANGE,
|
|
BASIC_DATA,
|
|
{
|
|
"thermostatTemperatureSetpointHigh": 15,
|
|
"thermostatTemperatureSetpointLow": 22,
|
|
},
|
|
{},
|
|
)
|
|
assert len(calls) == 1
|
|
assert calls[0].data == {ATTR_ENTITY_ID: "climate.bla", ATTR_TEMPERATURE: 18.5}
|
|
|
|
|
|
async def test_temperature_setting_climate_setpoint_auto(hass: HomeAssistant) -> None:
|
|
"""Test TemperatureSetting trait support for climate domain.
|
|
|
|
Setpoint in auto mode.
|
|
"""
|
|
hass.config.units.temperature_unit = UnitOfTemperature.CELSIUS
|
|
|
|
trt = trait.TemperatureSettingTrait(
|
|
hass,
|
|
State(
|
|
"climate.bla",
|
|
climate.HVACMode.HEAT_COOL,
|
|
{
|
|
climate.ATTR_HVAC_MODES: [
|
|
climate.HVACMode.OFF,
|
|
climate.HVACMode.HEAT_COOL,
|
|
],
|
|
climate.ATTR_MIN_TEMP: 10,
|
|
climate.ATTR_MAX_TEMP: 30,
|
|
ATTR_TEMPERATURE: 18,
|
|
climate.ATTR_CURRENT_TEMPERATURE: 20,
|
|
},
|
|
),
|
|
BASIC_CONFIG,
|
|
)
|
|
assert trt.sync_attributes() == {
|
|
"availableThermostatModes": ["off", "heatcool", "on"],
|
|
"thermostatTemperatureRange": {
|
|
"minThresholdCelsius": 10,
|
|
"maxThresholdCelsius": 30,
|
|
},
|
|
"thermostatTemperatureUnit": "C",
|
|
}
|
|
assert trt.query_attributes() == {
|
|
"thermostatMode": "heatcool",
|
|
"thermostatTemperatureAmbient": 20,
|
|
"thermostatTemperatureSetpointHigh": 18,
|
|
"thermostatTemperatureSetpointLow": 18,
|
|
}
|
|
assert trt.can_execute(trait.COMMAND_THERMOSTAT_TEMPERATURE_SETPOINT, {})
|
|
assert trt.can_execute(trait.COMMAND_THERMOSTAT_SET_MODE, {})
|
|
|
|
calls = async_mock_service(hass, climate.DOMAIN, climate.SERVICE_SET_TEMPERATURE)
|
|
|
|
await trt.execute(
|
|
trait.COMMAND_THERMOSTAT_TEMPERATURE_SETPOINT,
|
|
BASIC_DATA,
|
|
{"thermostatTemperatureSetpoint": 19},
|
|
{},
|
|
)
|
|
assert len(calls) == 1
|
|
assert calls[0].data == {ATTR_ENTITY_ID: "climate.bla", ATTR_TEMPERATURE: 19}
|
|
|
|
|
|
async def test_temperature_control(hass: HomeAssistant) -> None:
|
|
"""Test TemperatureControl trait support for sensor domain."""
|
|
hass.config.units.temperature_unit = UnitOfTemperature.CELSIUS
|
|
|
|
trt = trait.TemperatureControlTrait(
|
|
hass,
|
|
State("sensor.temp", 18),
|
|
BASIC_CONFIG,
|
|
)
|
|
assert trt.sync_attributes() == {
|
|
"queryOnlyTemperatureControl": True,
|
|
"temperatureUnitForUX": "C",
|
|
"temperatureRange": {"maxThresholdCelsius": 100, "minThresholdCelsius": -100},
|
|
}
|
|
assert trt.query_attributes() == {
|
|
"temperatureSetpointCelsius": 18,
|
|
"temperatureAmbientCelsius": 18,
|
|
}
|
|
with pytest.raises(helpers.SmartHomeError) as err:
|
|
await trt.execute(trait.COMMAND_ON_OFF, BASIC_DATA, {"on": False}, {})
|
|
assert err.value.code == const.ERR_NOT_SUPPORTED
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("unit_in", "unit_out", "temp_in", "temp_out", "current_in", "current_out"),
|
|
[
|
|
(UnitOfTemperature.CELSIUS, "C", "120", 120, "130", 130),
|
|
(UnitOfTemperature.FAHRENHEIT, "F", "248", 120, "266", 130),
|
|
],
|
|
)
|
|
async def test_temperature_control_water_heater(
|
|
hass: HomeAssistant,
|
|
unit_in: UnitOfTemperature,
|
|
unit_out: str,
|
|
temp_in: str,
|
|
temp_out: float,
|
|
current_in: str,
|
|
current_out: float,
|
|
) -> None:
|
|
"""Test TemperatureControl trait support for water heater domain."""
|
|
hass.config.units.temperature_unit = unit_in
|
|
|
|
min_temp = TemperatureConverter.convert(
|
|
water_heater.DEFAULT_MIN_TEMP,
|
|
UnitOfTemperature.CELSIUS,
|
|
unit_in,
|
|
)
|
|
max_temp = TemperatureConverter.convert(
|
|
water_heater.DEFAULT_MAX_TEMP,
|
|
UnitOfTemperature.CELSIUS,
|
|
unit_in,
|
|
)
|
|
|
|
trt = trait.TemperatureControlTrait(
|
|
hass,
|
|
State(
|
|
"water_heater.bla",
|
|
"attributes",
|
|
{
|
|
"min_temp": min_temp,
|
|
"max_temp": max_temp,
|
|
"temperature": temp_in,
|
|
"current_temperature": current_in,
|
|
},
|
|
),
|
|
BASIC_CONFIG,
|
|
)
|
|
|
|
assert trt.sync_attributes() == {
|
|
"temperatureUnitForUX": unit_out,
|
|
"temperatureRange": {
|
|
"maxThresholdCelsius": water_heater.DEFAULT_MAX_TEMP,
|
|
"minThresholdCelsius": water_heater.DEFAULT_MIN_TEMP,
|
|
},
|
|
}
|
|
assert trt.query_attributes() == {
|
|
"temperatureSetpointCelsius": temp_out,
|
|
"temperatureAmbientCelsius": current_out,
|
|
}
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("unit", "temp_init", "temp_in", "temp_out", "current_init"),
|
|
[
|
|
(UnitOfTemperature.CELSIUS, "180", 220, 220, "180"),
|
|
(UnitOfTemperature.FAHRENHEIT, "356", 220, 428, "356"),
|
|
],
|
|
)
|
|
async def test_temperature_control_water_heater_set_temperature(
|
|
hass: HomeAssistant,
|
|
unit: UnitOfTemperature,
|
|
temp_init: str,
|
|
temp_in: float,
|
|
temp_out: float,
|
|
current_init: str,
|
|
) -> None:
|
|
"""Test TemperatureControl trait support for water heater domain - SetTemperature."""
|
|
hass.config.units.temperature_unit = unit
|
|
|
|
min_temp = TemperatureConverter.convert(
|
|
40,
|
|
UnitOfTemperature.CELSIUS,
|
|
unit,
|
|
)
|
|
max_temp = TemperatureConverter.convert(
|
|
230,
|
|
UnitOfTemperature.CELSIUS,
|
|
unit,
|
|
)
|
|
|
|
trt = trait.TemperatureControlTrait(
|
|
hass,
|
|
State(
|
|
"water_heater.bla",
|
|
"attributes",
|
|
{
|
|
"min_temp": min_temp,
|
|
"max_temp": max_temp,
|
|
"temperature": temp_init,
|
|
"current_temperature": current_init,
|
|
},
|
|
),
|
|
BASIC_CONFIG,
|
|
)
|
|
|
|
assert trt.can_execute(trait.COMMAND_SET_TEMPERATURE, {})
|
|
|
|
calls = async_mock_service(
|
|
hass, water_heater.DOMAIN, water_heater.SERVICE_SET_TEMPERATURE
|
|
)
|
|
|
|
with pytest.raises(helpers.SmartHomeError):
|
|
await trt.execute(
|
|
trait.COMMAND_SET_TEMPERATURE,
|
|
BASIC_DATA,
|
|
{"temperature": -100},
|
|
{},
|
|
)
|
|
|
|
await trt.execute(
|
|
trait.COMMAND_SET_TEMPERATURE,
|
|
BASIC_DATA,
|
|
{"temperature": temp_in},
|
|
{},
|
|
)
|
|
assert len(calls) == 1
|
|
assert calls[0].data == {
|
|
ATTR_ENTITY_ID: "water_heater.bla",
|
|
ATTR_TEMPERATURE: temp_out,
|
|
}
|
|
|
|
|
|
async def test_humidity_setting_humidifier_setpoint(hass: HomeAssistant) -> None:
|
|
"""Test HumiditySetting trait support for humidifier domain - setpoint."""
|
|
assert helpers.get_google_type(humidifier.DOMAIN, None) is not None
|
|
assert trait.HumiditySettingTrait.supported(humidifier.DOMAIN, 0, None, None)
|
|
|
|
trt = trait.HumiditySettingTrait(
|
|
hass,
|
|
State(
|
|
"humidifier.bla",
|
|
STATE_ON,
|
|
{
|
|
humidifier.ATTR_MIN_HUMIDITY: 20,
|
|
humidifier.ATTR_MAX_HUMIDITY: 90,
|
|
humidifier.ATTR_HUMIDITY: 38,
|
|
humidifier.ATTR_CURRENT_HUMIDITY: 30,
|
|
},
|
|
),
|
|
BASIC_CONFIG,
|
|
)
|
|
assert trt.sync_attributes() == {
|
|
"humiditySetpointRange": {"minPercent": 20, "maxPercent": 90}
|
|
}
|
|
assert trt.query_attributes() == {
|
|
"humiditySetpointPercent": 38,
|
|
"humidityAmbientPercent": 30,
|
|
}
|
|
assert trt.can_execute(trait.COMMAND_SET_HUMIDITY, {})
|
|
|
|
calls = async_mock_service(hass, humidifier.DOMAIN, humidifier.SERVICE_SET_HUMIDITY)
|
|
|
|
await trt.execute(trait.COMMAND_SET_HUMIDITY, BASIC_DATA, {"humidity": 32}, {})
|
|
assert len(calls) == 1
|
|
assert calls[0].data == {
|
|
ATTR_ENTITY_ID: "humidifier.bla",
|
|
humidifier.ATTR_HUMIDITY: 32,
|
|
}
|
|
|
|
|
|
async def test_lock_unlock_lock(hass: HomeAssistant) -> None:
|
|
"""Test LockUnlock trait locking support for lock domain."""
|
|
assert helpers.get_google_type(lock.DOMAIN, None) is not None
|
|
assert trait.LockUnlockTrait.supported(
|
|
lock.DOMAIN, LockEntityFeature.OPEN, None, None
|
|
)
|
|
assert trait.LockUnlockTrait.might_2fa(lock.DOMAIN, LockEntityFeature.OPEN, None)
|
|
|
|
trt = trait.LockUnlockTrait(
|
|
hass, State("lock.front_door", lock.LockState.LOCKED), PIN_CONFIG
|
|
)
|
|
|
|
assert trt.sync_attributes() == {}
|
|
|
|
assert trt.query_attributes() == {"isLocked": True}
|
|
|
|
assert trt.can_execute(trait.COMMAND_LOCK_UNLOCK, {"lock": True})
|
|
|
|
calls = async_mock_service(hass, lock.DOMAIN, lock.SERVICE_LOCK)
|
|
|
|
await trt.execute(trait.COMMAND_LOCK_UNLOCK, PIN_DATA, {"lock": True}, {})
|
|
|
|
assert len(calls) == 1
|
|
assert calls[0].data == {ATTR_ENTITY_ID: "lock.front_door"}
|
|
|
|
|
|
async def test_lock_unlock_unlocking(hass: HomeAssistant) -> None:
|
|
"""Test LockUnlock trait locking support for lock domain."""
|
|
assert helpers.get_google_type(lock.DOMAIN, None) is not None
|
|
assert trait.LockUnlockTrait.supported(
|
|
lock.DOMAIN, LockEntityFeature.OPEN, None, None
|
|
)
|
|
assert trait.LockUnlockTrait.might_2fa(lock.DOMAIN, LockEntityFeature.OPEN, None)
|
|
|
|
trt = trait.LockUnlockTrait(
|
|
hass, State("lock.front_door", lock.LockState.UNLOCKING), PIN_CONFIG
|
|
)
|
|
|
|
assert trt.sync_attributes() == {}
|
|
|
|
assert trt.query_attributes() == {"isLocked": True}
|
|
|
|
|
|
async def test_lock_unlock_lock_jammed(hass: HomeAssistant) -> None:
|
|
"""Test LockUnlock trait locking support for lock domain that jams."""
|
|
assert helpers.get_google_type(lock.DOMAIN, None) is not None
|
|
assert trait.LockUnlockTrait.supported(
|
|
lock.DOMAIN, LockEntityFeature.OPEN, None, None
|
|
)
|
|
assert trait.LockUnlockTrait.might_2fa(lock.DOMAIN, LockEntityFeature.OPEN, None)
|
|
|
|
trt = trait.LockUnlockTrait(
|
|
hass, State("lock.front_door", lock.LockState.JAMMED), PIN_CONFIG
|
|
)
|
|
|
|
assert trt.sync_attributes() == {}
|
|
|
|
assert trt.query_attributes() == {"isJammed": True}
|
|
|
|
assert trt.can_execute(trait.COMMAND_LOCK_UNLOCK, {"lock": True})
|
|
|
|
calls = async_mock_service(hass, lock.DOMAIN, lock.SERVICE_LOCK)
|
|
|
|
await trt.execute(trait.COMMAND_LOCK_UNLOCK, PIN_DATA, {"lock": True}, {})
|
|
|
|
assert len(calls) == 1
|
|
assert calls[0].data == {ATTR_ENTITY_ID: "lock.front_door"}
|
|
|
|
|
|
async def test_lock_unlock_unlock(hass: HomeAssistant) -> None:
|
|
"""Test LockUnlock trait unlocking support for lock domain."""
|
|
assert helpers.get_google_type(lock.DOMAIN, None) is not None
|
|
assert trait.LockUnlockTrait.supported(
|
|
lock.DOMAIN, LockEntityFeature.OPEN, None, None
|
|
)
|
|
|
|
trt = trait.LockUnlockTrait(
|
|
hass, State("lock.front_door", lock.LockState.LOCKED), PIN_CONFIG
|
|
)
|
|
|
|
assert trt.sync_attributes() == {}
|
|
|
|
assert trt.query_attributes() == {"isLocked": True}
|
|
|
|
assert trt.can_execute(trait.COMMAND_LOCK_UNLOCK, {"lock": False})
|
|
|
|
calls = async_mock_service(hass, lock.DOMAIN, lock.SERVICE_UNLOCK)
|
|
|
|
# No challenge data
|
|
with pytest.raises(error.ChallengeNeeded) as err:
|
|
await trt.execute(trait.COMMAND_LOCK_UNLOCK, PIN_DATA, {"lock": False}, {})
|
|
assert len(calls) == 0
|
|
assert err.value.code == const.ERR_CHALLENGE_NEEDED
|
|
assert err.value.challenge_type == const.CHALLENGE_PIN_NEEDED
|
|
|
|
# invalid pin
|
|
with pytest.raises(error.ChallengeNeeded) as err:
|
|
await trt.execute(
|
|
trait.COMMAND_LOCK_UNLOCK, PIN_DATA, {"lock": False}, {"pin": 9999}
|
|
)
|
|
assert len(calls) == 0
|
|
assert err.value.code == const.ERR_CHALLENGE_NEEDED
|
|
assert err.value.challenge_type == const.CHALLENGE_FAILED_PIN_NEEDED
|
|
|
|
await trt.execute(
|
|
trait.COMMAND_LOCK_UNLOCK, PIN_DATA, {"lock": False}, {"pin": "1234"}
|
|
)
|
|
|
|
assert len(calls) == 1
|
|
assert calls[0].data == {ATTR_ENTITY_ID: "lock.front_door"}
|
|
|
|
# Test without pin
|
|
trt = trait.LockUnlockTrait(
|
|
hass, State("lock.front_door", lock.LockState.LOCKED), BASIC_CONFIG
|
|
)
|
|
|
|
with pytest.raises(error.SmartHomeError) as err:
|
|
await trt.execute(trait.COMMAND_LOCK_UNLOCK, BASIC_DATA, {"lock": False}, {})
|
|
assert len(calls) == 1
|
|
assert err.value.code == const.ERR_CHALLENGE_NOT_SETUP
|
|
|
|
# Test with 2FA override
|
|
with patch.object(
|
|
BASIC_CONFIG,
|
|
"should_2fa",
|
|
return_value=False,
|
|
):
|
|
await trt.execute(trait.COMMAND_LOCK_UNLOCK, BASIC_DATA, {"lock": False}, {})
|
|
assert len(calls) == 2
|
|
|
|
|
|
async def test_arm_disarm_arm_away(hass: HomeAssistant) -> None:
|
|
"""Test ArmDisarm trait Arming support for alarm_control_panel domain."""
|
|
assert helpers.get_google_type(alarm_control_panel.DOMAIN, None) is not None
|
|
assert trait.ArmDisArmTrait.supported(alarm_control_panel.DOMAIN, 0, None, None)
|
|
assert trait.ArmDisArmTrait.might_2fa(alarm_control_panel.DOMAIN, 0, None)
|
|
|
|
trt = trait.ArmDisArmTrait(
|
|
hass,
|
|
State(
|
|
"alarm_control_panel.alarm",
|
|
AlarmControlPanelState.ARMED_AWAY,
|
|
{
|
|
alarm_control_panel.ATTR_CODE_ARM_REQUIRED: True,
|
|
ATTR_SUPPORTED_FEATURES: AlarmControlPanelEntityFeature.ARM_HOME
|
|
| AlarmControlPanelEntityFeature.ARM_AWAY,
|
|
},
|
|
),
|
|
PIN_CONFIG,
|
|
)
|
|
assert trt.sync_attributes() == {
|
|
"availableArmLevels": {
|
|
"levels": [
|
|
{
|
|
"level_name": "armed_home",
|
|
"level_values": [
|
|
{"level_synonym": ["armed home", "home"], "lang": "en"}
|
|
],
|
|
},
|
|
{
|
|
"level_name": "armed_away",
|
|
"level_values": [
|
|
{"level_synonym": ["armed away", "away"], "lang": "en"}
|
|
],
|
|
},
|
|
],
|
|
"ordered": True,
|
|
}
|
|
}
|
|
|
|
assert trt.query_attributes() == {
|
|
"isArmed": True,
|
|
"currentArmLevel": AlarmControlPanelState.ARMED_AWAY,
|
|
}
|
|
|
|
assert trt.can_execute(
|
|
trait.COMMAND_ARM_DISARM,
|
|
{"arm": True, "armLevel": AlarmControlPanelState.ARMED_AWAY},
|
|
)
|
|
|
|
calls = async_mock_service(
|
|
hass, alarm_control_panel.DOMAIN, alarm_control_panel.SERVICE_ALARM_ARM_AWAY
|
|
)
|
|
|
|
# Test with no secure_pin configured
|
|
|
|
trt = trait.ArmDisArmTrait(
|
|
hass,
|
|
State(
|
|
"alarm_control_panel.alarm",
|
|
AlarmControlPanelState.DISARMED,
|
|
{alarm_control_panel.ATTR_CODE_ARM_REQUIRED: True},
|
|
),
|
|
BASIC_CONFIG,
|
|
)
|
|
with pytest.raises(error.SmartHomeError) as err:
|
|
await trt.execute(
|
|
trait.COMMAND_ARM_DISARM,
|
|
BASIC_DATA,
|
|
{"arm": True, "armLevel": AlarmControlPanelState.ARMED_AWAY},
|
|
{},
|
|
)
|
|
assert len(calls) == 0
|
|
assert err.value.code == const.ERR_CHALLENGE_NOT_SETUP
|
|
|
|
trt = trait.ArmDisArmTrait(
|
|
hass,
|
|
State(
|
|
"alarm_control_panel.alarm",
|
|
AlarmControlPanelState.DISARMED,
|
|
{alarm_control_panel.ATTR_CODE_ARM_REQUIRED: True},
|
|
),
|
|
PIN_CONFIG,
|
|
)
|
|
# No challenge data
|
|
with pytest.raises(error.ChallengeNeeded) as err:
|
|
await trt.execute(
|
|
trait.COMMAND_ARM_DISARM,
|
|
PIN_DATA,
|
|
{"arm": True, "armLevel": AlarmControlPanelState.ARMED_AWAY},
|
|
{},
|
|
)
|
|
assert len(calls) == 0
|
|
assert err.value.code == const.ERR_CHALLENGE_NEEDED
|
|
assert err.value.challenge_type == const.CHALLENGE_PIN_NEEDED
|
|
|
|
# invalid pin
|
|
with pytest.raises(error.ChallengeNeeded) as err:
|
|
await trt.execute(
|
|
trait.COMMAND_ARM_DISARM,
|
|
PIN_DATA,
|
|
{"arm": True, "armLevel": AlarmControlPanelState.ARMED_AWAY},
|
|
{"pin": 9999},
|
|
)
|
|
assert len(calls) == 0
|
|
assert err.value.code == const.ERR_CHALLENGE_NEEDED
|
|
assert err.value.challenge_type == const.CHALLENGE_FAILED_PIN_NEEDED
|
|
|
|
# correct pin
|
|
await trt.execute(
|
|
trait.COMMAND_ARM_DISARM,
|
|
PIN_DATA,
|
|
{"arm": True, "armLevel": AlarmControlPanelState.ARMED_AWAY},
|
|
{"pin": "1234"},
|
|
)
|
|
|
|
assert len(calls) == 1
|
|
|
|
# Test already armed
|
|
trt = trait.ArmDisArmTrait(
|
|
hass,
|
|
State(
|
|
"alarm_control_panel.alarm",
|
|
AlarmControlPanelState.ARMED_AWAY,
|
|
{alarm_control_panel.ATTR_CODE_ARM_REQUIRED: True},
|
|
),
|
|
PIN_CONFIG,
|
|
)
|
|
with pytest.raises(error.SmartHomeError) as err:
|
|
await trt.execute(
|
|
trait.COMMAND_ARM_DISARM,
|
|
PIN_DATA,
|
|
{"arm": True, "armLevel": AlarmControlPanelState.ARMED_AWAY},
|
|
{},
|
|
)
|
|
assert len(calls) == 1
|
|
assert err.value.code == const.ERR_ALREADY_ARMED
|
|
|
|
# Test with code_arm_required False
|
|
trt = trait.ArmDisArmTrait(
|
|
hass,
|
|
State(
|
|
"alarm_control_panel.alarm",
|
|
AlarmControlPanelState.DISARMED,
|
|
{alarm_control_panel.ATTR_CODE_ARM_REQUIRED: False},
|
|
),
|
|
PIN_CONFIG,
|
|
)
|
|
await trt.execute(
|
|
trait.COMMAND_ARM_DISARM,
|
|
PIN_DATA,
|
|
{"arm": True, "armLevel": AlarmControlPanelState.ARMED_AWAY},
|
|
{},
|
|
)
|
|
assert len(calls) == 2
|
|
|
|
with pytest.raises(error.SmartHomeError) as err:
|
|
await trt.execute(
|
|
trait.COMMAND_ARM_DISARM,
|
|
PIN_DATA,
|
|
{"arm": True},
|
|
{},
|
|
)
|
|
|
|
|
|
async def test_arm_disarm_disarm(hass: HomeAssistant) -> None:
|
|
"""Test ArmDisarm trait Disarming support for alarm_control_panel domain."""
|
|
assert helpers.get_google_type(alarm_control_panel.DOMAIN, None) is not None
|
|
assert trait.ArmDisArmTrait.supported(alarm_control_panel.DOMAIN, 0, None, None)
|
|
assert trait.ArmDisArmTrait.might_2fa(alarm_control_panel.DOMAIN, 0, None)
|
|
|
|
trt = trait.ArmDisArmTrait(
|
|
hass,
|
|
State(
|
|
"alarm_control_panel.alarm",
|
|
AlarmControlPanelState.DISARMED,
|
|
{
|
|
alarm_control_panel.ATTR_CODE_ARM_REQUIRED: True,
|
|
ATTR_SUPPORTED_FEATURES: AlarmControlPanelEntityFeature.TRIGGER
|
|
| AlarmControlPanelEntityFeature.ARM_HOME
|
|
| AlarmControlPanelEntityFeature.ARM_AWAY,
|
|
},
|
|
),
|
|
PIN_CONFIG,
|
|
)
|
|
assert trt.sync_attributes() == {
|
|
"availableArmLevels": {
|
|
"levels": [
|
|
{
|
|
"level_name": "armed_home",
|
|
"level_values": [
|
|
{
|
|
"level_synonym": ["armed home", "home"],
|
|
"lang": "en",
|
|
}
|
|
],
|
|
},
|
|
{
|
|
"level_name": "armed_away",
|
|
"level_values": [
|
|
{
|
|
"level_synonym": ["armed away", "away"],
|
|
"lang": "en",
|
|
}
|
|
],
|
|
},
|
|
{
|
|
"level_name": "triggered",
|
|
"level_values": [{"level_synonym": ["triggered"], "lang": "en"}],
|
|
},
|
|
],
|
|
"ordered": True,
|
|
}
|
|
}
|
|
|
|
assert trt.query_attributes() == {
|
|
"currentArmLevel": "armed_home",
|
|
"isArmed": False,
|
|
}
|
|
|
|
assert trt.can_execute(trait.COMMAND_ARM_DISARM, {"arm": False})
|
|
|
|
calls = async_mock_service(
|
|
hass, alarm_control_panel.DOMAIN, alarm_control_panel.SERVICE_ALARM_DISARM
|
|
)
|
|
|
|
# Test without secure_pin configured
|
|
trt = trait.ArmDisArmTrait(
|
|
hass,
|
|
State(
|
|
"alarm_control_panel.alarm",
|
|
AlarmControlPanelState.ARMED_AWAY,
|
|
{alarm_control_panel.ATTR_CODE_ARM_REQUIRED: True},
|
|
),
|
|
BASIC_CONFIG,
|
|
)
|
|
with pytest.raises(error.SmartHomeError) as err:
|
|
await trt.execute(trait.COMMAND_ARM_DISARM, BASIC_DATA, {"arm": False}, {})
|
|
|
|
assert len(calls) == 0
|
|
assert err.value.code == const.ERR_CHALLENGE_NOT_SETUP
|
|
|
|
trt = trait.ArmDisArmTrait(
|
|
hass,
|
|
State(
|
|
"alarm_control_panel.alarm",
|
|
AlarmControlPanelState.ARMED_AWAY,
|
|
{alarm_control_panel.ATTR_CODE_ARM_REQUIRED: True},
|
|
),
|
|
PIN_CONFIG,
|
|
)
|
|
|
|
# No challenge data
|
|
with pytest.raises(error.ChallengeNeeded) as err:
|
|
await trt.execute(trait.COMMAND_ARM_DISARM, PIN_DATA, {"arm": False}, {})
|
|
assert len(calls) == 0
|
|
assert err.value.code == const.ERR_CHALLENGE_NEEDED
|
|
assert err.value.challenge_type == const.CHALLENGE_PIN_NEEDED
|
|
|
|
# invalid pin
|
|
with pytest.raises(error.ChallengeNeeded) as err:
|
|
await trt.execute(
|
|
trait.COMMAND_ARM_DISARM, PIN_DATA, {"arm": False}, {"pin": 9999}
|
|
)
|
|
assert len(calls) == 0
|
|
assert err.value.code == const.ERR_CHALLENGE_NEEDED
|
|
assert err.value.challenge_type == const.CHALLENGE_FAILED_PIN_NEEDED
|
|
|
|
# correct pin
|
|
await trt.execute(
|
|
trait.COMMAND_ARM_DISARM, PIN_DATA, {"arm": False}, {"pin": "1234"}
|
|
)
|
|
|
|
assert len(calls) == 1
|
|
|
|
# Test already disarmed
|
|
trt = trait.ArmDisArmTrait(
|
|
hass,
|
|
State(
|
|
"alarm_control_panel.alarm",
|
|
AlarmControlPanelState.DISARMED,
|
|
{alarm_control_panel.ATTR_CODE_ARM_REQUIRED: True},
|
|
),
|
|
PIN_CONFIG,
|
|
)
|
|
with pytest.raises(error.SmartHomeError) as err:
|
|
await trt.execute(trait.COMMAND_ARM_DISARM, PIN_DATA, {"arm": False}, {})
|
|
assert len(calls) == 1
|
|
assert err.value.code == const.ERR_ALREADY_DISARMED
|
|
|
|
trt = trait.ArmDisArmTrait(
|
|
hass,
|
|
State(
|
|
"alarm_control_panel.alarm",
|
|
AlarmControlPanelState.ARMED_AWAY,
|
|
{alarm_control_panel.ATTR_CODE_ARM_REQUIRED: False},
|
|
),
|
|
PIN_CONFIG,
|
|
)
|
|
|
|
# Cancel arming after already armed will require pin
|
|
with pytest.raises(error.SmartHomeError) as err:
|
|
await trt.execute(
|
|
trait.COMMAND_ARM_DISARM, PIN_DATA, {"arm": True, "cancel": True}, {}
|
|
)
|
|
assert len(calls) == 1
|
|
assert err.value.code == const.ERR_CHALLENGE_NEEDED
|
|
assert err.value.challenge_type == const.CHALLENGE_PIN_NEEDED
|
|
|
|
# Cancel arming while pending to arm doesn't require pin
|
|
trt = trait.ArmDisArmTrait(
|
|
hass,
|
|
State(
|
|
"alarm_control_panel.alarm",
|
|
AlarmControlPanelState.PENDING,
|
|
{alarm_control_panel.ATTR_CODE_ARM_REQUIRED: False},
|
|
),
|
|
PIN_CONFIG,
|
|
)
|
|
await trt.execute(
|
|
trait.COMMAND_ARM_DISARM, PIN_DATA, {"arm": True, "cancel": True}, {}
|
|
)
|
|
assert len(calls) == 2
|
|
|
|
|
|
async def test_fan_speed(hass: HomeAssistant) -> None:
|
|
"""Test FanSpeed trait speed control support for fan domain."""
|
|
assert helpers.get_google_type(fan.DOMAIN, None) is not None
|
|
assert trait.FanSpeedTrait.supported(
|
|
fan.DOMAIN, FanEntityFeature.SET_SPEED, None, None
|
|
)
|
|
|
|
trt = trait.FanSpeedTrait(
|
|
hass,
|
|
State(
|
|
"fan.living_room_fan",
|
|
STATE_ON,
|
|
attributes={
|
|
"percentage": 33,
|
|
"percentage_step": 1.0,
|
|
},
|
|
),
|
|
BASIC_CONFIG,
|
|
)
|
|
|
|
assert trt.sync_attributes() == {
|
|
"reversible": False,
|
|
"supportsFanSpeedPercent": True,
|
|
"availableFanSpeeds": ANY,
|
|
}
|
|
|
|
assert trt.query_attributes() == {
|
|
"currentFanSpeedPercent": 33,
|
|
"currentFanSpeedSetting": ANY,
|
|
}
|
|
|
|
assert trt.can_execute(trait.COMMAND_SET_FAN_SPEED, params={"fanSpeedPercent": 10})
|
|
|
|
calls = async_mock_service(hass, fan.DOMAIN, fan.SERVICE_SET_PERCENTAGE)
|
|
await trt.execute(
|
|
trait.COMMAND_SET_FAN_SPEED, BASIC_DATA, {"fanSpeedPercent": 10}, {}
|
|
)
|
|
|
|
assert len(calls) == 1
|
|
assert calls[0].data == {"entity_id": "fan.living_room_fan", "percentage": 10}
|
|
|
|
|
|
async def test_fan_speed_without_percentage_step(hass: HomeAssistant) -> None:
|
|
"""Test FanSpeed trait speed control percentage step for fan domain."""
|
|
assert helpers.get_google_type(fan.DOMAIN, None) is not None
|
|
assert trait.FanSpeedTrait.supported(
|
|
fan.DOMAIN, FanEntityFeature.SET_SPEED, None, None
|
|
)
|
|
|
|
trt = trait.FanSpeedTrait(
|
|
hass,
|
|
State(
|
|
"fan.living_room_fan",
|
|
STATE_ON,
|
|
),
|
|
BASIC_CONFIG,
|
|
)
|
|
|
|
assert trt.sync_attributes() == {
|
|
"reversible": False,
|
|
"supportsFanSpeedPercent": True,
|
|
"availableFanSpeeds": ANY,
|
|
}
|
|
# If a fan state has (temporary) no percentage_step attribute return 1 available
|
|
assert trt.query_attributes() == {
|
|
"currentFanSpeedPercent": 0,
|
|
"currentFanSpeedSetting": "1/5",
|
|
}
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("percentage", "percentage_step", "speed", "speeds", "percentage_result"),
|
|
[
|
|
(
|
|
33,
|
|
1.0,
|
|
"2/5",
|
|
[
|
|
["Low", "Min", "Slow", "1"],
|
|
["Medium Low", "2"],
|
|
["Medium", "3"],
|
|
["Medium High", "4"],
|
|
["High", "Max", "Fast", "5"],
|
|
],
|
|
40,
|
|
),
|
|
(
|
|
40,
|
|
1.0,
|
|
"2/5",
|
|
[
|
|
["Low", "Min", "Slow", "1"],
|
|
["Medium Low", "2"],
|
|
["Medium", "3"],
|
|
["Medium High", "4"],
|
|
["High", "Max", "Fast", "5"],
|
|
],
|
|
40,
|
|
),
|
|
(
|
|
33,
|
|
100 / 3,
|
|
"1/3",
|
|
[
|
|
["Low", "Min", "Slow", "1"],
|
|
["Medium", "2"],
|
|
["High", "Max", "Fast", "3"],
|
|
],
|
|
33,
|
|
),
|
|
(
|
|
20,
|
|
100 / 4,
|
|
"1/4",
|
|
[
|
|
["Low", "Min", "Slow", "1"],
|
|
["Medium Low", "2"],
|
|
["Medium High", "3"],
|
|
["High", "Max", "Fast", "4"],
|
|
],
|
|
25,
|
|
),
|
|
],
|
|
)
|
|
async def test_fan_speed_ordered(
|
|
hass: HomeAssistant,
|
|
percentage: int,
|
|
percentage_step: float,
|
|
speed: str,
|
|
speeds: list[list[str]],
|
|
percentage_result: int,
|
|
) -> None:
|
|
"""Test FanSpeed trait speed control support for fan domain."""
|
|
assert helpers.get_google_type(fan.DOMAIN, None) is not None
|
|
assert trait.FanSpeedTrait.supported(
|
|
fan.DOMAIN, FanEntityFeature.SET_SPEED, None, None
|
|
)
|
|
|
|
trt = trait.FanSpeedTrait(
|
|
hass,
|
|
State(
|
|
"fan.living_room_fan",
|
|
STATE_ON,
|
|
attributes={
|
|
"percentage": percentage,
|
|
"percentage_step": percentage_step,
|
|
},
|
|
),
|
|
BASIC_CONFIG,
|
|
)
|
|
|
|
assert trt.sync_attributes() == {
|
|
"reversible": False,
|
|
"supportsFanSpeedPercent": True,
|
|
"availableFanSpeeds": {
|
|
"ordered": True,
|
|
"speeds": [
|
|
{
|
|
"speed_name": f"{idx+1}/{len(speeds)}",
|
|
"speed_values": [{"lang": "en", "speed_synonym": x}],
|
|
}
|
|
for idx, x in enumerate(speeds)
|
|
],
|
|
},
|
|
}
|
|
|
|
assert trt.query_attributes() == {
|
|
"currentFanSpeedPercent": percentage,
|
|
"currentFanSpeedSetting": speed,
|
|
}
|
|
|
|
assert trt.can_execute(trait.COMMAND_SET_FAN_SPEED, params={"fanSpeed": speed})
|
|
|
|
calls = async_mock_service(hass, fan.DOMAIN, fan.SERVICE_SET_PERCENTAGE)
|
|
await trt.execute(trait.COMMAND_SET_FAN_SPEED, BASIC_DATA, {"fanSpeed": speed}, {})
|
|
|
|
assert len(calls) == 1
|
|
assert calls[0].data == {
|
|
"entity_id": "fan.living_room_fan",
|
|
"percentage": percentage_result,
|
|
}
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("direction_state", "direction_call"),
|
|
[
|
|
(fan.DIRECTION_FORWARD, fan.DIRECTION_REVERSE),
|
|
(fan.DIRECTION_REVERSE, fan.DIRECTION_FORWARD),
|
|
(None, fan.DIRECTION_FORWARD),
|
|
],
|
|
)
|
|
async def test_fan_reverse(
|
|
hass: HomeAssistant, direction_state, direction_call
|
|
) -> None:
|
|
"""Test FanSpeed trait speed control support for fan domain."""
|
|
|
|
calls = async_mock_service(hass, fan.DOMAIN, fan.SERVICE_SET_DIRECTION)
|
|
|
|
trt = trait.FanSpeedTrait(
|
|
hass,
|
|
State(
|
|
"fan.living_room_fan",
|
|
STATE_ON,
|
|
attributes={
|
|
"percentage": 33,
|
|
"percentage_step": 1.0,
|
|
"direction": direction_state,
|
|
"supported_features": FanEntityFeature.DIRECTION,
|
|
},
|
|
),
|
|
BASIC_CONFIG,
|
|
)
|
|
|
|
assert trt.sync_attributes() == {
|
|
"reversible": True,
|
|
"supportsFanSpeedPercent": True,
|
|
"availableFanSpeeds": ANY,
|
|
}
|
|
|
|
assert trt.query_attributes() == {
|
|
"currentFanSpeedPercent": 33,
|
|
"currentFanSpeedSetting": ANY,
|
|
}
|
|
|
|
assert trt.can_execute(trait.COMMAND_REVERSE, params={})
|
|
await trt.execute(trait.COMMAND_REVERSE, BASIC_DATA, {}, {})
|
|
|
|
assert len(calls) == 1
|
|
assert calls[0].data == {
|
|
"entity_id": "fan.living_room_fan",
|
|
"direction": direction_call,
|
|
}
|
|
|
|
|
|
async def test_climate_fan_speed(hass: HomeAssistant) -> None:
|
|
"""Test FanSpeed trait speed control support for climate domain."""
|
|
assert helpers.get_google_type(climate.DOMAIN, None) is not None
|
|
assert trait.FanSpeedTrait.supported(
|
|
climate.DOMAIN, ClimateEntityFeature.FAN_MODE, None, None
|
|
)
|
|
|
|
trt = trait.FanSpeedTrait(
|
|
hass,
|
|
State(
|
|
"climate.living_room_ac",
|
|
"on",
|
|
attributes={
|
|
"fan_modes": ["auto", "low", "medium", "high"],
|
|
"fan_mode": "low",
|
|
},
|
|
),
|
|
BASIC_CONFIG,
|
|
)
|
|
|
|
assert trt.sync_attributes() == {
|
|
"availableFanSpeeds": {
|
|
"ordered": True,
|
|
"speeds": [
|
|
{
|
|
"speed_name": "auto",
|
|
"speed_values": [{"speed_synonym": ["auto"], "lang": "en"}],
|
|
},
|
|
{
|
|
"speed_name": "low",
|
|
"speed_values": [{"speed_synonym": ["low"], "lang": "en"}],
|
|
},
|
|
{
|
|
"speed_name": "medium",
|
|
"speed_values": [{"speed_synonym": ["medium"], "lang": "en"}],
|
|
},
|
|
{
|
|
"speed_name": "high",
|
|
"speed_values": [{"speed_synonym": ["high"], "lang": "en"}],
|
|
},
|
|
],
|
|
},
|
|
"reversible": False,
|
|
}
|
|
|
|
assert trt.query_attributes() == {
|
|
"currentFanSpeedSetting": "low",
|
|
}
|
|
|
|
assert trt.can_execute(trait.COMMAND_SET_FAN_SPEED, params={"fanSpeed": "medium"})
|
|
|
|
calls = async_mock_service(hass, climate.DOMAIN, climate.SERVICE_SET_FAN_MODE)
|
|
await trt.execute(
|
|
trait.COMMAND_SET_FAN_SPEED, BASIC_DATA, {"fanSpeed": "medium"}, {}
|
|
)
|
|
|
|
assert len(calls) == 1
|
|
assert calls[0].data == {
|
|
"entity_id": "climate.living_room_ac",
|
|
"fan_mode": "medium",
|
|
}
|
|
|
|
|
|
async def test_inputselector(hass: HomeAssistant) -> None:
|
|
"""Test input selector trait."""
|
|
assert helpers.get_google_type(media_player.DOMAIN, None) is not None
|
|
assert trait.InputSelectorTrait.supported(
|
|
media_player.DOMAIN,
|
|
MediaPlayerEntityFeature.SELECT_SOURCE,
|
|
None,
|
|
None,
|
|
)
|
|
|
|
trt = trait.InputSelectorTrait(
|
|
hass,
|
|
State(
|
|
"media_player.living_room",
|
|
media_player.STATE_PLAYING,
|
|
attributes={
|
|
media_player.ATTR_INPUT_SOURCE_LIST: [
|
|
"media",
|
|
"game",
|
|
"chromecast",
|
|
"plex",
|
|
],
|
|
media_player.ATTR_INPUT_SOURCE: "game",
|
|
},
|
|
),
|
|
BASIC_CONFIG,
|
|
)
|
|
|
|
attribs = trt.sync_attributes()
|
|
assert attribs == {
|
|
"availableInputs": [
|
|
{"key": "media", "names": [{"name_synonym": ["media"], "lang": "en"}]},
|
|
{"key": "game", "names": [{"name_synonym": ["game"], "lang": "en"}]},
|
|
{
|
|
"key": "chromecast",
|
|
"names": [{"name_synonym": ["chromecast"], "lang": "en"}],
|
|
},
|
|
{"key": "plex", "names": [{"name_synonym": ["plex"], "lang": "en"}]},
|
|
],
|
|
"orderedInputs": True,
|
|
}
|
|
|
|
assert trt.query_attributes() == {
|
|
"currentInput": "game",
|
|
}
|
|
|
|
assert trt.can_execute(
|
|
trait.COMMAND_SET_INPUT,
|
|
params={"newInput": "media"},
|
|
)
|
|
|
|
calls = async_mock_service(
|
|
hass, media_player.DOMAIN, media_player.SERVICE_SELECT_SOURCE
|
|
)
|
|
await trt.execute(
|
|
trait.COMMAND_SET_INPUT,
|
|
BASIC_DATA,
|
|
{"newInput": "media"},
|
|
{},
|
|
)
|
|
|
|
assert len(calls) == 1
|
|
assert calls[0].data == {"entity_id": "media_player.living_room", "source": "media"}
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("sources", "source", "source_next", "source_prev"),
|
|
[
|
|
(["a"], "a", "a", "a"),
|
|
(["a", "b"], "a", "b", "b"),
|
|
(["a", "b", "c"], "a", "b", "c"),
|
|
],
|
|
)
|
|
async def test_inputselector_nextprev(
|
|
hass: HomeAssistant, sources, source, source_next, source_prev
|
|
) -> None:
|
|
"""Test input selector trait."""
|
|
trt = trait.InputSelectorTrait(
|
|
hass,
|
|
State(
|
|
"media_player.living_room",
|
|
media_player.STATE_PLAYING,
|
|
attributes={
|
|
media_player.ATTR_INPUT_SOURCE_LIST: sources,
|
|
media_player.ATTR_INPUT_SOURCE: source,
|
|
},
|
|
),
|
|
BASIC_CONFIG,
|
|
)
|
|
|
|
assert trt.can_execute("action.devices.commands.NextInput", params={})
|
|
assert trt.can_execute("action.devices.commands.PreviousInput", params={})
|
|
|
|
calls = async_mock_service(
|
|
hass, media_player.DOMAIN, media_player.SERVICE_SELECT_SOURCE
|
|
)
|
|
await trt.execute(
|
|
"action.devices.commands.NextInput",
|
|
BASIC_DATA,
|
|
{},
|
|
{},
|
|
)
|
|
await trt.execute(
|
|
"action.devices.commands.PreviousInput",
|
|
BASIC_DATA,
|
|
{},
|
|
{},
|
|
)
|
|
|
|
assert len(calls) == 2
|
|
assert calls[0].data == {
|
|
"entity_id": "media_player.living_room",
|
|
"source": source_next,
|
|
}
|
|
assert calls[1].data == {
|
|
"entity_id": "media_player.living_room",
|
|
"source": source_prev,
|
|
}
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("sources", "source"), [(None, "a"), (["a", "b"], None), (["a", "b"], "c")]
|
|
)
|
|
async def test_inputselector_nextprev_invalid(
|
|
hass: HomeAssistant, sources, source
|
|
) -> None:
|
|
"""Test input selector trait."""
|
|
trt = trait.InputSelectorTrait(
|
|
hass,
|
|
State(
|
|
"media_player.living_room",
|
|
media_player.STATE_PLAYING,
|
|
attributes={
|
|
media_player.ATTR_INPUT_SOURCE_LIST: sources,
|
|
media_player.ATTR_INPUT_SOURCE: source,
|
|
},
|
|
),
|
|
BASIC_CONFIG,
|
|
)
|
|
|
|
with pytest.raises(SmartHomeError):
|
|
await trt.execute(
|
|
"action.devices.commands.NextInput",
|
|
BASIC_DATA,
|
|
{},
|
|
{},
|
|
)
|
|
|
|
with pytest.raises(SmartHomeError):
|
|
await trt.execute(
|
|
"action.devices.commands.PreviousInput",
|
|
BASIC_DATA,
|
|
{},
|
|
{},
|
|
)
|
|
|
|
with pytest.raises(SmartHomeError):
|
|
await trt.execute(
|
|
"action.devices.commands.InvalidCommand",
|
|
BASIC_DATA,
|
|
{},
|
|
{},
|
|
)
|
|
|
|
|
|
async def test_modes_input_select(hass: HomeAssistant) -> None:
|
|
"""Test Input Select Mode trait."""
|
|
assert helpers.get_google_type(input_select.DOMAIN, None) is not None
|
|
assert trait.ModesTrait.supported(input_select.DOMAIN, None, None, None)
|
|
|
|
trt = trait.ModesTrait(
|
|
hass,
|
|
State("input_select.bla", "unavailable"),
|
|
BASIC_CONFIG,
|
|
)
|
|
assert trt.sync_attributes() == {"availableModes": []}
|
|
|
|
trt = trait.ModesTrait(
|
|
hass,
|
|
State(
|
|
"input_select.bla",
|
|
"abc",
|
|
attributes={input_select.ATTR_OPTIONS: ["abc", "123", "xyz"]},
|
|
),
|
|
BASIC_CONFIG,
|
|
)
|
|
|
|
attribs = trt.sync_attributes()
|
|
assert attribs == {
|
|
"availableModes": [
|
|
{
|
|
"name": "option",
|
|
"name_values": [
|
|
{
|
|
"name_synonym": ["option", "setting", "mode", "value"],
|
|
"lang": "en",
|
|
}
|
|
],
|
|
"settings": [
|
|
{
|
|
"setting_name": "abc",
|
|
"setting_values": [{"setting_synonym": ["abc"], "lang": "en"}],
|
|
},
|
|
{
|
|
"setting_name": "123",
|
|
"setting_values": [{"setting_synonym": ["123"], "lang": "en"}],
|
|
},
|
|
{
|
|
"setting_name": "xyz",
|
|
"setting_values": [{"setting_synonym": ["xyz"], "lang": "en"}],
|
|
},
|
|
],
|
|
"ordered": False,
|
|
}
|
|
]
|
|
}
|
|
|
|
assert trt.query_attributes() == {
|
|
"currentModeSettings": {"option": "abc"},
|
|
"on": True,
|
|
}
|
|
|
|
assert trt.can_execute(
|
|
trait.COMMAND_SET_MODES,
|
|
params={"updateModeSettings": {"option": "xyz"}},
|
|
)
|
|
|
|
calls = async_mock_service(
|
|
hass, input_select.DOMAIN, input_select.SERVICE_SELECT_OPTION
|
|
)
|
|
await trt.execute(
|
|
trait.COMMAND_SET_MODES,
|
|
BASIC_DATA,
|
|
{"updateModeSettings": {"option": "xyz"}},
|
|
{},
|
|
)
|
|
|
|
assert len(calls) == 1
|
|
assert calls[0].data == {"entity_id": "input_select.bla", "option": "xyz"}
|
|
|
|
|
|
async def test_modes_select(hass: HomeAssistant) -> None:
|
|
"""Test Select Mode trait."""
|
|
assert helpers.get_google_type(select.DOMAIN, None) is not None
|
|
assert trait.ModesTrait.supported(select.DOMAIN, None, None, None)
|
|
|
|
trt = trait.ModesTrait(
|
|
hass,
|
|
State("select.bla", "unavailable"),
|
|
BASIC_CONFIG,
|
|
)
|
|
assert trt.sync_attributes() == {"availableModes": []}
|
|
|
|
trt = trait.ModesTrait(
|
|
hass,
|
|
State(
|
|
"select.bla",
|
|
"abc",
|
|
attributes={select.ATTR_OPTIONS: ["abc", "123", "xyz"]},
|
|
),
|
|
BASIC_CONFIG,
|
|
)
|
|
|
|
attribs = trt.sync_attributes()
|
|
assert attribs == {
|
|
"availableModes": [
|
|
{
|
|
"name": "option",
|
|
"name_values": [
|
|
{
|
|
"name_synonym": ["option", "setting", "mode", "value"],
|
|
"lang": "en",
|
|
}
|
|
],
|
|
"settings": [
|
|
{
|
|
"setting_name": "abc",
|
|
"setting_values": [{"setting_synonym": ["abc"], "lang": "en"}],
|
|
},
|
|
{
|
|
"setting_name": "123",
|
|
"setting_values": [{"setting_synonym": ["123"], "lang": "en"}],
|
|
},
|
|
{
|
|
"setting_name": "xyz",
|
|
"setting_values": [{"setting_synonym": ["xyz"], "lang": "en"}],
|
|
},
|
|
],
|
|
"ordered": False,
|
|
}
|
|
]
|
|
}
|
|
|
|
assert trt.query_attributes() == {
|
|
"currentModeSettings": {"option": "abc"},
|
|
"on": True,
|
|
}
|
|
|
|
assert trt.can_execute(
|
|
trait.COMMAND_SET_MODES,
|
|
params={"updateModeSettings": {"option": "xyz"}},
|
|
)
|
|
|
|
calls = async_mock_service(hass, select.DOMAIN, select.SERVICE_SELECT_OPTION)
|
|
await trt.execute(
|
|
trait.COMMAND_SET_MODES,
|
|
BASIC_DATA,
|
|
{"updateModeSettings": {"option": "xyz"}},
|
|
{},
|
|
)
|
|
|
|
assert len(calls) == 1
|
|
assert calls[0].data == {"entity_id": "select.bla", "option": "xyz"}
|
|
|
|
|
|
async def test_modes_humidifier(hass: HomeAssistant) -> None:
|
|
"""Test Humidifier Mode trait."""
|
|
assert helpers.get_google_type(humidifier.DOMAIN, None) is not None
|
|
assert trait.ModesTrait.supported(
|
|
humidifier.DOMAIN, HumidifierEntityFeature.MODES, None, None
|
|
)
|
|
|
|
trt = trait.ModesTrait(
|
|
hass,
|
|
State(
|
|
"humidifier.humidifier",
|
|
STATE_OFF,
|
|
attributes={
|
|
humidifier.ATTR_AVAILABLE_MODES: [
|
|
humidifier.MODE_NORMAL,
|
|
humidifier.MODE_AUTO,
|
|
humidifier.MODE_AWAY,
|
|
],
|
|
ATTR_SUPPORTED_FEATURES: HumidifierEntityFeature.MODES,
|
|
humidifier.ATTR_MIN_HUMIDITY: 30,
|
|
humidifier.ATTR_MAX_HUMIDITY: 99,
|
|
humidifier.ATTR_HUMIDITY: 50,
|
|
ATTR_MODE: humidifier.MODE_AUTO,
|
|
},
|
|
),
|
|
BASIC_CONFIG,
|
|
)
|
|
|
|
attribs = trt.sync_attributes()
|
|
assert attribs == {
|
|
"availableModes": [
|
|
{
|
|
"name": "mode",
|
|
"name_values": [{"name_synonym": ["mode"], "lang": "en"}],
|
|
"settings": [
|
|
{
|
|
"setting_name": "normal",
|
|
"setting_values": [
|
|
{"setting_synonym": ["normal"], "lang": "en"}
|
|
],
|
|
},
|
|
{
|
|
"setting_name": "auto",
|
|
"setting_values": [{"setting_synonym": ["auto"], "lang": "en"}],
|
|
},
|
|
{
|
|
"setting_name": "away",
|
|
"setting_values": [{"setting_synonym": ["away"], "lang": "en"}],
|
|
},
|
|
],
|
|
"ordered": False,
|
|
},
|
|
]
|
|
}
|
|
|
|
assert trt.query_attributes() == {
|
|
"currentModeSettings": {"mode": "auto"},
|
|
"on": False,
|
|
}
|
|
|
|
assert trt.can_execute(
|
|
trait.COMMAND_SET_MODES, params={"updateModeSettings": {"mode": "away"}}
|
|
)
|
|
|
|
calls = async_mock_service(hass, humidifier.DOMAIN, humidifier.SERVICE_SET_MODE)
|
|
await trt.execute(
|
|
trait.COMMAND_SET_MODES,
|
|
BASIC_DATA,
|
|
{"updateModeSettings": {"mode": "away"}},
|
|
{},
|
|
)
|
|
|
|
assert len(calls) == 1
|
|
assert calls[0].data == {
|
|
"entity_id": "humidifier.humidifier",
|
|
"mode": "away",
|
|
}
|
|
|
|
|
|
async def test_modes_water_heater(hass: HomeAssistant) -> None:
|
|
"""Test Humidifier Mode trait."""
|
|
assert helpers.get_google_type(water_heater.DOMAIN, None) is not None
|
|
assert trait.ModesTrait.supported(
|
|
water_heater.DOMAIN, WaterHeaterEntityFeature.OPERATION_MODE, None, None
|
|
)
|
|
|
|
trt = trait.ModesTrait(
|
|
hass,
|
|
State(
|
|
"water_heater.water_heater",
|
|
STATE_OFF,
|
|
attributes={
|
|
water_heater.ATTR_OPERATION_LIST: [
|
|
water_heater.STATE_ECO,
|
|
water_heater.STATE_HEAT_PUMP,
|
|
water_heater.STATE_GAS,
|
|
],
|
|
ATTR_SUPPORTED_FEATURES: WaterHeaterEntityFeature.OPERATION_MODE,
|
|
water_heater.ATTR_OPERATION_MODE: water_heater.STATE_HEAT_PUMP,
|
|
},
|
|
),
|
|
BASIC_CONFIG,
|
|
)
|
|
|
|
attribs = trt.sync_attributes()
|
|
assert attribs == {
|
|
"availableModes": [
|
|
{
|
|
"name": "operation mode",
|
|
"name_values": [{"name_synonym": ["operation mode"], "lang": "en"}],
|
|
"settings": [
|
|
{
|
|
"setting_name": "eco",
|
|
"setting_values": [{"setting_synonym": ["eco"], "lang": "en"}],
|
|
},
|
|
{
|
|
"setting_name": "heat_pump",
|
|
"setting_values": [
|
|
{"setting_synonym": ["heat_pump"], "lang": "en"}
|
|
],
|
|
},
|
|
{
|
|
"setting_name": "gas",
|
|
"setting_values": [{"setting_synonym": ["gas"], "lang": "en"}],
|
|
},
|
|
],
|
|
"ordered": False,
|
|
},
|
|
]
|
|
}
|
|
|
|
assert trt.query_attributes() == {
|
|
"currentModeSettings": {"operation mode": "heat_pump"},
|
|
"on": False,
|
|
}
|
|
|
|
assert trt.can_execute(
|
|
trait.COMMAND_SET_MODES,
|
|
params={"updateModeSettings": {"operation mode": "gas"}},
|
|
)
|
|
|
|
calls = async_mock_service(
|
|
hass, water_heater.DOMAIN, water_heater.SERVICE_SET_OPERATION_MODE
|
|
)
|
|
await trt.execute(
|
|
trait.COMMAND_SET_MODES,
|
|
BASIC_DATA,
|
|
{"updateModeSettings": {"operation mode": "gas"}},
|
|
{},
|
|
)
|
|
|
|
assert len(calls) == 1
|
|
assert calls[0].data == {
|
|
"entity_id": "water_heater.water_heater",
|
|
"operation_mode": "gas",
|
|
}
|
|
|
|
|
|
async def test_sound_modes(hass: HomeAssistant) -> None:
|
|
"""Test Mode trait."""
|
|
assert helpers.get_google_type(media_player.DOMAIN, None) is not None
|
|
assert trait.ModesTrait.supported(
|
|
media_player.DOMAIN,
|
|
MediaPlayerEntityFeature.SELECT_SOUND_MODE,
|
|
None,
|
|
None,
|
|
)
|
|
|
|
trt = trait.ModesTrait(
|
|
hass,
|
|
State(
|
|
"media_player.living_room",
|
|
media_player.STATE_PLAYING,
|
|
attributes={
|
|
media_player.ATTR_SOUND_MODE_LIST: ["stereo", "prologic"],
|
|
media_player.ATTR_SOUND_MODE: "stereo",
|
|
},
|
|
),
|
|
BASIC_CONFIG,
|
|
)
|
|
|
|
attribs = trt.sync_attributes()
|
|
assert attribs == {
|
|
"availableModes": [
|
|
{
|
|
"name": "sound mode",
|
|
"name_values": [
|
|
{"name_synonym": ["sound mode", "effects"], "lang": "en"}
|
|
],
|
|
"settings": [
|
|
{
|
|
"setting_name": "stereo",
|
|
"setting_values": [
|
|
{"setting_synonym": ["stereo"], "lang": "en"}
|
|
],
|
|
},
|
|
{
|
|
"setting_name": "prologic",
|
|
"setting_values": [
|
|
{"setting_synonym": ["prologic"], "lang": "en"}
|
|
],
|
|
},
|
|
],
|
|
"ordered": False,
|
|
}
|
|
]
|
|
}
|
|
|
|
assert trt.query_attributes() == {
|
|
"currentModeSettings": {"sound mode": "stereo"},
|
|
"on": True,
|
|
}
|
|
|
|
assert trt.can_execute(
|
|
trait.COMMAND_SET_MODES,
|
|
params={"updateModeSettings": {"sound mode": "stereo"}},
|
|
)
|
|
|
|
calls = async_mock_service(
|
|
hass, media_player.DOMAIN, media_player.SERVICE_SELECT_SOUND_MODE
|
|
)
|
|
await trt.execute(
|
|
trait.COMMAND_SET_MODES,
|
|
BASIC_DATA,
|
|
{"updateModeSettings": {"sound mode": "stereo"}},
|
|
{},
|
|
)
|
|
|
|
assert len(calls) == 1
|
|
assert calls[0].data == {
|
|
"entity_id": "media_player.living_room",
|
|
"sound_mode": "stereo",
|
|
}
|
|
|
|
|
|
async def test_preset_modes(hass: HomeAssistant) -> None:
|
|
"""Test Mode trait for fan preset modes."""
|
|
assert helpers.get_google_type(fan.DOMAIN, None) is not None
|
|
assert trait.ModesTrait.supported(
|
|
fan.DOMAIN, FanEntityFeature.PRESET_MODE, None, None
|
|
)
|
|
|
|
trt = trait.ModesTrait(
|
|
hass,
|
|
State(
|
|
"fan.living_room",
|
|
STATE_ON,
|
|
attributes={
|
|
fan.ATTR_PRESET_MODES: ["auto", "whoosh"],
|
|
fan.ATTR_PRESET_MODE: "auto",
|
|
ATTR_SUPPORTED_FEATURES: FanEntityFeature.PRESET_MODE,
|
|
},
|
|
),
|
|
BASIC_CONFIG,
|
|
)
|
|
|
|
attribs = trt.sync_attributes()
|
|
assert attribs == {
|
|
"availableModes": [
|
|
{
|
|
"name": "preset mode",
|
|
"name_values": [
|
|
{"name_synonym": ["preset mode", "mode", "preset"], "lang": "en"}
|
|
],
|
|
"settings": [
|
|
{
|
|
"setting_name": "auto",
|
|
"setting_values": [{"setting_synonym": ["auto"], "lang": "en"}],
|
|
},
|
|
{
|
|
"setting_name": "whoosh",
|
|
"setting_values": [
|
|
{"setting_synonym": ["whoosh"], "lang": "en"}
|
|
],
|
|
},
|
|
],
|
|
"ordered": False,
|
|
}
|
|
]
|
|
}
|
|
|
|
assert trt.query_attributes() == {
|
|
"currentModeSettings": {"preset mode": "auto"},
|
|
"on": True,
|
|
}
|
|
|
|
assert trt.can_execute(
|
|
trait.COMMAND_SET_MODES,
|
|
params={"updateModeSettings": {"preset mode": "auto"}},
|
|
)
|
|
|
|
calls = async_mock_service(hass, fan.DOMAIN, fan.SERVICE_SET_PRESET_MODE)
|
|
await trt.execute(
|
|
trait.COMMAND_SET_MODES,
|
|
BASIC_DATA,
|
|
{"updateModeSettings": {"preset mode": "auto"}},
|
|
{},
|
|
)
|
|
|
|
assert len(calls) == 1
|
|
assert calls[0].data == {
|
|
"entity_id": "fan.living_room",
|
|
"preset_mode": "auto",
|
|
}
|
|
|
|
|
|
async def test_traits_unknown_domains(
|
|
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
|
|
) -> None:
|
|
"""Test Mode trait for unsupported domain."""
|
|
trt = trait.ModesTrait(
|
|
hass,
|
|
State(
|
|
"switch.living_room",
|
|
STATE_ON,
|
|
),
|
|
BASIC_CONFIG,
|
|
)
|
|
|
|
assert trt.supported("not_supported_domain", False, None, None) is False
|
|
await trt.execute(
|
|
trait.COMMAND_SET_MODES,
|
|
BASIC_DATA,
|
|
{"updateModeSettings": {}},
|
|
{},
|
|
)
|
|
assert "Received an Options command for unrecognised domain" in caplog.text
|
|
caplog.clear()
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
(
|
|
"domain",
|
|
"set_position_service",
|
|
"close_service",
|
|
"open_service",
|
|
"set_position_feature",
|
|
"attr_position",
|
|
"attr_current_position",
|
|
),
|
|
[
|
|
(
|
|
cover.DOMAIN,
|
|
cover.SERVICE_SET_COVER_POSITION,
|
|
cover.SERVICE_CLOSE_COVER,
|
|
cover.SERVICE_OPEN_COVER,
|
|
CoverEntityFeature.SET_POSITION,
|
|
cover.ATTR_POSITION,
|
|
cover.ATTR_CURRENT_POSITION,
|
|
),
|
|
(
|
|
valve.DOMAIN,
|
|
valve.SERVICE_SET_VALVE_POSITION,
|
|
valve.SERVICE_CLOSE_VALVE,
|
|
valve.SERVICE_OPEN_VALVE,
|
|
ValveEntityFeature.SET_POSITION,
|
|
valve.ATTR_POSITION,
|
|
valve.ATTR_CURRENT_POSITION,
|
|
),
|
|
],
|
|
)
|
|
async def test_openclose_cover_valve(
|
|
hass: HomeAssistant,
|
|
domain: str,
|
|
set_position_service: str,
|
|
close_service: str,
|
|
open_service: str,
|
|
set_position_feature: int,
|
|
attr_position: str,
|
|
attr_current_position: str,
|
|
) -> None:
|
|
"""Test OpenClose trait support."""
|
|
assert helpers.get_google_type(domain, None) is not None
|
|
assert trait.OpenCloseTrait.supported(domain, set_position_service, None, None)
|
|
|
|
trt = trait.OpenCloseTrait(
|
|
hass,
|
|
State(
|
|
f"{domain}.bla",
|
|
"open",
|
|
{
|
|
attr_current_position: 75,
|
|
ATTR_SUPPORTED_FEATURES: set_position_feature,
|
|
},
|
|
),
|
|
BASIC_CONFIG,
|
|
)
|
|
|
|
assert trt.sync_attributes() == {}
|
|
assert trt.query_attributes() == {"openPercent": 75}
|
|
|
|
calls_set = async_mock_service(hass, domain, set_position_service)
|
|
calls_open = async_mock_service(hass, domain, open_service)
|
|
calls_close = async_mock_service(hass, domain, close_service)
|
|
|
|
await trt.execute(trait.COMMAND_OPEN_CLOSE, BASIC_DATA, {"openPercent": 50}, {})
|
|
await trt.execute(
|
|
trait.COMMAND_OPEN_CLOSE_RELATIVE, BASIC_DATA, {"openRelativePercent": 50}, {}
|
|
)
|
|
assert len(calls_set) == 1
|
|
assert calls_set[0].data == {
|
|
ATTR_ENTITY_ID: f"{domain}.bla",
|
|
attr_position: 50,
|
|
}
|
|
calls_set.pop(0)
|
|
|
|
assert len(calls_open) == 1
|
|
assert calls_open[0].data == {ATTR_ENTITY_ID: f"{domain}.bla"}
|
|
calls_open.pop(0)
|
|
|
|
assert len(calls_close) == 0
|
|
|
|
await trt.execute(trait.COMMAND_OPEN_CLOSE, BASIC_DATA, {"openPercent": 0}, {})
|
|
await trt.execute(
|
|
trait.COMMAND_OPEN_CLOSE_RELATIVE, BASIC_DATA, {"openRelativePercent": 0}, {}
|
|
)
|
|
assert len(calls_set) == 1
|
|
assert len(calls_close) == 1
|
|
assert calls_close[0].data == {ATTR_ENTITY_ID: f"{domain}.bla"}
|
|
assert len(calls_open) == 0
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("domain", "open_service", "set_position_feature", "open_feature"),
|
|
[
|
|
(
|
|
cover.DOMAIN,
|
|
cover.SERVICE_OPEN_COVER,
|
|
CoverEntityFeature.SET_POSITION,
|
|
CoverEntityFeature.OPEN,
|
|
),
|
|
(
|
|
valve.DOMAIN,
|
|
valve.SERVICE_OPEN_VALVE,
|
|
ValveEntityFeature.SET_POSITION,
|
|
ValveEntityFeature.OPEN,
|
|
),
|
|
],
|
|
)
|
|
async def test_openclose_cover_valve_unknown_state(
|
|
hass: HomeAssistant,
|
|
open_service: str,
|
|
domain: str,
|
|
set_position_feature: int,
|
|
open_feature: int,
|
|
) -> None:
|
|
"""Test OpenClose trait support with unknown state."""
|
|
assert helpers.get_google_type(domain, None) is not None
|
|
assert trait.OpenCloseTrait.supported(
|
|
cover.DOMAIN, set_position_feature, None, None
|
|
)
|
|
|
|
# No state
|
|
trt = trait.OpenCloseTrait(
|
|
hass,
|
|
State(
|
|
f"{domain}.bla",
|
|
STATE_UNKNOWN,
|
|
{ATTR_SUPPORTED_FEATURES: open_feature},
|
|
),
|
|
BASIC_CONFIG,
|
|
)
|
|
|
|
assert trt.sync_attributes() == {"discreteOnlyOpenClose": True}
|
|
|
|
with pytest.raises(helpers.SmartHomeError):
|
|
trt.query_attributes()
|
|
|
|
calls = async_mock_service(hass, domain, open_service)
|
|
await trt.execute(trait.COMMAND_OPEN_CLOSE, BASIC_DATA, {"openPercent": 100}, {})
|
|
assert len(calls) == 1
|
|
assert calls[0].data == {ATTR_ENTITY_ID: f"{domain}.bla"}
|
|
|
|
with pytest.raises(helpers.SmartHomeError):
|
|
trt.query_attributes()
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("domain", "set_position_service", "set_position_feature", "state_open"),
|
|
[
|
|
(
|
|
cover.DOMAIN,
|
|
cover.SERVICE_SET_COVER_POSITION,
|
|
CoverEntityFeature.SET_POSITION,
|
|
cover.STATE_OPEN,
|
|
),
|
|
(
|
|
valve.DOMAIN,
|
|
valve.SERVICE_SET_VALVE_POSITION,
|
|
ValveEntityFeature.SET_POSITION,
|
|
valve.ValveState.OPEN,
|
|
),
|
|
],
|
|
)
|
|
async def test_openclose_cover_valve_assumed_state(
|
|
hass: HomeAssistant,
|
|
domain: str,
|
|
set_position_service: str,
|
|
set_position_feature: int,
|
|
state_open: str,
|
|
) -> None:
|
|
"""Test OpenClose trait support."""
|
|
assert helpers.get_google_type(domain, None) is not None
|
|
assert trait.OpenCloseTrait.supported(domain, set_position_feature, None, None)
|
|
|
|
trt = trait.OpenCloseTrait(
|
|
hass,
|
|
State(
|
|
f"{domain}.bla",
|
|
state_open,
|
|
{
|
|
ATTR_ASSUMED_STATE: True,
|
|
ATTR_SUPPORTED_FEATURES: set_position_feature,
|
|
},
|
|
),
|
|
BASIC_CONFIG,
|
|
)
|
|
|
|
assert trt.sync_attributes() == {"commandOnlyOpenClose": True}
|
|
|
|
assert trt.query_attributes() == {}
|
|
|
|
calls = async_mock_service(hass, domain, set_position_service)
|
|
await trt.execute(trait.COMMAND_OPEN_CLOSE, BASIC_DATA, {"openPercent": 40}, {})
|
|
assert len(calls) == 1
|
|
assert calls[0].data == {ATTR_ENTITY_ID: f"{domain}.bla", cover.ATTR_POSITION: 40}
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("domain", "state_open"),
|
|
[
|
|
(
|
|
cover.DOMAIN,
|
|
cover.STATE_OPEN,
|
|
),
|
|
(
|
|
valve.DOMAIN,
|
|
valve.ValveState.OPEN,
|
|
),
|
|
],
|
|
)
|
|
async def test_openclose_cover_valve_query_only(
|
|
hass: HomeAssistant,
|
|
domain: str,
|
|
state_open: str,
|
|
) -> None:
|
|
"""Test OpenClose trait support."""
|
|
assert helpers.get_google_type(domain, None) is not None
|
|
assert trait.OpenCloseTrait.supported(domain, 0, None, None)
|
|
|
|
state = State(
|
|
f"{domain}.bla",
|
|
state_open,
|
|
)
|
|
|
|
trt = trait.OpenCloseTrait(
|
|
hass,
|
|
state,
|
|
BASIC_CONFIG,
|
|
)
|
|
|
|
assert trt.sync_attributes() == {
|
|
"discreteOnlyOpenClose": True,
|
|
"queryOnlyOpenClose": True,
|
|
}
|
|
assert trt.query_attributes() == {"openPercent": 100}
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
(
|
|
"domain",
|
|
"state_open",
|
|
"state_closed",
|
|
"supported_features",
|
|
"open_service",
|
|
"close_service",
|
|
),
|
|
[
|
|
(
|
|
cover.DOMAIN,
|
|
cover.STATE_OPEN,
|
|
cover.STATE_CLOSED,
|
|
CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE,
|
|
cover.SERVICE_OPEN_COVER,
|
|
cover.SERVICE_CLOSE_COVER,
|
|
),
|
|
(
|
|
valve.DOMAIN,
|
|
valve.ValveState.OPEN,
|
|
valve.ValveState.CLOSED,
|
|
ValveEntityFeature.OPEN | ValveEntityFeature.CLOSE,
|
|
valve.SERVICE_OPEN_VALVE,
|
|
valve.SERVICE_CLOSE_VALVE,
|
|
),
|
|
],
|
|
)
|
|
async def test_openclose_cover_valve_no_position(
|
|
hass: HomeAssistant,
|
|
domain: str,
|
|
state_open: str,
|
|
state_closed: str,
|
|
supported_features: int,
|
|
open_service: str,
|
|
close_service: str,
|
|
) -> None:
|
|
"""Test OpenClose trait support."""
|
|
assert helpers.get_google_type(domain, None) is not None
|
|
assert trait.OpenCloseTrait.supported(
|
|
domain,
|
|
supported_features,
|
|
None,
|
|
None,
|
|
)
|
|
|
|
state = State(
|
|
f"{domain}.bla",
|
|
state_open,
|
|
{
|
|
ATTR_SUPPORTED_FEATURES: supported_features,
|
|
},
|
|
)
|
|
|
|
trt = trait.OpenCloseTrait(
|
|
hass,
|
|
state,
|
|
BASIC_CONFIG,
|
|
)
|
|
|
|
assert trt.sync_attributes() == {"discreteOnlyOpenClose": True}
|
|
assert trt.query_attributes() == {"openPercent": 100}
|
|
|
|
state.state = state_closed
|
|
|
|
assert trt.sync_attributes() == {"discreteOnlyOpenClose": True}
|
|
assert trt.query_attributes() == {"openPercent": 0}
|
|
|
|
calls = async_mock_service(hass, domain, close_service)
|
|
await trt.execute(trait.COMMAND_OPEN_CLOSE, BASIC_DATA, {"openPercent": 0}, {})
|
|
assert len(calls) == 1
|
|
assert calls[0].data == {ATTR_ENTITY_ID: f"{domain}.bla"}
|
|
|
|
calls = async_mock_service(hass, domain, open_service)
|
|
await trt.execute(trait.COMMAND_OPEN_CLOSE, BASIC_DATA, {"openPercent": 100}, {})
|
|
assert len(calls) == 1
|
|
assert calls[0].data == {ATTR_ENTITY_ID: f"{domain}.bla"}
|
|
|
|
with pytest.raises(
|
|
SmartHomeError, match=r"Current position not know for relative command"
|
|
):
|
|
await trt.execute(
|
|
trait.COMMAND_OPEN_CLOSE_RELATIVE,
|
|
BASIC_DATA,
|
|
{"openRelativePercent": 100},
|
|
{},
|
|
)
|
|
|
|
with pytest.raises(SmartHomeError, match=r"No support for partial open close"):
|
|
await trt.execute(trait.COMMAND_OPEN_CLOSE, BASIC_DATA, {"openPercent": 50}, {})
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"device_class",
|
|
[
|
|
cover.CoverDeviceClass.DOOR,
|
|
cover.CoverDeviceClass.GARAGE,
|
|
cover.CoverDeviceClass.GATE,
|
|
],
|
|
)
|
|
async def test_openclose_cover_secure(hass: HomeAssistant, device_class) -> None:
|
|
"""Test OpenClose trait support for cover domain."""
|
|
assert helpers.get_google_type(cover.DOMAIN, device_class) is not None
|
|
assert trait.OpenCloseTrait.supported(
|
|
cover.DOMAIN, CoverEntityFeature.SET_POSITION, device_class, None
|
|
)
|
|
assert trait.OpenCloseTrait.might_2fa(
|
|
cover.DOMAIN, CoverEntityFeature.SET_POSITION, device_class
|
|
)
|
|
|
|
trt = trait.OpenCloseTrait(
|
|
hass,
|
|
State(
|
|
"cover.bla",
|
|
cover.STATE_OPEN,
|
|
{
|
|
ATTR_DEVICE_CLASS: device_class,
|
|
ATTR_SUPPORTED_FEATURES: CoverEntityFeature.SET_POSITION,
|
|
cover.ATTR_CURRENT_POSITION: 75,
|
|
},
|
|
),
|
|
PIN_CONFIG,
|
|
)
|
|
|
|
assert trt.sync_attributes() == {}
|
|
assert trt.query_attributes() == {"openPercent": 75}
|
|
|
|
calls = async_mock_service(hass, cover.DOMAIN, cover.SERVICE_SET_COVER_POSITION)
|
|
calls_close = async_mock_service(hass, cover.DOMAIN, cover.SERVICE_CLOSE_COVER)
|
|
|
|
# No challenge data
|
|
with pytest.raises(error.ChallengeNeeded) as err:
|
|
await trt.execute(trait.COMMAND_OPEN_CLOSE, PIN_DATA, {"openPercent": 50}, {})
|
|
assert len(calls) == 0
|
|
assert err.value.code == const.ERR_CHALLENGE_NEEDED
|
|
assert err.value.challenge_type == const.CHALLENGE_PIN_NEEDED
|
|
|
|
# invalid pin
|
|
with pytest.raises(error.ChallengeNeeded) as err:
|
|
await trt.execute(
|
|
trait.COMMAND_OPEN_CLOSE, PIN_DATA, {"openPercent": 50}, {"pin": "9999"}
|
|
)
|
|
assert len(calls) == 0
|
|
assert err.value.code == const.ERR_CHALLENGE_NEEDED
|
|
assert err.value.challenge_type == const.CHALLENGE_FAILED_PIN_NEEDED
|
|
|
|
await trt.execute(
|
|
trait.COMMAND_OPEN_CLOSE, PIN_DATA, {"openPercent": 50}, {"pin": "1234"}
|
|
)
|
|
assert len(calls) == 1
|
|
assert calls[0].data == {ATTR_ENTITY_ID: "cover.bla", cover.ATTR_POSITION: 50}
|
|
|
|
# no challenge on close
|
|
await trt.execute(trait.COMMAND_OPEN_CLOSE, PIN_DATA, {"openPercent": 0}, {})
|
|
assert len(calls_close) == 1
|
|
assert calls_close[0].data == {ATTR_ENTITY_ID: "cover.bla"}
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"device_class",
|
|
[
|
|
binary_sensor.BinarySensorDeviceClass.DOOR,
|
|
binary_sensor.BinarySensorDeviceClass.GARAGE_DOOR,
|
|
binary_sensor.BinarySensorDeviceClass.LOCK,
|
|
binary_sensor.BinarySensorDeviceClass.OPENING,
|
|
binary_sensor.BinarySensorDeviceClass.WINDOW,
|
|
],
|
|
)
|
|
async def test_openclose_binary_sensor(hass: HomeAssistant, device_class) -> None:
|
|
"""Test OpenClose trait support for binary_sensor domain."""
|
|
assert helpers.get_google_type(binary_sensor.DOMAIN, device_class) is not None
|
|
assert trait.OpenCloseTrait.supported(binary_sensor.DOMAIN, 0, device_class, None)
|
|
|
|
trt = trait.OpenCloseTrait(
|
|
hass,
|
|
State("binary_sensor.test", STATE_ON, {ATTR_DEVICE_CLASS: device_class}),
|
|
BASIC_CONFIG,
|
|
)
|
|
|
|
assert trt.sync_attributes() == {
|
|
"queryOnlyOpenClose": True,
|
|
"discreteOnlyOpenClose": True,
|
|
}
|
|
|
|
assert trt.query_attributes() == {"openPercent": 100}
|
|
|
|
trt = trait.OpenCloseTrait(
|
|
hass,
|
|
State("binary_sensor.test", STATE_OFF, {ATTR_DEVICE_CLASS: device_class}),
|
|
BASIC_CONFIG,
|
|
)
|
|
|
|
assert trt.sync_attributes() == {
|
|
"queryOnlyOpenClose": True,
|
|
"discreteOnlyOpenClose": True,
|
|
}
|
|
|
|
assert trt.query_attributes() == {"openPercent": 0}
|
|
|
|
|
|
async def test_volume_media_player(hass: HomeAssistant) -> None:
|
|
"""Test volume trait support for media player domain."""
|
|
assert helpers.get_google_type(media_player.DOMAIN, None) is not None
|
|
assert trait.VolumeTrait.supported(
|
|
media_player.DOMAIN,
|
|
MediaPlayerEntityFeature.VOLUME_SET,
|
|
None,
|
|
None,
|
|
)
|
|
|
|
trt = trait.VolumeTrait(
|
|
hass,
|
|
State(
|
|
"media_player.bla",
|
|
media_player.STATE_PLAYING,
|
|
{
|
|
ATTR_SUPPORTED_FEATURES: MediaPlayerEntityFeature.VOLUME_SET,
|
|
media_player.ATTR_MEDIA_VOLUME_LEVEL: 0.3,
|
|
},
|
|
),
|
|
BASIC_CONFIG,
|
|
)
|
|
|
|
assert trt.sync_attributes() == {
|
|
"volumeMaxLevel": 100,
|
|
"levelStepSize": 10,
|
|
"volumeCanMuteAndUnmute": False,
|
|
"commandOnlyVolume": False,
|
|
}
|
|
|
|
assert trt.query_attributes() == {"currentVolume": 30}
|
|
|
|
calls = async_mock_service(
|
|
hass, media_player.DOMAIN, media_player.SERVICE_VOLUME_SET
|
|
)
|
|
await trt.execute(trait.COMMAND_SET_VOLUME, BASIC_DATA, {"volumeLevel": 60}, {})
|
|
assert len(calls) == 1
|
|
assert calls[0].data == {
|
|
ATTR_ENTITY_ID: "media_player.bla",
|
|
media_player.ATTR_MEDIA_VOLUME_LEVEL: 0.6,
|
|
}
|
|
|
|
calls = async_mock_service(
|
|
hass, media_player.DOMAIN, media_player.SERVICE_VOLUME_SET
|
|
)
|
|
await trt.execute(
|
|
trait.COMMAND_VOLUME_RELATIVE, BASIC_DATA, {"relativeSteps": 10}, {}
|
|
)
|
|
assert len(calls) == 1
|
|
assert calls[0].data == {
|
|
ATTR_ENTITY_ID: "media_player.bla",
|
|
media_player.ATTR_MEDIA_VOLUME_LEVEL: 0.4,
|
|
}
|
|
|
|
|
|
async def test_volume_media_player_relative(hass: HomeAssistant) -> None:
|
|
"""Test volume trait support for relative-volume-only media players."""
|
|
assert trait.VolumeTrait.supported(
|
|
media_player.DOMAIN,
|
|
MediaPlayerEntityFeature.VOLUME_STEP,
|
|
None,
|
|
None,
|
|
)
|
|
trt = trait.VolumeTrait(
|
|
hass,
|
|
State(
|
|
"media_player.bla",
|
|
media_player.STATE_PLAYING,
|
|
{
|
|
ATTR_ASSUMED_STATE: True,
|
|
ATTR_SUPPORTED_FEATURES: MediaPlayerEntityFeature.VOLUME_STEP,
|
|
},
|
|
),
|
|
BASIC_CONFIG,
|
|
)
|
|
|
|
assert trt.sync_attributes() == {
|
|
"volumeMaxLevel": 100,
|
|
"levelStepSize": 10,
|
|
"volumeCanMuteAndUnmute": False,
|
|
"commandOnlyVolume": True,
|
|
}
|
|
|
|
assert trt.query_attributes() == {}
|
|
|
|
calls = async_mock_service(
|
|
hass, media_player.DOMAIN, media_player.SERVICE_VOLUME_UP
|
|
)
|
|
|
|
await trt.execute(
|
|
trait.COMMAND_VOLUME_RELATIVE,
|
|
BASIC_DATA,
|
|
{"relativeSteps": 10},
|
|
{},
|
|
)
|
|
assert len(calls) == 10
|
|
for call in calls:
|
|
assert call.data == {
|
|
ATTR_ENTITY_ID: "media_player.bla",
|
|
}
|
|
|
|
calls = async_mock_service(
|
|
hass, media_player.DOMAIN, media_player.SERVICE_VOLUME_DOWN
|
|
)
|
|
await trt.execute(
|
|
trait.COMMAND_VOLUME_RELATIVE,
|
|
BASIC_DATA,
|
|
{"relativeSteps": -10},
|
|
{},
|
|
)
|
|
assert len(calls) == 10
|
|
for call in calls:
|
|
assert call.data == {
|
|
ATTR_ENTITY_ID: "media_player.bla",
|
|
}
|
|
|
|
with pytest.raises(SmartHomeError):
|
|
await trt.execute(trait.COMMAND_SET_VOLUME, BASIC_DATA, {"volumeLevel": 42}, {})
|
|
|
|
with pytest.raises(SmartHomeError):
|
|
await trt.execute(trait.COMMAND_MUTE, BASIC_DATA, {"mute": True}, {})
|
|
|
|
|
|
async def test_media_player_mute(hass: HomeAssistant) -> None:
|
|
"""Test volume trait support for muting."""
|
|
assert trait.VolumeTrait.supported(
|
|
media_player.DOMAIN,
|
|
MediaPlayerEntityFeature.VOLUME_STEP | MediaPlayerEntityFeature.VOLUME_MUTE,
|
|
None,
|
|
None,
|
|
)
|
|
trt = trait.VolumeTrait(
|
|
hass,
|
|
State(
|
|
"media_player.bla",
|
|
media_player.STATE_PLAYING,
|
|
{
|
|
ATTR_SUPPORTED_FEATURES: (
|
|
MediaPlayerEntityFeature.VOLUME_STEP
|
|
| MediaPlayerEntityFeature.VOLUME_MUTE
|
|
),
|
|
media_player.ATTR_MEDIA_VOLUME_MUTED: False,
|
|
},
|
|
),
|
|
BASIC_CONFIG,
|
|
)
|
|
|
|
assert trt.sync_attributes() == {
|
|
"volumeMaxLevel": 100,
|
|
"levelStepSize": 10,
|
|
"volumeCanMuteAndUnmute": True,
|
|
"commandOnlyVolume": False,
|
|
}
|
|
assert trt.query_attributes() == {"isMuted": False}
|
|
|
|
mute_calls = async_mock_service(
|
|
hass, media_player.DOMAIN, media_player.SERVICE_VOLUME_MUTE
|
|
)
|
|
await trt.execute(
|
|
trait.COMMAND_MUTE,
|
|
BASIC_DATA,
|
|
{"mute": True},
|
|
{},
|
|
)
|
|
assert len(mute_calls) == 1
|
|
assert mute_calls[0].data == {
|
|
ATTR_ENTITY_ID: "media_player.bla",
|
|
media_player.ATTR_MEDIA_VOLUME_MUTED: True,
|
|
}
|
|
|
|
unmute_calls = async_mock_service(
|
|
hass, media_player.DOMAIN, media_player.SERVICE_VOLUME_MUTE
|
|
)
|
|
await trt.execute(
|
|
trait.COMMAND_MUTE,
|
|
BASIC_DATA,
|
|
{"mute": False},
|
|
{},
|
|
)
|
|
assert len(unmute_calls) == 1
|
|
assert unmute_calls[0].data == {
|
|
ATTR_ENTITY_ID: "media_player.bla",
|
|
media_player.ATTR_MEDIA_VOLUME_MUTED: False,
|
|
}
|
|
|
|
|
|
async def test_temperature_control_sensor(hass: HomeAssistant) -> None:
|
|
"""Test TemperatureControl trait support for temperature sensor."""
|
|
assert (
|
|
helpers.get_google_type(sensor.DOMAIN, sensor.SensorDeviceClass.TEMPERATURE)
|
|
is not None
|
|
)
|
|
assert not trait.TemperatureControlTrait.supported(
|
|
sensor.DOMAIN, 0, sensor.SensorDeviceClass.HUMIDITY, None
|
|
)
|
|
assert trait.TemperatureControlTrait.supported(
|
|
sensor.DOMAIN, 0, sensor.SensorDeviceClass.TEMPERATURE, None
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("unit_in", "unit_out", "state", "ambient"),
|
|
[
|
|
(UnitOfTemperature.FAHRENHEIT, "F", "70", 21.1),
|
|
(UnitOfTemperature.CELSIUS, "C", "21.1", 21.1),
|
|
(UnitOfTemperature.FAHRENHEIT, "F", "unavailable", None),
|
|
(UnitOfTemperature.FAHRENHEIT, "F", "unknown", None),
|
|
],
|
|
)
|
|
async def test_temperature_control_sensor_data(
|
|
hass: HomeAssistant, unit_in, unit_out, state, ambient
|
|
) -> None:
|
|
"""Test TemperatureControl trait support for temperature sensor."""
|
|
hass.config.units.temperature_unit = unit_in
|
|
|
|
trt = trait.TemperatureControlTrait(
|
|
hass,
|
|
State(
|
|
"sensor.test",
|
|
state,
|
|
{ATTR_DEVICE_CLASS: sensor.SensorDeviceClass.TEMPERATURE},
|
|
),
|
|
BASIC_CONFIG,
|
|
)
|
|
|
|
assert trt.sync_attributes() == {
|
|
"queryOnlyTemperatureControl": True,
|
|
"temperatureUnitForUX": unit_out,
|
|
"temperatureRange": {"maxThresholdCelsius": 100, "minThresholdCelsius": -100},
|
|
}
|
|
|
|
if ambient:
|
|
assert trt.query_attributes() == {
|
|
"temperatureAmbientCelsius": ambient,
|
|
"temperatureSetpointCelsius": ambient,
|
|
}
|
|
else:
|
|
assert trt.query_attributes() == {}
|
|
hass.config.units.temperature_unit = UnitOfTemperature.CELSIUS
|
|
|
|
|
|
async def test_humidity_setting_sensor(hass: HomeAssistant) -> None:
|
|
"""Test HumiditySetting trait support for humidity sensor."""
|
|
assert (
|
|
helpers.get_google_type(sensor.DOMAIN, sensor.SensorDeviceClass.HUMIDITY)
|
|
is not None
|
|
)
|
|
assert not trait.HumiditySettingTrait.supported(
|
|
sensor.DOMAIN, 0, sensor.SensorDeviceClass.TEMPERATURE, None
|
|
)
|
|
assert trait.HumiditySettingTrait.supported(
|
|
sensor.DOMAIN, 0, sensor.SensorDeviceClass.HUMIDITY, None
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("state", "ambient"), [("70", 70), ("unavailable", None), ("unknown", None)]
|
|
)
|
|
async def test_humidity_setting_sensor_data(
|
|
hass: HomeAssistant, state, ambient
|
|
) -> None:
|
|
"""Test HumiditySetting trait support for humidity sensor."""
|
|
trt = trait.HumiditySettingTrait(
|
|
hass,
|
|
State(
|
|
"sensor.test", state, {ATTR_DEVICE_CLASS: sensor.SensorDeviceClass.HUMIDITY}
|
|
),
|
|
BASIC_CONFIG,
|
|
)
|
|
|
|
assert trt.sync_attributes() == {"queryOnlyHumiditySetting": True}
|
|
if ambient:
|
|
assert trt.query_attributes() == {"humidityAmbientPercent": ambient}
|
|
else:
|
|
assert trt.query_attributes() == {}
|
|
|
|
with pytest.raises(helpers.SmartHomeError) as err:
|
|
await trt.execute(trait.COMMAND_ON_OFF, BASIC_DATA, {"on": False}, {})
|
|
assert err.value.code == const.ERR_NOT_SUPPORTED
|
|
|
|
|
|
async def test_transport_control(
|
|
hass: HomeAssistant, freezer: FrozenDateTimeFactory
|
|
) -> None:
|
|
"""Test the TransportControlTrait."""
|
|
assert helpers.get_google_type(media_player.DOMAIN, None) is not None
|
|
|
|
for feature in trait.MEDIA_COMMAND_SUPPORT_MAPPING.values():
|
|
assert trait.TransportControlTrait.supported(
|
|
media_player.DOMAIN, feature, None, None
|
|
)
|
|
|
|
now = datetime(2020, 1, 1, tzinfo=dt_util.UTC)
|
|
|
|
trt = trait.TransportControlTrait(
|
|
hass,
|
|
State(
|
|
"media_player.bla",
|
|
media_player.STATE_PLAYING,
|
|
{
|
|
media_player.ATTR_MEDIA_POSITION: 100,
|
|
media_player.ATTR_MEDIA_DURATION: 200,
|
|
media_player.ATTR_MEDIA_POSITION_UPDATED_AT: now
|
|
- timedelta(seconds=10),
|
|
media_player.ATTR_MEDIA_VOLUME_LEVEL: 0.5,
|
|
ATTR_SUPPORTED_FEATURES: MediaPlayerEntityFeature.PLAY
|
|
| MediaPlayerEntityFeature.STOP,
|
|
},
|
|
),
|
|
BASIC_CONFIG,
|
|
)
|
|
|
|
assert trt.sync_attributes() == {
|
|
"transportControlSupportedCommands": ["RESUME", "STOP"]
|
|
}
|
|
assert trt.query_attributes() == {}
|
|
|
|
# COMMAND_MEDIA_SEEK_RELATIVE
|
|
calls = async_mock_service(
|
|
hass, media_player.DOMAIN, media_player.SERVICE_MEDIA_SEEK
|
|
)
|
|
|
|
# Patch to avoid time ticking over during the command failing the test
|
|
freezer.move_to(now)
|
|
await trt.execute(
|
|
trait.COMMAND_MEDIA_SEEK_RELATIVE,
|
|
BASIC_DATA,
|
|
{"relativePositionMs": 10000},
|
|
{},
|
|
)
|
|
assert len(calls) == 1
|
|
assert calls[0].data == {
|
|
ATTR_ENTITY_ID: "media_player.bla",
|
|
# 100s (current position) + 10s (from command) + 10s (from updated_at)
|
|
media_player.ATTR_MEDIA_SEEK_POSITION: 120,
|
|
}
|
|
|
|
# COMMAND_MEDIA_SEEK_TO_POSITION
|
|
calls = async_mock_service(
|
|
hass, media_player.DOMAIN, media_player.SERVICE_MEDIA_SEEK
|
|
)
|
|
await trt.execute(
|
|
trait.COMMAND_MEDIA_SEEK_TO_POSITION, BASIC_DATA, {"absPositionMs": 50000}, {}
|
|
)
|
|
assert len(calls) == 1
|
|
assert calls[0].data == {
|
|
ATTR_ENTITY_ID: "media_player.bla",
|
|
media_player.ATTR_MEDIA_SEEK_POSITION: 50,
|
|
}
|
|
|
|
# COMMAND_MEDIA_NEXT
|
|
calls = async_mock_service(
|
|
hass, media_player.DOMAIN, media_player.SERVICE_MEDIA_NEXT_TRACK
|
|
)
|
|
await trt.execute(trait.COMMAND_MEDIA_NEXT, BASIC_DATA, {}, {})
|
|
assert len(calls) == 1
|
|
assert calls[0].data == {ATTR_ENTITY_ID: "media_player.bla"}
|
|
|
|
# COMMAND_MEDIA_PAUSE
|
|
calls = async_mock_service(
|
|
hass, media_player.DOMAIN, media_player.SERVICE_MEDIA_PAUSE
|
|
)
|
|
await trt.execute(trait.COMMAND_MEDIA_PAUSE, BASIC_DATA, {}, {})
|
|
assert len(calls) == 1
|
|
assert calls[0].data == {ATTR_ENTITY_ID: "media_player.bla"}
|
|
|
|
# COMMAND_MEDIA_PREVIOUS
|
|
calls = async_mock_service(
|
|
hass, media_player.DOMAIN, media_player.SERVICE_MEDIA_PREVIOUS_TRACK
|
|
)
|
|
await trt.execute(trait.COMMAND_MEDIA_PREVIOUS, BASIC_DATA, {}, {})
|
|
assert len(calls) == 1
|
|
assert calls[0].data == {ATTR_ENTITY_ID: "media_player.bla"}
|
|
|
|
# COMMAND_MEDIA_RESUME
|
|
calls = async_mock_service(
|
|
hass, media_player.DOMAIN, media_player.SERVICE_MEDIA_PLAY
|
|
)
|
|
await trt.execute(trait.COMMAND_MEDIA_RESUME, BASIC_DATA, {}, {})
|
|
assert len(calls) == 1
|
|
assert calls[0].data == {ATTR_ENTITY_ID: "media_player.bla"}
|
|
|
|
# COMMAND_MEDIA_SHUFFLE
|
|
calls = async_mock_service(
|
|
hass, media_player.DOMAIN, media_player.SERVICE_SHUFFLE_SET
|
|
)
|
|
await trt.execute(trait.COMMAND_MEDIA_SHUFFLE, BASIC_DATA, {}, {})
|
|
assert len(calls) == 1
|
|
assert calls[0].data == {
|
|
ATTR_ENTITY_ID: "media_player.bla",
|
|
media_player.ATTR_MEDIA_SHUFFLE: True,
|
|
}
|
|
|
|
# COMMAND_MEDIA_STOP
|
|
calls = async_mock_service(
|
|
hass, media_player.DOMAIN, media_player.SERVICE_MEDIA_STOP
|
|
)
|
|
await trt.execute(trait.COMMAND_MEDIA_STOP, BASIC_DATA, {}, {})
|
|
assert len(calls) == 1
|
|
assert calls[0].data == {ATTR_ENTITY_ID: "media_player.bla"}
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"state",
|
|
[
|
|
STATE_OFF,
|
|
STATE_IDLE,
|
|
STATE_PLAYING,
|
|
STATE_ON,
|
|
STATE_PAUSED,
|
|
STATE_STANDBY,
|
|
STATE_UNAVAILABLE,
|
|
STATE_UNKNOWN,
|
|
],
|
|
)
|
|
async def test_media_state(hass: HomeAssistant, state) -> None:
|
|
"""Test the MediaStateTrait."""
|
|
assert helpers.get_google_type(media_player.DOMAIN, None) is not None
|
|
|
|
assert trait.TransportControlTrait.supported(
|
|
media_player.DOMAIN, MediaPlayerEntityFeature.PLAY, None, None
|
|
)
|
|
|
|
trt = trait.MediaStateTrait(
|
|
hass,
|
|
State(
|
|
"media_player.bla",
|
|
state,
|
|
{
|
|
media_player.ATTR_MEDIA_POSITION: 100,
|
|
media_player.ATTR_MEDIA_DURATION: 200,
|
|
media_player.ATTR_MEDIA_VOLUME_LEVEL: 0.5,
|
|
ATTR_SUPPORTED_FEATURES: MediaPlayerEntityFeature.PLAY
|
|
| MediaPlayerEntityFeature.STOP,
|
|
},
|
|
),
|
|
BASIC_CONFIG,
|
|
)
|
|
|
|
assert trt.sync_attributes() == {
|
|
"supportActivityState": True,
|
|
"supportPlaybackState": True,
|
|
}
|
|
assert trt.query_attributes() == {
|
|
"activityState": trt.activity_lookup.get(state),
|
|
"playbackState": trt.playback_lookup.get(state),
|
|
}
|
|
|
|
|
|
async def test_channel(hass: HomeAssistant) -> None:
|
|
"""Test Channel trait support."""
|
|
assert helpers.get_google_type(media_player.DOMAIN, None) is not None
|
|
assert trait.ChannelTrait.supported(
|
|
media_player.DOMAIN,
|
|
MediaPlayerEntityFeature.PLAY_MEDIA,
|
|
media_player.MediaPlayerDeviceClass.TV,
|
|
None,
|
|
)
|
|
assert (
|
|
trait.ChannelTrait.supported(
|
|
media_player.DOMAIN,
|
|
MediaPlayerEntityFeature.PLAY_MEDIA,
|
|
None,
|
|
None,
|
|
)
|
|
is False
|
|
)
|
|
assert trait.ChannelTrait.supported(media_player.DOMAIN, 0, None, None) is False
|
|
|
|
trt = trait.ChannelTrait(hass, State("media_player.demo", STATE_ON), BASIC_CONFIG)
|
|
|
|
assert trt.sync_attributes() == {
|
|
"availableChannels": [],
|
|
"commandOnlyChannels": True,
|
|
}
|
|
assert trt.query_attributes() == {}
|
|
|
|
media_player_calls = async_mock_service(
|
|
hass, media_player.DOMAIN, SERVICE_PLAY_MEDIA
|
|
)
|
|
await trt.execute(
|
|
trait.COMMAND_SELECT_CHANNEL, BASIC_DATA, {"channelNumber": "1"}, {}
|
|
)
|
|
assert len(media_player_calls) == 1
|
|
assert media_player_calls[0].data == {
|
|
ATTR_ENTITY_ID: "media_player.demo",
|
|
media_player.ATTR_MEDIA_CONTENT_ID: "1",
|
|
media_player.ATTR_MEDIA_CONTENT_TYPE: MediaType.CHANNEL,
|
|
}
|
|
|
|
with pytest.raises(SmartHomeError, match="Channel is not available"):
|
|
await trt.execute(
|
|
trait.COMMAND_SELECT_CHANNEL, BASIC_DATA, {"channelCode": "Channel 3"}, {}
|
|
)
|
|
assert len(media_player_calls) == 1
|
|
|
|
with pytest.raises(SmartHomeError, match="Unsupported command"):
|
|
await trt.execute("Unknown command", BASIC_DATA, {"channelNumber": "1"}, {})
|
|
assert len(media_player_calls) == 1
|
|
|
|
|
|
async def test_air_quality_description_for_aqi(hass: HomeAssistant) -> None:
|
|
"""Test air quality description for a given AQI value."""
|
|
trt = trait.SensorStateTrait(
|
|
hass,
|
|
State(
|
|
"sensor.test",
|
|
100.0,
|
|
{
|
|
"device_class": sensor.SensorDeviceClass.AQI,
|
|
},
|
|
),
|
|
BASIC_CONFIG,
|
|
)
|
|
|
|
assert trt._air_quality_description_for_aqi(0) == "healthy"
|
|
assert trt._air_quality_description_for_aqi(75) == "moderate"
|
|
assert (
|
|
trt._air_quality_description_for_aqi(125.0) == "unhealthy for sensitive groups"
|
|
)
|
|
assert trt._air_quality_description_for_aqi(175) == "unhealthy"
|
|
assert trt._air_quality_description_for_aqi(250) == "very unhealthy"
|
|
assert trt._air_quality_description_for_aqi(350) == "hazardous"
|
|
assert trt._air_quality_description_for_aqi(-1) == "unknown"
|
|
|
|
|
|
async def test_null_device_class(hass: HomeAssistant) -> None:
|
|
"""Test handling a null device_class in sync_attributes and query_attributes."""
|
|
trt = trait.SensorStateTrait(
|
|
hass,
|
|
State(
|
|
"sensor.test",
|
|
100.0,
|
|
{
|
|
"device_class": None,
|
|
},
|
|
),
|
|
BASIC_CONFIG,
|
|
)
|
|
|
|
assert trt.sync_attributes() == {}
|
|
assert trt.query_attributes() == {}
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("value", "published", "aqi"),
|
|
[
|
|
(100.0, 100.0, "moderate"),
|
|
(10.0, 10.0, "healthy"),
|
|
(0, 0.0, "healthy"),
|
|
("", None, "unknown"),
|
|
("unknown", None, "unknown"),
|
|
],
|
|
)
|
|
async def test_sensorstate(
|
|
hass: HomeAssistant, value: Any, published: Any, aqi: Any
|
|
) -> None:
|
|
"""Test SensorState trait support for sensor domain."""
|
|
sensor_types = {
|
|
sensor.SensorDeviceClass.AQI: ("AirQuality", "AQI"),
|
|
sensor.SensorDeviceClass.CO: ("CarbonMonoxideLevel", "PARTS_PER_MILLION"),
|
|
sensor.SensorDeviceClass.CO2: ("CarbonDioxideLevel", "PARTS_PER_MILLION"),
|
|
sensor.SensorDeviceClass.PM25: ("PM2.5", "MICROGRAMS_PER_CUBIC_METER"),
|
|
sensor.SensorDeviceClass.PM10: ("PM10", "MICROGRAMS_PER_CUBIC_METER"),
|
|
sensor.SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS: (
|
|
"VolatileOrganicCompounds",
|
|
"PARTS_PER_MILLION",
|
|
),
|
|
}
|
|
|
|
for sensor_type, item in sensor_types.items():
|
|
assert helpers.get_google_type(sensor.DOMAIN, None) is not None
|
|
assert trait.SensorStateTrait.supported(sensor.DOMAIN, None, sensor_type, None)
|
|
|
|
trt = trait.SensorStateTrait(
|
|
hass,
|
|
State(
|
|
"sensor.test",
|
|
value,
|
|
{
|
|
"device_class": sensor_type,
|
|
},
|
|
),
|
|
BASIC_CONFIG,
|
|
)
|
|
|
|
name = item[0]
|
|
unit = item[1]
|
|
|
|
if sensor_type == sensor.SensorDeviceClass.AQI:
|
|
assert trt.sync_attributes() == {
|
|
"sensorStatesSupported": [
|
|
{
|
|
"name": name,
|
|
"numericCapabilities": {"rawValueUnit": unit},
|
|
"descriptiveCapabilities": {
|
|
"availableStates": [
|
|
"healthy",
|
|
"moderate",
|
|
"unhealthy for sensitive groups",
|
|
"unhealthy",
|
|
"very unhealthy",
|
|
"hazardous",
|
|
"unknown",
|
|
],
|
|
},
|
|
}
|
|
]
|
|
}
|
|
else:
|
|
assert trt.sync_attributes() == {
|
|
"sensorStatesSupported": [
|
|
{
|
|
"name": name,
|
|
"numericCapabilities": {"rawValueUnit": unit},
|
|
}
|
|
]
|
|
}
|
|
|
|
if sensor_type == sensor.SensorDeviceClass.AQI:
|
|
assert trt.query_attributes() == {
|
|
"currentSensorStateData": [
|
|
{
|
|
"name": name,
|
|
"currentSensorState": aqi,
|
|
"rawValue": published,
|
|
},
|
|
]
|
|
}
|
|
else:
|
|
assert trt.query_attributes() == {
|
|
"currentSensorStateData": [{"name": name, "rawValue": published}]
|
|
}
|
|
|
|
assert helpers.get_google_type(sensor.DOMAIN, None) is not None
|
|
assert (
|
|
trait.SensorStateTrait.supported(
|
|
sensor.DOMAIN, None, sensor.SensorDeviceClass.MONETARY, None
|
|
)
|
|
is False
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("state", "identifier"),
|
|
[
|
|
(STATE_ON, 0),
|
|
(STATE_OFF, 1),
|
|
(STATE_UNKNOWN, 2),
|
|
],
|
|
)
|
|
@pytest.mark.parametrize(
|
|
("device_class", "name", "states"),
|
|
[
|
|
(
|
|
binary_sensor.BinarySensorDeviceClass.CO,
|
|
"CarbonMonoxideLevel",
|
|
["carbon monoxide detected", "no carbon monoxide detected", "unknown"],
|
|
),
|
|
(
|
|
binary_sensor.BinarySensorDeviceClass.SMOKE,
|
|
"SmokeLevel",
|
|
["smoke detected", "no smoke detected", "unknown"],
|
|
),
|
|
(
|
|
binary_sensor.BinarySensorDeviceClass.MOISTURE,
|
|
"WaterLeak",
|
|
["leak", "no leak", "unknown"],
|
|
),
|
|
],
|
|
)
|
|
async def test_binary_sensorstate(
|
|
hass: HomeAssistant,
|
|
state: str,
|
|
identifier: int,
|
|
device_class: binary_sensor.BinarySensorDeviceClass,
|
|
name: str,
|
|
states: list[str],
|
|
) -> None:
|
|
"""Test SensorState trait support for binary sensor domain."""
|
|
|
|
assert helpers.get_google_type(binary_sensor.DOMAIN, None) is not None
|
|
assert trait.SensorStateTrait.supported(
|
|
binary_sensor.DOMAIN, None, device_class, None
|
|
)
|
|
|
|
trt = trait.SensorStateTrait(
|
|
hass,
|
|
State(
|
|
"binary_sensor.test",
|
|
state,
|
|
{
|
|
"device_class": device_class,
|
|
},
|
|
),
|
|
BASIC_CONFIG,
|
|
)
|
|
|
|
assert trt.sync_attributes() == {
|
|
"sensorStatesSupported": [
|
|
{
|
|
"name": name,
|
|
"descriptiveCapabilities": {
|
|
"availableStates": states,
|
|
},
|
|
}
|
|
]
|
|
}
|
|
assert trt.query_attributes() == {
|
|
"currentSensorStateData": [
|
|
{
|
|
"name": name,
|
|
"currentSensorState": states[identifier],
|
|
"rawValue": None,
|
|
},
|
|
]
|
|
}
|
|
|
|
assert helpers.get_google_type(binary_sensor.DOMAIN, None) is not None
|
|
assert (
|
|
trait.SensorStateTrait.supported(
|
|
binary_sensor.DOMAIN,
|
|
None,
|
|
binary_sensor.BinarySensorDeviceClass.TAMPER,
|
|
None,
|
|
)
|
|
is False
|
|
)
|