"""Tests for the Google Assistant traits."""
from datetime import datetime, timedelta
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
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.config import async_process_ha_core_config
from homeassistant.const import (
    ATTR_ASSUMED_STATE,
    ATTR_BATTERY_LEVEL,
    ATTR_DEVICE_CLASS,
    ATTR_ENTITY_ID,
    ATTR_MODE,
    ATTR_SUPPORTED_FEATURES,
    ATTR_TEMPERATURE,
    SERVICE_TURN_OFF,
    SERVICE_TURN_ON,
    STATE_ALARM_ARMED_AWAY,
    STATE_ALARM_DISARMED,
    STATE_ALARM_PENDING,
    STATE_IDLE,
    STATE_OFF,
    STATE_ON,
    STATE_PAUSED,
    STATE_PLAYING,
    STATE_STANDBY,
    STATE_UNAVAILABLE,
    STATE_UNKNOWN,
    UnitOfTemperature,
)
from homeassistant.core import (
    DOMAIN as HA_DOMAIN,
    EVENT_CALL_SERVICE,
    HomeAssistant,
    State,
)
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, HA_DOMAIN, SERVICE_TURN_ON)
    await trt_on.execute(trait.COMMAND_ONOFF, 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, HA_DOMAIN, SERVICE_TURN_OFF)
    await trt_on.execute(trait.COMMAND_ONOFF, 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_ONOFF, 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_ONOFF, 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_ONOFF, 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_ONOFF, 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_ONOFF, 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_ONOFF, 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_ONOFF, 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_ONOFF, 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_ONOFF, 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_ONOFF, 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_ONOFF, 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_ONOFF, 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_ONOFF, 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_ONOFF, 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_STARTSTOP, 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_STARTSTOP, 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_PAUSEUNPAUSE, 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_PAUSEUNPAUSE, 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.STATE_OPEN,
            valve.STATE_CLOSED,
            valve.STATE_OPENING,
            valve.STATE_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_STARTSTOP, 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_STARTSTOP, BASIC_DATA, {"start": False}, {})

    # Start triggers toggle open
    state.state = state_closed
    await trt.execute(trait.COMMAND_STARTSTOP, 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_STARTSTOP, 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_PAUSEUNPAUSE, 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.STATE_OPEN,
            valve.STATE_CLOSED,
            valve.STATE_OPENING,
            valve.STATE_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_STARTSTOP, 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_STARTSTOP, 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_MODES,
        params={"updateModeSettings": {"effect": "colorloop"}},
    )

    calls = async_mock_service(hass, light.DOMAIN, SERVICE_TURN_ON)
    await trt.execute(
        trait.COMMAND_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,
                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,
                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,
                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_ONOFF, 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.STATE_LOCKED), PIN_CONFIG
    )

    assert trt.sync_attributes() == {}

    assert trt.query_attributes() == {"isLocked": True}

    assert trt.can_execute(trait.COMMAND_LOCKUNLOCK, {"lock": True})

    calls = async_mock_service(hass, lock.DOMAIN, lock.SERVICE_LOCK)

    await trt.execute(trait.COMMAND_LOCKUNLOCK, 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.STATE_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.STATE_JAMMED), PIN_CONFIG
    )

    assert trt.sync_attributes() == {}

    assert trt.query_attributes() == {"isJammed": True}

    assert trt.can_execute(trait.COMMAND_LOCKUNLOCK, {"lock": True})

    calls = async_mock_service(hass, lock.DOMAIN, lock.SERVICE_LOCK)

    await trt.execute(trait.COMMAND_LOCKUNLOCK, 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.STATE_LOCKED), PIN_CONFIG
    )

    assert trt.sync_attributes() == {}

    assert trt.query_attributes() == {"isLocked": True}

    assert trt.can_execute(trait.COMMAND_LOCKUNLOCK, {"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_LOCKUNLOCK, 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_LOCKUNLOCK, 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_LOCKUNLOCK, 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.STATE_LOCKED), BASIC_CONFIG
    )

    with pytest.raises(error.SmartHomeError) as err:
        await trt.execute(trait.COMMAND_LOCKUNLOCK, 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_LOCKUNLOCK, 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",
            STATE_ALARM_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": False,
        }
    }

    assert trt.query_attributes() == {
        "isArmed": True,
        "currentArmLevel": STATE_ALARM_ARMED_AWAY,
    }

    assert trt.can_execute(
        trait.COMMAND_ARMDISARM, {"arm": True, "armLevel": STATE_ALARM_ARMED_AWAY}
    )

    calls = async_mock_service(
        hass, alarm_control_panel.DOMAIN, alarm_control_panel.SERVICE_ALARM_ARM_AWAY
    )

    # Test with no secure_pin configured

    with pytest.raises(error.SmartHomeError) as err:
        trt = trait.ArmDisArmTrait(
            hass,
            State(
                "alarm_control_panel.alarm",
                STATE_ALARM_DISARMED,
                {alarm_control_panel.ATTR_CODE_ARM_REQUIRED: True},
            ),
            BASIC_CONFIG,
        )
        await trt.execute(
            trait.COMMAND_ARMDISARM,
            BASIC_DATA,
            {"arm": True, "armLevel": STATE_ALARM_ARMED_AWAY},
            {},
        )
    assert len(calls) == 0
    assert err.value.code == const.ERR_CHALLENGE_NOT_SETUP

    trt = trait.ArmDisArmTrait(
        hass,
        State(
            "alarm_control_panel.alarm",
            STATE_ALARM_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_ARMDISARM,
            PIN_DATA,
            {"arm": True, "armLevel": STATE_ALARM_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_ARMDISARM,
            PIN_DATA,
            {"arm": True, "armLevel": STATE_ALARM_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_ARMDISARM,
        PIN_DATA,
        {"arm": True, "armLevel": STATE_ALARM_ARMED_AWAY},
        {"pin": "1234"},
    )

    assert len(calls) == 1

    # Test already armed
    with pytest.raises(error.SmartHomeError) as err:
        trt = trait.ArmDisArmTrait(
            hass,
            State(
                "alarm_control_panel.alarm",
                STATE_ALARM_ARMED_AWAY,
                {alarm_control_panel.ATTR_CODE_ARM_REQUIRED: True},
            ),
            PIN_CONFIG,
        )
        await trt.execute(
            trait.COMMAND_ARMDISARM,
            PIN_DATA,
            {"arm": True, "armLevel": STATE_ALARM_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",
            STATE_ALARM_DISARMED,
            {alarm_control_panel.ATTR_CODE_ARM_REQUIRED: False},
        ),
        PIN_CONFIG,
    )
    await trt.execute(
        trait.COMMAND_ARMDISARM,
        PIN_DATA,
        {"arm": True, "armLevel": STATE_ALARM_ARMED_AWAY},
        {},
    )
    assert len(calls) == 2

    with pytest.raises(error.SmartHomeError) as err:
        await trt.execute(
            trait.COMMAND_ARMDISARM,
            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",
            STATE_ALARM_DISARMED,
            {
                alarm_control_panel.ATTR_CODE_ARM_REQUIRED: True,
                ATTR_SUPPORTED_FEATURES: AlarmControlPanelEntityFeature.TRIGGER
                | AlarmControlPanelEntityFeature.ARM_CUSTOM_BYPASS,
            },
        ),
        PIN_CONFIG,
    )
    assert trt.sync_attributes() == {
        "availableArmLevels": {
            "levels": [
                {
                    "level_name": "armed_custom_bypass",
                    "level_values": [
                        {
                            "level_synonym": ["armed custom bypass", "custom"],
                            "lang": "en",
                        }
                    ],
                },
                {
                    "level_name": "triggered",
                    "level_values": [{"level_synonym": ["triggered"], "lang": "en"}],
                },
            ],
            "ordered": False,
        }
    }

    assert trt.query_attributes() == {"isArmed": False}

    assert trt.can_execute(trait.COMMAND_ARMDISARM, {"arm": False})

    calls = async_mock_service(
        hass, alarm_control_panel.DOMAIN, alarm_control_panel.SERVICE_ALARM_DISARM
    )

    # Test without secure_pin configured
    with pytest.raises(error.SmartHomeError) as err:
        trt = trait.ArmDisArmTrait(
            hass,
            State(
                "alarm_control_panel.alarm",
                STATE_ALARM_ARMED_AWAY,
                {alarm_control_panel.ATTR_CODE_ARM_REQUIRED: True},
            ),
            BASIC_CONFIG,
        )
        await trt.execute(trait.COMMAND_ARMDISARM, 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",
            STATE_ALARM_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_ARMDISARM, 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_ARMDISARM, 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_ARMDISARM, PIN_DATA, {"arm": False}, {"pin": "1234"}
    )

    assert len(calls) == 1

    # Test already disarmed
    with pytest.raises(error.SmartHomeError) as err:
        trt = trait.ArmDisArmTrait(
            hass,
            State(
                "alarm_control_panel.alarm",
                STATE_ALARM_DISARMED,
                {alarm_control_panel.ATTR_CODE_ARM_REQUIRED: True},
            ),
            PIN_CONFIG,
        )
        await trt.execute(trait.COMMAND_ARMDISARM, PIN_DATA, {"arm": False}, {})
    assert len(calls) == 1
    assert err.value.code == const.ERR_ALREADY_DISARMED

    # Cancel arming after already armed will require pin
    with pytest.raises(error.SmartHomeError) as err:
        trt = trait.ArmDisArmTrait(
            hass,
            State(
                "alarm_control_panel.alarm",
                STATE_ALARM_ARMED_AWAY,
                {alarm_control_panel.ATTR_CODE_ARM_REQUIRED: False},
            ),
            PIN_CONFIG,
        )
        await trt.execute(
            trait.COMMAND_ARMDISARM, 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",
            STATE_ALARM_PENDING,
            {alarm_control_panel.ATTR_CODE_ARM_REQUIRED: False},
        ),
        PIN_CONFIG,
    )
    await trt.execute(
        trait.COMMAND_ARMDISARM, 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_FANSPEED, params={"fanSpeedPercent": 10})

    calls = async_mock_service(hass, fan.DOMAIN, fan.SERVICE_SET_PERCENTAGE)
    await trt.execute(trait.COMMAND_FANSPEED, 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,
    percentage: int,
    percentage_step: float,
    speed: str,
    speeds: list[list[str]],
    percentage_result: int,
):
    """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_FANSPEED, params={"fanSpeed": speed})

    calls = async_mock_service(hass, fan.DOMAIN, fan.SERVICE_SET_PERCENTAGE)
    await trt.execute(trait.COMMAND_FANSPEED, 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_FANSPEED, params={"fanSpeed": "medium"})

    calls = async_mock_service(hass, climate.DOMAIN, climate.SERVICE_SET_FAN_MODE)
    await trt.execute(trait.COMMAND_FANSPEED, 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_INPUT,
        params={"newInput": "media"},
    )

    calls = async_mock_service(
        hass, media_player.DOMAIN, media_player.SERVICE_SELECT_SOURCE
    )
    await trt.execute(
        trait.COMMAND_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_MODES,
        params={"updateModeSettings": {"option": "xyz"}},
    )

    calls = async_mock_service(
        hass, input_select.DOMAIN, input_select.SERVICE_SELECT_OPTION
    )
    await trt.execute(
        trait.COMMAND_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_MODES,
        params={"updateModeSettings": {"option": "xyz"}},
    )

    calls = async_mock_service(hass, select.DOMAIN, select.SERVICE_SELECT_OPTION)
    await trt.execute(
        trait.COMMAND_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_MODES, params={"updateModeSettings": {"mode": "away"}}
    )

    calls = async_mock_service(hass, humidifier.DOMAIN, humidifier.SERVICE_SET_MODE)
    await trt.execute(
        trait.COMMAND_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_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_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_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_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_MODES,
        params={"updateModeSettings": {"preset mode": "auto"}},
    )

    calls = async_mock_service(hass, fan.DOMAIN, fan.SERVICE_SET_PRESET_MODE)
    await trt.execute(
        trait.COMMAND_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_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_OPENCLOSE, BASIC_DATA, {"openPercent": 50}, {})
    await trt.execute(
        trait.COMMAND_OPENCLOSE_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_OPENCLOSE, BASIC_DATA, {"openPercent": 0}, {})
    await trt.execute(
        trait.COMMAND_OPENCLOSE_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_OPENCLOSE, 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.STATE_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_OPENCLOSE, 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.STATE_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.STATE_OPEN,
            valve.STATE_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_OPENCLOSE, 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_OPENCLOSE, 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_OPENCLOSE_RELATIVE,
            BASIC_DATA,
            {"openRelativePercent": 100},
            {},
        )

    with pytest.raises(SmartHomeError, match=r"No support for partial open close"):
        await trt.execute(trait.COMMAND_OPENCLOSE, 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_OPENCLOSE, 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_OPENCLOSE, 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_OPENCLOSE, 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_OPENCLOSE, 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_ONOFF, 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") == "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"
    assert trt._air_quality_description_for_aqi("non-numeric") == "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() == {}


async def test_sensorstate(hass: HomeAssistant) -> 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 in sensor_types:
        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",
                100.0,
                {
                    "device_class": sensor_type,
                },
            ),
            BASIC_CONFIG,
        )

        name = sensor_types[sensor_type][0]
        unit = sensor_types[sensor_type][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": trt._air_quality_description_for_aqi(
                            trt.state.state
                        ),
                        "rawValue": trt.state.state,
                    },
                ]
            }
        else:
            assert trt.query_attributes() == {
                "currentSensorStateData": [{"name": name, "rawValue": trt.state.state}]
            }

    assert helpers.get_google_type(sensor.DOMAIN, None) is not None
    assert (
        trait.SensorStateTrait.supported(
            sensor.DOMAIN, None, sensor.SensorDeviceClass.MONETARY, None
        )
        is False
    )