",
+ },
+ "longitude": REDACTED,
+ "memory": None,
+ "model": "PA-II",
+ "name": "Test Sensor 2",
+ "ozone1": None,
+ "pa_latency": None,
+ "pm0_3_um_count": 76.0,
+ "pm0_3_um_count_a": None,
+ "pm0_3_um_count_b": None,
+ "pm0_5_um_count": 68.0,
+ "pm0_5_um_count_a": None,
+ "pm0_5_um_count_b": None,
+ "pm10_0": 0.0,
+ "pm10_0_a": None,
+ "pm10_0_atm": None,
+ "pm10_0_atm_a": None,
+ "pm10_0_atm_b": None,
+ "pm10_0_b": None,
+ "pm10_0_cf_1": None,
+ "pm10_0_cf_1_a": None,
+ "pm10_0_cf_1_b": None,
+ "pm10_0_um_count": 0.0,
+ "pm10_0_um_count_a": None,
+ "pm10_0_um_count_b": None,
+ "pm1_0": 0.0,
+ "pm1_0_a": None,
+ "pm1_0_atm": None,
+ "pm1_0_atm_a": None,
+ "pm1_0_atm_b": None,
+ "pm1_0_b": None,
+ "pm1_0_cf_1": None,
+ "pm1_0_cf_1_a": None,
+ "pm1_0_cf_1_b": None,
+ "pm1_0_um_count": 0.0,
+ "pm1_0_um_count_a": None,
+ "pm1_0_um_count_b": None,
+ "pm2_5": 0.0,
+ "pm2_5_10minute": None,
+ "pm2_5_10minute_a": None,
+ "pm2_5_10minute_b": None,
+ "pm2_5_1week": None,
+ "pm2_5_1week_a": None,
+ "pm2_5_1week_b": None,
+ "pm2_5_24hour": None,
+ "pm2_5_24hour_a": None,
+ "pm2_5_24hour_b": None,
+ "pm2_5_30minute": None,
+ "pm2_5_30minute_a": None,
+ "pm2_5_30minute_b": None,
+ "pm2_5_60minute": None,
+ "pm2_5_60minute_a": None,
+ "pm2_5_60minute_b": None,
+ "pm2_5_6hour": None,
+ "pm2_5_6hour_a": None,
+ "pm2_5_6hour_b": None,
+ "pm2_5_a": None,
+ "pm2_5_alt": None,
+ "pm2_5_alt_a": None,
+ "pm2_5_alt_b": None,
+ "pm2_5_atm": None,
+ "pm2_5_atm_a": None,
+ "pm2_5_atm_b": None,
+ "pm2_5_b": None,
+ "pm2_5_cf_1": None,
+ "pm2_5_cf_1_a": None,
+ "pm2_5_cf_1_b": None,
+ "pm2_5_um_count": 0.0,
+ "pm2_5_um_count_a": None,
+ "pm2_5_um_count_b": None,
+ "pm5_0_um_count": 0.0,
+ "pm5_0_um_count_a": None,
+ "pm5_0_um_count_b": None,
+ "position_rating": None,
+ "pressure": 1000.74,
+ "pressure_a": None,
+ "pressure_b": None,
+ "primary_id_a": None,
+ "primary_id_b": None,
+ "primary_key_a": None,
+ "primary_key_b": None,
+ "private": None,
+ "rssi": -69,
+ "scattering_coefficient": None,
+ "scattering_coefficient_a": None,
+ "scattering_coefficient_b": None,
+ "secondary_id_a": None,
+ "secondary_id_b": None,
+ "secondary_key_a": None,
+ "secondary_key_b": None,
+ "stats": None,
+ "stats_a": None,
+ "stats_b": None,
+ "temperature": 82.0,
+ "temperature_a": None,
+ "temperature_b": None,
+ "uptime": 13788,
+ "visual_range": None,
+ "visual_range_a": None,
+ "visual_range_b": None,
+ "voc": None,
+ "voc_a": None,
+ "voc_b": None,
+ },
+ },
+ "api_version": "V1.0.11-0.0.41",
+ "firmware_default_version": "7.02",
+ "max_age": 604800,
+ "data_timestamp_utc": "2022-11-20T23:10:00",
+ "timestamp_utc": "2022-11-20T23:10:17",
+ },
+ }
diff --git a/tests/components/rainbird/__init__.py b/tests/components/rainbird/__init__.py
new file mode 100644
index 00000000000..f2d3f91e098
--- /dev/null
+++ b/tests/components/rainbird/__init__.py
@@ -0,0 +1 @@
+"""Tests for the rainbird integration."""
diff --git a/tests/components/rainbird/conftest.py b/tests/components/rainbird/conftest.py
new file mode 100644
index 00000000000..660307f1c60
--- /dev/null
+++ b/tests/components/rainbird/conftest.py
@@ -0,0 +1,116 @@
+"""Test fixtures for rainbird."""
+
+from __future__ import annotations
+
+from collections.abc import Awaitable, Callable, Generator
+from typing import Any
+from unittest.mock import patch
+
+from pyrainbird import encryption
+import pytest
+
+from homeassistant.components.rainbird import DOMAIN
+from homeassistant.const import Platform
+from homeassistant.core import HomeAssistant
+from homeassistant.setup import async_setup_component
+
+from tests.test_util.aiohttp import AiohttpClientMocker, AiohttpClientMockResponse
+
+ComponentSetup = Callable[[], Awaitable[bool]]
+
+HOST = "example.com"
+URL = "http://example.com/stick"
+PASSWORD = "password"
+
+#
+# Response payloads below come from pyrainbird test cases.
+#
+
+# Get serial number Command 0x85. Serial is 0x12635436566
+SERIAL_RESPONSE = "850000012635436566"
+# Get available stations command 0x83
+AVAILABLE_STATIONS_RESPONSE = "83017F000000" # Mask for 7 zones
+EMPTY_STATIONS_RESPONSE = "830000000000"
+# Get zone state command 0xBF.
+ZONE_3_ON_RESPONSE = "BF0004000000" # Zone 3 is on
+ZONE_5_ON_RESPONSE = "BF0010000000" # Zone 5 is on
+ZONE_OFF_RESPONSE = "BF0000000000" # All zones off
+ZONE_STATE_OFF_RESPONSE = "BF0000000000"
+# Get rain sensor state command 0XBE
+RAIN_SENSOR_OFF = "BE00"
+RAIN_SENSOR_ON = "BE01"
+# Get rain delay command 0xB6
+RAIN_DELAY = "B60010" # 0x10 is 16
+RAIN_DELAY_OFF = "B60000"
+# ACK command 0x10, Echo 0x06
+ACK_ECHO = "0106"
+
+CONFIG = {
+ DOMAIN: {
+ "host": HOST,
+ "password": PASSWORD,
+ "trigger_time": 360,
+ }
+}
+
+
+@pytest.fixture
+def platforms() -> list[Platform]:
+ """Fixture to specify platforms to test."""
+ return []
+
+
+@pytest.fixture
+def yaml_config() -> dict[str, Any]:
+ """Fixture for configuration.yaml."""
+ return CONFIG
+
+
+@pytest.fixture
+async def setup_integration(
+ hass: HomeAssistant,
+ platforms: list[str],
+ yaml_config: dict[str, Any],
+) -> Generator[ComponentSetup, None, None]:
+ """Fixture for setting up the component."""
+
+ with patch(f"homeassistant.components.{DOMAIN}.PLATFORMS", platforms):
+
+ async def func() -> bool:
+ result = await async_setup_component(hass, DOMAIN, yaml_config)
+ await hass.async_block_till_done()
+ return result
+
+ yield func
+
+
+def rainbird_response(data: str) -> bytes:
+ """Create a fake API response."""
+ return encryption.encrypt(
+ '{"jsonrpc": "2.0", "result": {"data":"%s"}, "id": 1} ' % data,
+ PASSWORD,
+ )
+
+
+def mock_response(data: str) -> AiohttpClientMockResponse:
+ """Create a fake AiohttpClientMockResponse."""
+ return AiohttpClientMockResponse("POST", URL, response=rainbird_response(data))
+
+
+@pytest.fixture(name="responses")
+def mock_responses() -> list[AiohttpClientMockResponse]:
+ """Fixture to set up a list of fake API responsees for tests to extend."""
+ return [mock_response(SERIAL_RESPONSE)]
+
+
+@pytest.fixture(autouse=True)
+def handle_responses(
+ aioclient_mock: AiohttpClientMocker,
+ responses: list[AiohttpClientMockResponse],
+) -> None:
+ """Fixture for command mocking for fake responses to the API url."""
+
+ async def handle(method, url, data) -> AiohttpClientMockResponse:
+ return responses.pop(0)
+
+ aioclient_mock.post(URL, side_effect=handle)
diff --git a/tests/components/rainbird/test_binary_sensor.py b/tests/components/rainbird/test_binary_sensor.py
new file mode 100644
index 00000000000..7ed6f2d1a29
--- /dev/null
+++ b/tests/components/rainbird/test_binary_sensor.py
@@ -0,0 +1,78 @@
+"""Tests for rainbird sensor platform."""
+
+
+import pytest
+
+from homeassistant.const import Platform
+from homeassistant.core import HomeAssistant
+
+from .conftest import (
+ RAIN_DELAY,
+ RAIN_DELAY_OFF,
+ RAIN_SENSOR_OFF,
+ RAIN_SENSOR_ON,
+ ComponentSetup,
+ mock_response,
+)
+
+from tests.test_util.aiohttp import AiohttpClientMockResponse
+
+
+@pytest.fixture
+def platforms() -> list[Platform]:
+ """Fixture to specify platforms to test."""
+ return [Platform.BINARY_SENSOR]
+
+
+@pytest.mark.parametrize(
+ "sensor_payload,expected_state",
+ [(RAIN_SENSOR_OFF, "off"), (RAIN_SENSOR_ON, "on")],
+)
+async def test_rainsensor(
+ hass: HomeAssistant,
+ setup_integration: ComponentSetup,
+ responses: list[AiohttpClientMockResponse],
+ sensor_payload: str,
+ expected_state: bool,
+) -> None:
+ """Test rainsensor binary sensor."""
+
+ responses.extend(
+ [
+ mock_response(sensor_payload),
+ mock_response(RAIN_DELAY),
+ ]
+ )
+
+ assert await setup_integration()
+
+ rainsensor = hass.states.get("binary_sensor.rainsensor")
+ assert rainsensor is not None
+ assert rainsensor.state == expected_state
+
+
+@pytest.mark.parametrize(
+ "sensor_payload,expected_state",
+ [(RAIN_DELAY_OFF, "off"), (RAIN_DELAY, "on")],
+)
+async def test_raindelay(
+ hass: HomeAssistant,
+ setup_integration: ComponentSetup,
+ responses: list[AiohttpClientMockResponse],
+ sensor_payload: str,
+ expected_state: bool,
+) -> None:
+ """Test raindelay binary sensor."""
+
+ responses.extend(
+ [
+ mock_response(RAIN_SENSOR_OFF),
+ mock_response(sensor_payload),
+ ]
+ )
+
+ assert await setup_integration()
+
+ raindelay = hass.states.get("binary_sensor.raindelay")
+ assert raindelay is not None
+ assert raindelay.state == expected_state
diff --git a/tests/components/rainbird/test_init.py b/tests/components/rainbird/test_init.py
new file mode 100644
index 00000000000..acf6a92d4a5
--- /dev/null
+++ b/tests/components/rainbird/test_init.py
@@ -0,0 +1,34 @@
+"""Tests for rainbird initialization."""
+
+from http import HTTPStatus
+
+from homeassistant.core import HomeAssistant
+
+from .conftest import URL, ComponentSetup
+
+from tests.test_util.aiohttp import AiohttpClientMocker, AiohttpClientMockResponse
+
+
+async def test_setup_success(
+ hass: HomeAssistant,
+ setup_integration: ComponentSetup,
+) -> None:
+ """Test successful setup and unload."""
+
+ assert await setup_integration()
+
+
+async def test_setup_communication_failure(
+ hass: HomeAssistant,
+ setup_integration: ComponentSetup,
+ responses: list[AiohttpClientMockResponse],
+ aioclient_mock: AiohttpClientMocker,
+) -> None:
+ """Test unable to talk to server on startup, which permanently fails setup."""
+
+ responses.clear()
+ responses.append(
+ AiohttpClientMockResponse("POST", URL, status=HTTPStatus.SERVICE_UNAVAILABLE)
+ )
+
+ assert not await setup_integration()
diff --git a/tests/components/rainbird/test_sensor.py b/tests/components/rainbird/test_sensor.py
new file mode 100644
index 00000000000..b80e014b236
--- /dev/null
+++ b/tests/components/rainbird/test_sensor.py
@@ -0,0 +1,49 @@
+"""Tests for rainbird sensor platform."""
+
+
+import pytest
+
+from homeassistant.const import Platform
+from homeassistant.core import HomeAssistant
+
+from .conftest import (
+ RAIN_DELAY,
+ RAIN_SENSOR_OFF,
+ RAIN_SENSOR_ON,
+ ComponentSetup,
+ mock_response,
+)
+
+from tests.test_util.aiohttp import AiohttpClientMockResponse
+
+
+@pytest.fixture
+def platforms() -> list[str]:
+ """Fixture to specify platforms to test."""
+ return [Platform.SENSOR]
+
+
+@pytest.mark.parametrize(
+ "sensor_payload,expected_state",
+ [(RAIN_SENSOR_OFF, "False"), (RAIN_SENSOR_ON, "True")],
+)
+async def test_sensors(
+ hass: HomeAssistant,
+ setup_integration: ComponentSetup,
+ responses: list[AiohttpClientMockResponse],
+ sensor_payload: str,
+ expected_state: bool,
+) -> None:
+ """Test sensor platform."""
+
+ responses.extend([mock_response(sensor_payload), mock_response(RAIN_DELAY)])
+
+ assert await setup_integration()
+
+ rainsensor = hass.states.get("sensor.rainsensor")
+ assert rainsensor is not None
+ assert rainsensor.state == expected_state
+
+ raindelay = hass.states.get("sensor.raindelay")
+ assert raindelay is not None
+ assert raindelay.state == "16"
diff --git a/tests/components/rainbird/test_switch.py b/tests/components/rainbird/test_switch.py
new file mode 100644
index 00000000000..d6e89c58527
--- /dev/null
+++ b/tests/components/rainbird/test_switch.py
@@ -0,0 +1,301 @@
+"""Tests for rainbird sensor platform."""
+
+
+from http import HTTPStatus
+import logging
+
+import pytest
+
+from homeassistant.components.rainbird import DOMAIN
+from homeassistant.const import ATTR_ENTITY_ID, Platform
+from homeassistant.core import HomeAssistant
+
+from .conftest import (
+ ACK_ECHO,
+ AVAILABLE_STATIONS_RESPONSE,
+ EMPTY_STATIONS_RESPONSE,
+ HOST,
+ PASSWORD,
+ URL,
+ ZONE_3_ON_RESPONSE,
+ ZONE_5_ON_RESPONSE,
+ ZONE_OFF_RESPONSE,
+ ComponentSetup,
+ mock_response,
+)
+
+from tests.components.switch import common as switch_common
+from tests.test_util.aiohttp import AiohttpClientMocker, AiohttpClientMockResponse
+
+
+@pytest.fixture
+def platforms() -> list[str]:
+ """Fixture to specify platforms to test."""
+ return [Platform.SWITCH]
+
+
+async def test_no_zones(
+ hass: HomeAssistant,
+ setup_integration: ComponentSetup,
+ responses: list[AiohttpClientMockResponse],
+) -> None:
+ """Test case where listing stations returns no stations."""
+
+ responses.append(mock_response(EMPTY_STATIONS_RESPONSE))
+ assert await setup_integration()
+
+ zone = hass.states.get("switch.sprinkler_1")
+ assert zone is None
+
+
+async def test_zones(
+ hass: HomeAssistant,
+ setup_integration: ComponentSetup,
+ responses: list[AiohttpClientMockResponse],
+) -> None:
+ """Test switch platform with fake data that creates 7 zones with one enabled."""
+
+ responses.extend(
+ [mock_response(AVAILABLE_STATIONS_RESPONSE), mock_response(ZONE_5_ON_RESPONSE)]
+ )
+
+ assert await setup_integration()
+
+ zone = hass.states.get("switch.sprinkler_1")
+ assert zone is not None
+ assert zone.state == "off"
+
+ zone = hass.states.get("switch.sprinkler_2")
+ assert zone is not None
+ assert zone.state == "off"
+
+ zone = hass.states.get("switch.sprinkler_3")
+ assert zone is not None
+ assert zone.state == "off"
+
+ zone = hass.states.get("switch.sprinkler_4")
+ assert zone is not None
+ assert zone.state == "off"
+
+ zone = hass.states.get("switch.sprinkler_5")
+ assert zone is not None
+ assert zone.state == "on"
+
+ zone = hass.states.get("switch.sprinkler_6")
+ assert zone is not None
+ assert zone.state == "off"
+
+ zone = hass.states.get("switch.sprinkler_7")
+ assert zone is not None
+ assert zone.state == "off"
+
+ assert not hass.states.get("switch.sprinkler_8")
+
+
+async def test_switch_on(
+ hass: HomeAssistant,
+ setup_integration: ComponentSetup,
+ aioclient_mock: AiohttpClientMocker,
+ responses: list[AiohttpClientMockResponse],
+) -> None:
+ """Test turning on irrigation switch."""
+
+ responses.extend(
+ [mock_response(AVAILABLE_STATIONS_RESPONSE), mock_response(ZONE_OFF_RESPONSE)]
+ )
+ assert await setup_integration()
+
+ # Initially all zones are off. Pick zone3 as an arbitrary to assert
+ # state, then update below as a switch.
+ zone = hass.states.get("switch.sprinkler_3")
+ assert zone is not None
+ assert zone.state == "off"
+
+ aioclient_mock.mock_calls.clear()
+ responses.extend(
+ [
+ mock_response(ACK_ECHO), # Switch on response
+ mock_response(ZONE_3_ON_RESPONSE), # Updated zone state
+ ]
+ )
+ await switch_common.async_turn_on(hass, "switch.sprinkler_3")
+ await hass.async_block_till_done()
+ assert len(aioclient_mock.mock_calls) == 2
+ aioclient_mock.mock_calls.clear()
+
+ # Verify switch state is updated
+ zone = hass.states.get("switch.sprinkler_3")
+ assert zone is not None
+ assert zone.state == "on"
+
+
+async def test_switch_off(
+ hass: HomeAssistant,
+ setup_integration: ComponentSetup,
+ aioclient_mock: AiohttpClientMocker,
+ responses: list[AiohttpClientMockResponse],
+) -> None:
+ """Test turning off irrigation switch."""
+
+ responses.extend(
+ [mock_response(AVAILABLE_STATIONS_RESPONSE), mock_response(ZONE_3_ON_RESPONSE)]
+ )
+ assert await setup_integration()
+
+ # Initially the test zone is on
+ zone = hass.states.get("switch.sprinkler_3")
+ assert zone is not None
+ assert zone.state == "on"
+
+ aioclient_mock.mock_calls.clear()
+ responses.extend(
+ [
+ mock_response(ACK_ECHO), # Switch off response
+ mock_response(ZONE_OFF_RESPONSE), # Updated zone state
+ ]
+ )
+ await switch_common.async_turn_off(hass, "switch.sprinkler_3")
+ await hass.async_block_till_done()
+
+ # One call to change the service and one to refresh state
+ assert len(aioclient_mock.mock_calls) == 2
+
+ # Verify switch state is updated
+ zone = hass.states.get("switch.sprinkler_3")
+ assert zone is not None
+ assert zone.state == "off"
+
+
+async def test_irrigation_service(
+ hass: HomeAssistant,
+ setup_integration: ComponentSetup,
+ aioclient_mock: AiohttpClientMocker,
+ responses: list[AiohttpClientMockResponse],
+) -> None:
+ """Test calling the irrigation service."""
+
+ responses.extend(
+ [mock_response(AVAILABLE_STATIONS_RESPONSE), mock_response(ZONE_3_ON_RESPONSE)]
+ )
+ assert await setup_integration()
+
+ aioclient_mock.mock_calls.clear()
+ responses.extend([mock_response(ACK_ECHO), mock_response(ZONE_OFF_RESPONSE)])
+
+ await hass.services.async_call(
+ DOMAIN,
+ "start_irrigation",
+ {ATTR_ENTITY_ID: "switch.sprinkler_5", "duration": 30},
+ blocking=True,
+ )
+
+ # One call to change the service and one to refresh state
+ assert len(aioclient_mock.mock_calls) == 2
+
+
+async def test_rain_delay_service(
+ hass: HomeAssistant,
+ setup_integration: ComponentSetup,
+ aioclient_mock: AiohttpClientMocker,
+ responses: list[AiohttpClientMockResponse],
+) -> None:
+ """Test calling the rain delay service."""
+
+ responses.extend(
+ [mock_response(AVAILABLE_STATIONS_RESPONSE), mock_response(ZONE_3_ON_RESPONSE)]
+ )
+ assert await setup_integration()
+
+ aioclient_mock.mock_calls.clear()
+ responses.extend(
+ [
+ mock_response(ACK_ECHO),
+ ]
+ )
+
+ await hass.services.async_call(
+ DOMAIN, "set_rain_delay", {"duration": 30}, blocking=True
+ )
+
+ assert len(aioclient_mock.mock_calls) == 1
+
+
+async def test_platform_unavailable(
+ hass: HomeAssistant,
+ setup_integration: ComponentSetup,
+ responses: list[AiohttpClientMockResponse],
+ caplog: pytest.LogCaptureFixture,
+) -> None:
+ """Test failure while listing the stations when setting up the platform."""
+
+ responses.append(
+ AiohttpClientMockResponse("POST", URL, status=HTTPStatus.SERVICE_UNAVAILABLE)
+ )
+
+ with caplog.at_level(logging.WARNING):
+ assert await setup_integration()
+
+ assert "Failed to get stations" in caplog.text
+
+
+async def test_coordinator_unavailable(
+ hass: HomeAssistant,
+ setup_integration: ComponentSetup,
+ responses: list[AiohttpClientMockResponse],
+ caplog: pytest.LogCaptureFixture,
+) -> None:
+ """Test failure to refresh the update coordinator."""
+
+ responses.extend(
+ [
+ mock_response(AVAILABLE_STATIONS_RESPONSE),
+ AiohttpClientMockResponse(
+ "POST", URL, status=HTTPStatus.SERVICE_UNAVAILABLE
+ ),
+ ],
+ )
+
+ with caplog.at_level(logging.WARNING):
+ assert await setup_integration()
+
+ assert "Failed to load zone state" in caplog.text
+
+
+@pytest.mark.parametrize(
+ "yaml_config",
+ [
+ {
+ DOMAIN: {
+ "host": HOST,
+ "password": PASSWORD,
+ "trigger_time": 360,
+ "zones": {
+ 1: {
+ "friendly_name": "Garden Sprinkler",
+ },
+ 2: {
+ "friendly_name": "Back Yard",
+ },
+ },
+ }
+ },
+ ],
+)
+async def test_yaml_config(
+ hass: HomeAssistant,
+ setup_integration: ComponentSetup,
+ responses: list[AiohttpClientMockResponse],
+) -> None:
+ """Test switch platform with fake data that creates 7 zones with one enabled."""
+
+ responses.extend(
+ [mock_response(AVAILABLE_STATIONS_RESPONSE), mock_response(ZONE_5_ON_RESPONSE)]
+ )
+
+ assert await setup_integration()
+
+ assert hass.states.get("switch.garden_sprinkler")
+ assert not hass.states.get("switch.sprinkler_1")
+ assert hass.states.get("switch.back_yard")
+ assert not hass.states.get("switch.sprinkler_2")
+ assert hass.states.get("switch.sprinkler_3")
diff --git a/tests/components/renault/const.py b/tests/components/renault/const.py
index 97499d19bea..96cdeed2493 100644
--- a/tests/components/renault/const.py
+++ b/tests/components/renault/const.py
@@ -3,9 +3,6 @@ from homeassistant.components.binary_sensor import BinarySensorDeviceClass
from homeassistant.components.renault.const import (
CONF_KAMEREON_ACCOUNT_ID,
CONF_LOCALE,
- DEVICE_CLASS_CHARGE_MODE,
- DEVICE_CLASS_CHARGE_STATE,
- DEVICE_CLASS_PLUG_STATE,
DOMAIN,
)
from homeassistant.components.select import ATTR_OPTIONS
@@ -121,7 +118,6 @@ MOCK_VEHICLES = {
Platform.DEVICE_TRACKER: [],
Platform.SELECT: [
{
- ATTR_DEVICE_CLASS: DEVICE_CLASS_CHARGE_MODE,
ATTR_ENTITY_ID: "select.reg_number_charge_mode",
ATTR_ICON: "mdi:calendar-remove",
ATTR_OPTIONS: ["always", "always_charging", "schedule_mode"],
@@ -171,9 +167,19 @@ MOCK_VEHICLES = {
ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS,
},
{
- ATTR_DEVICE_CLASS: DEVICE_CLASS_CHARGE_STATE,
+ ATTR_DEVICE_CLASS: SensorDeviceClass.ENUM,
ATTR_ENTITY_ID: "sensor.reg_number_charge_state",
ATTR_ICON: "mdi:flash",
+ ATTR_OPTIONS: [
+ "not_in_charge",
+ "waiting_for_a_planned_charge",
+ "charge_ended",
+ "waiting_for_current_charge",
+ "energy_flap_opened",
+ "charge_in_progress",
+ "charge_error",
+ "unavailable",
+ ],
ATTR_STATE: "charge_in_progress",
ATTR_UNIQUE_ID: "vf1aaaaa555777999_charge_state",
},
@@ -224,9 +230,10 @@ MOCK_VEHICLES = {
ATTR_UNIQUE_ID: "vf1aaaaa555777999_hvac_last_activity",
},
{
- ATTR_DEVICE_CLASS: DEVICE_CLASS_PLUG_STATE,
+ ATTR_DEVICE_CLASS: SensorDeviceClass.ENUM,
ATTR_ENTITY_ID: "sensor.reg_number_plug_state",
ATTR_ICON: "mdi:power-plug",
+ ATTR_OPTIONS: ["unplugged", "plugged", "plug_error", "plug_unknown"],
ATTR_STATE: "plugged",
ATTR_UNIQUE_ID: "vf1aaaaa555777999_plug_state",
},
@@ -340,7 +347,6 @@ MOCK_VEHICLES = {
],
Platform.SELECT: [
{
- ATTR_DEVICE_CLASS: DEVICE_CLASS_CHARGE_MODE,
ATTR_ENTITY_ID: "select.reg_number_charge_mode",
ATTR_ICON: "mdi:calendar-clock",
ATTR_OPTIONS: ["always", "always_charging", "schedule_mode"],
@@ -390,9 +396,19 @@ MOCK_VEHICLES = {
ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS,
},
{
- ATTR_DEVICE_CLASS: DEVICE_CLASS_CHARGE_STATE,
+ ATTR_DEVICE_CLASS: SensorDeviceClass.ENUM,
ATTR_ENTITY_ID: "sensor.reg_number_charge_state",
ATTR_ICON: "mdi:flash-off",
+ ATTR_OPTIONS: [
+ "not_in_charge",
+ "waiting_for_a_planned_charge",
+ "charge_ended",
+ "waiting_for_current_charge",
+ "energy_flap_opened",
+ "charge_in_progress",
+ "charge_error",
+ "unavailable",
+ ],
ATTR_STATE: "charge_error",
ATTR_UNIQUE_ID: "vf1aaaaa555777999_charge_state",
},
@@ -443,9 +459,10 @@ MOCK_VEHICLES = {
ATTR_UNIQUE_ID: "vf1aaaaa555777999_hvac_last_activity",
},
{
- ATTR_DEVICE_CLASS: DEVICE_CLASS_PLUG_STATE,
+ ATTR_DEVICE_CLASS: SensorDeviceClass.ENUM,
ATTR_ENTITY_ID: "sensor.reg_number_plug_state",
ATTR_ICON: "mdi:power-plug-off",
+ ATTR_OPTIONS: ["unplugged", "plugged", "plug_error", "plug_unknown"],
ATTR_STATE: "unplugged",
ATTR_UNIQUE_ID: "vf1aaaaa555777999_plug_state",
},
@@ -559,7 +576,6 @@ MOCK_VEHICLES = {
],
Platform.SELECT: [
{
- ATTR_DEVICE_CLASS: DEVICE_CLASS_CHARGE_MODE,
ATTR_ENTITY_ID: "select.reg_number_charge_mode",
ATTR_ICON: "mdi:calendar-remove",
ATTR_OPTIONS: ["always", "always_charging", "schedule_mode"],
@@ -609,9 +625,19 @@ MOCK_VEHICLES = {
ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS,
},
{
- ATTR_DEVICE_CLASS: DEVICE_CLASS_CHARGE_STATE,
+ ATTR_DEVICE_CLASS: SensorDeviceClass.ENUM,
ATTR_ENTITY_ID: "sensor.reg_number_charge_state",
ATTR_ICON: "mdi:flash",
+ ATTR_OPTIONS: [
+ "not_in_charge",
+ "waiting_for_a_planned_charge",
+ "charge_ended",
+ "waiting_for_current_charge",
+ "energy_flap_opened",
+ "charge_in_progress",
+ "charge_error",
+ "unavailable",
+ ],
ATTR_STATE: "charge_in_progress",
ATTR_UNIQUE_ID: "vf1aaaaa555777123_charge_state",
},
@@ -659,9 +685,10 @@ MOCK_VEHICLES = {
ATTR_UNIT_OF_MEASUREMENT: LENGTH_KILOMETERS,
},
{
- ATTR_DEVICE_CLASS: DEVICE_CLASS_PLUG_STATE,
+ ATTR_DEVICE_CLASS: SensorDeviceClass.ENUM,
ATTR_ENTITY_ID: "sensor.reg_number_plug_state",
ATTR_ICON: "mdi:power-plug",
+ ATTR_OPTIONS: ["unplugged", "plugged", "plug_error", "plug_unknown"],
ATTR_STATE: "plugged",
ATTR_UNIQUE_ID: "vf1aaaaa555777123_plug_state",
},
diff --git a/tests/components/reolink/__init__.py b/tests/components/reolink/__init__.py
new file mode 100644
index 00000000000..45bcb2fab8c
--- /dev/null
+++ b/tests/components/reolink/__init__.py
@@ -0,0 +1 @@
+"""Tests for the Reolink integration."""
diff --git a/tests/components/reolink/test_config_flow.py b/tests/components/reolink/test_config_flow.py
new file mode 100644
index 00000000000..b69fab9797f
--- /dev/null
+++ b/tests/components/reolink/test_config_flow.py
@@ -0,0 +1,263 @@
+"""Test the Reolink config flow."""
+import json
+from unittest.mock import AsyncMock, Mock, patch
+
+import pytest
+from reolink_aio.exceptions import ApiError, CredentialsInvalidError
+
+from homeassistant import config_entries, data_entry_flow
+from homeassistant.components.reolink import const
+from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME
+from homeassistant.helpers.device_registry import format_mac
+
+from tests.common import MockConfigEntry
+
+TEST_HOST = "1.2.3.4"
+TEST_HOST2 = "4.5.6.7"
+TEST_USERNAME = "admin"
+TEST_USERNAME2 = "username"
+TEST_PASSWORD = "password"
+TEST_PASSWORD2 = "new_password"
+TEST_MAC = "ab:cd:ef:gh:ij:kl"
+TEST_PORT = 1234
+TEST_NVR_NAME = "test_reolink_name"
+TEST_USE_HTTPS = True
+
+
+def get_mock_info(error=None, host_data_return=True):
+ """Return a mock gateway info instance."""
+ host_mock = Mock()
+ if error is None:
+ host_mock.get_host_data = AsyncMock(return_value=host_data_return)
+ else:
+ host_mock.get_host_data = AsyncMock(side_effect=error)
+ host_mock.unsubscribe_all = AsyncMock(return_value=True)
+ host_mock.logout = AsyncMock(return_value=True)
+ host_mock.mac_address = TEST_MAC
+ host_mock.onvif_enabled = True
+ host_mock.rtmp_enabled = True
+ host_mock.rtsp_enabled = True
+ host_mock.nvr_name = TEST_NVR_NAME
+ host_mock.port = TEST_PORT
+ host_mock.use_https = TEST_USE_HTTPS
+ return host_mock
+
+
+@pytest.fixture(name="reolink_connect", autouse=True)
+def reolink_connect_fixture(mock_get_source_ip):
+ """Mock reolink connection and entry setup."""
+ with patch(
+ "homeassistant.components.reolink.async_setup_entry", return_value=True
+ ), patch(
+ "homeassistant.components.reolink.host.Host", return_value=get_mock_info()
+ ):
+ yield
+
+
+async def test_config_flow_manual_success(hass):
+ """Successful flow manually initialized by the user."""
+ result = await hass.config_entries.flow.async_init(
+ const.DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ assert result["type"] is data_entry_flow.FlowResultType.FORM
+ assert result["step_id"] == "user"
+ assert result["errors"] == {}
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {
+ CONF_USERNAME: TEST_USERNAME,
+ CONF_PASSWORD: TEST_PASSWORD,
+ CONF_HOST: TEST_HOST,
+ },
+ )
+
+ assert result["type"] is data_entry_flow.FlowResultType.CREATE_ENTRY
+ assert result["title"] == TEST_NVR_NAME
+ assert result["data"] == {
+ CONF_HOST: TEST_HOST,
+ CONF_USERNAME: TEST_USERNAME,
+ CONF_PASSWORD: TEST_PASSWORD,
+ CONF_PORT: TEST_PORT,
+ const.CONF_USE_HTTPS: TEST_USE_HTTPS,
+ }
+ assert result["options"] == {
+ const.CONF_PROTOCOL: const.DEFAULT_PROTOCOL,
+ }
+
+
+async def test_config_flow_errors(hass):
+ """Successful flow manually initialized by the user after some errors."""
+ result = await hass.config_entries.flow.async_init(
+ const.DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ assert result["type"] is data_entry_flow.FlowResultType.FORM
+ assert result["step_id"] == "user"
+ assert result["errors"] == {}
+
+ host_mock = get_mock_info(host_data_return=False)
+ with patch("homeassistant.components.reolink.host.Host", return_value=host_mock):
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {
+ CONF_USERNAME: TEST_USERNAME,
+ CONF_PASSWORD: TEST_PASSWORD,
+ CONF_HOST: TEST_HOST,
+ },
+ )
+
+ assert result["type"] is data_entry_flow.FlowResultType.FORM
+ assert result["step_id"] == "user"
+ assert result["errors"] == {"host": "cannot_connect"}
+
+ host_mock = get_mock_info(error=json.JSONDecodeError("test_error", "test", 1))
+ with patch("homeassistant.components.reolink.host.Host", return_value=host_mock):
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {
+ CONF_USERNAME: TEST_USERNAME,
+ CONF_PASSWORD: TEST_PASSWORD,
+ CONF_HOST: TEST_HOST,
+ },
+ )
+
+ assert result["type"] is data_entry_flow.FlowResultType.FORM
+ assert result["step_id"] == "user"
+ assert result["errors"] == {"host": "unknown"}
+
+ host_mock = get_mock_info(error=CredentialsInvalidError("Test error"))
+ with patch("homeassistant.components.reolink.host.Host", return_value=host_mock):
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {
+ CONF_USERNAME: TEST_USERNAME,
+ CONF_PASSWORD: TEST_PASSWORD,
+ CONF_HOST: TEST_HOST,
+ },
+ )
+
+ assert result["type"] is data_entry_flow.FlowResultType.FORM
+ assert result["step_id"] == "user"
+ assert result["errors"] == {"host": "invalid_auth"}
+
+ host_mock = get_mock_info(error=ApiError("Test error"))
+ with patch("homeassistant.components.reolink.host.Host", return_value=host_mock):
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {
+ CONF_USERNAME: TEST_USERNAME,
+ CONF_PASSWORD: TEST_PASSWORD,
+ CONF_HOST: TEST_HOST,
+ },
+ )
+
+ assert result["type"] is data_entry_flow.FlowResultType.FORM
+ assert result["step_id"] == "user"
+ assert result["errors"] == {"host": "api_error"}
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {
+ CONF_USERNAME: TEST_USERNAME,
+ CONF_PASSWORD: TEST_PASSWORD,
+ CONF_HOST: TEST_HOST,
+ CONF_PORT: TEST_PORT,
+ const.CONF_USE_HTTPS: TEST_USE_HTTPS,
+ },
+ )
+
+ assert result["type"] is data_entry_flow.FlowResultType.CREATE_ENTRY
+ assert result["title"] == TEST_NVR_NAME
+ assert result["data"] == {
+ CONF_HOST: TEST_HOST,
+ CONF_USERNAME: TEST_USERNAME,
+ CONF_PASSWORD: TEST_PASSWORD,
+ CONF_PORT: TEST_PORT,
+ const.CONF_USE_HTTPS: TEST_USE_HTTPS,
+ }
+ assert result["options"] == {
+ const.CONF_PROTOCOL: const.DEFAULT_PROTOCOL,
+ }
+
+
+async def test_options_flow(hass):
+ """Test specifying non default settings using options flow."""
+ config_entry = MockConfigEntry(
+ domain=const.DOMAIN,
+ unique_id=format_mac(TEST_MAC),
+ data={
+ CONF_HOST: TEST_HOST,
+ CONF_USERNAME: TEST_USERNAME,
+ CONF_PASSWORD: TEST_PASSWORD,
+ CONF_PORT: TEST_PORT,
+ const.CONF_USE_HTTPS: TEST_USE_HTTPS,
+ },
+ options={
+ const.CONF_PROTOCOL: "rtsp",
+ },
+ title=TEST_NVR_NAME,
+ )
+ config_entry.add_to_hass(hass)
+
+ assert await hass.config_entries.async_setup(config_entry.entry_id)
+ await hass.async_block_till_done()
+
+ result = await hass.config_entries.options.async_init(config_entry.entry_id)
+
+ assert result["type"] == data_entry_flow.FlowResultType.FORM
+ assert result["step_id"] == "init"
+
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"],
+ user_input={const.CONF_PROTOCOL: "rtmp"},
+ )
+
+ assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
+ assert config_entry.options == {
+ const.CONF_PROTOCOL: "rtmp",
+ }
+
+
+async def test_change_connection_settings(hass):
+ """Test changing connection settings by issuing a second user config flow."""
+ config_entry = MockConfigEntry(
+ domain=const.DOMAIN,
+ unique_id=format_mac(TEST_MAC),
+ data={
+ CONF_HOST: TEST_HOST,
+ CONF_USERNAME: TEST_USERNAME,
+ CONF_PASSWORD: TEST_PASSWORD,
+ CONF_PORT: TEST_PORT,
+ const.CONF_USE_HTTPS: TEST_USE_HTTPS,
+ },
+ options={
+ const.CONF_PROTOCOL: const.DEFAULT_PROTOCOL,
+ },
+ title=TEST_NVR_NAME,
+ )
+ config_entry.add_to_hass(hass)
+
+ result = await hass.config_entries.flow.async_init(
+ const.DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ assert result["type"] is data_entry_flow.FlowResultType.FORM
+ assert result["step_id"] == "user"
+ assert result["errors"] == {}
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {
+ CONF_HOST: TEST_HOST2,
+ CONF_USERNAME: TEST_USERNAME2,
+ CONF_PASSWORD: TEST_PASSWORD2,
+ },
+ )
+
+ assert result["type"] is data_entry_flow.FlowResultType.ABORT
+ assert result["reason"] == "already_configured"
+ assert config_entry.data[CONF_HOST] == TEST_HOST2
+ assert config_entry.data[CONF_USERNAME] == TEST_USERNAME2
+ assert config_entry.data[CONF_PASSWORD] == TEST_PASSWORD2
diff --git a/tests/components/rest/test_sensor.py b/tests/components/rest/test_sensor.py
index 49ad69b1caa..db55df4ff16 100644
--- a/tests/components/rest/test_sensor.py
+++ b/tests/components/rest/test_sensor.py
@@ -243,7 +243,6 @@ async def test_setup_timestamp(
"method": "GET",
"value_template": "{{ value_json.key }}",
"device_class": SensorDeviceClass.TIMESTAMP,
- "state_class": SensorStateClass.MEASUREMENT,
}
},
)
@@ -255,7 +254,6 @@ async def test_setup_timestamp(
state = hass.states.get("sensor.rest_sensor")
assert state.state == "2021-11-11T11:39:00+00:00"
assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.TIMESTAMP
- assert state.attributes[ATTR_STATE_CLASS] is SensorStateClass.MEASUREMENT
assert "sensor.rest_sensor rendered invalid timestamp" not in caplog.text
assert "sensor.rest_sensor rendered timestamp without timezone" not in caplog.text
@@ -904,7 +902,7 @@ async def test_entity_config(hass: HomeAssistant) -> None:
"name": "{{'REST' + ' ' + 'Sensor'}}",
"state_class": "measurement",
"unique_id": "very_unique",
- "unit_of_measurement": "beardsecond",
+ "unit_of_measurement": "°C",
},
}
@@ -923,5 +921,5 @@ async def test_entity_config(hass: HomeAssistant) -> None:
"friendly_name": "REST Sensor",
"icon": "mdi:one_two_three",
"state_class": "measurement",
- "unit_of_measurement": "beardsecond",
+ "unit_of_measurement": "°C",
}
diff --git a/tests/components/rfxtrx/test_device_action.py b/tests/components/rfxtrx/test_device_action.py
index 7dda64bbe62..69ba983cd5d 100644
--- a/tests/components/rfxtrx/test_device_action.py
+++ b/tests/components/rfxtrx/test_device_action.py
@@ -169,7 +169,7 @@ async def test_action(
rfxtrx.transport.send.assert_called_once_with(bytearray.fromhex(expected))
-async def test_invalid_action(hass, device_reg: DeviceRegistry):
+async def test_invalid_action(hass, device_reg: DeviceRegistry, caplog):
"""Test for invalid actions."""
device = DEVICE_LIGHTING_1
@@ -201,8 +201,4 @@ async def test_invalid_action(hass, device_reg: DeviceRegistry):
)
await hass.async_block_till_done()
- assert len(notifications := hass.states.async_all("persistent_notification")) == 1
- assert (
- "The following integrations and platforms could not be set up"
- in notifications[0].attributes["message"]
- )
+ assert "Subtype invalid not found in device commands" in caplog.text
diff --git a/tests/components/rfxtrx/test_device_trigger.py b/tests/components/rfxtrx/test_device_trigger.py
index b783d10ed27..57d604ce64f 100644
--- a/tests/components/rfxtrx/test_device_trigger.py
+++ b/tests/components/rfxtrx/test_device_trigger.py
@@ -155,7 +155,7 @@ async def test_firing_event(hass, device_reg: DeviceRegistry, rfxtrx, event):
assert calls[0].data["some"] == "device"
-async def test_invalid_trigger(hass, device_reg: DeviceRegistry):
+async def test_invalid_trigger(hass, device_reg: DeviceRegistry, caplog):
"""Test for invalid actions."""
event = EVENT_LIGHTING_1
@@ -188,8 +188,4 @@ async def test_invalid_trigger(hass, device_reg: DeviceRegistry):
)
await hass.async_block_till_done()
- assert len(notifications := hass.states.async_all("persistent_notification")) == 1
- assert (
- "The following integrations and platforms could not be set up"
- in notifications[0].attributes["message"]
- )
+ assert "Subtype invalid not found in device triggers" in caplog.text
diff --git a/tests/components/rituals_perfume_genie/test_sensor.py b/tests/components/rituals_perfume_genie/test_sensor.py
index e7b8daec27f..5b15febced3 100644
--- a/tests/components/rituals_perfume_genie/test_sensor.py
+++ b/tests/components/rituals_perfume_genie/test_sensor.py
@@ -64,7 +64,7 @@ async def test_sensors_diffuser_v1_battery_cartridge(hass: HomeAssistant) -> Non
state = hass.states.get("sensor.genie_wifi")
assert state
assert state.state == str(diffuser.wifi_percentage)
- assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.SIGNAL_STRENGTH
+ assert state.attributes.get(ATTR_DEVICE_CLASS) is None
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE
entry = registry.async_get("sensor.genie_wifi")
diff --git a/tests/components/roon/test_config_flow.py b/tests/components/roon/test_config_flow.py
index 3e5bce8c1c2..32810d22789 100644
--- a/tests/components/roon/test_config_flow.py
+++ b/tests/components/roon/test_config_flow.py
@@ -144,7 +144,6 @@ async def test_unsuccessful_discovery_user_form_and_auth(hass):
"host": "1.1.1.1",
"api_key": "good_token",
"port": 9331,
- "api_key": "good_token",
"roon_server_id": "core_id",
"roon_server_name": "Roon Core",
}
diff --git a/tests/components/scrape/__init__.py b/tests/components/scrape/__init__.py
index b13cc4a7326..1bf3040513f 100644
--- a/tests/components/scrape/__init__.py
+++ b/tests/components/scrape/__init__.py
@@ -103,6 +103,9 @@ class MockRestData:
"Current Version: 2021.12.10
Released: January 17, 2022"
""
"Trying to get"
+ ""
+ "
Current Time:
2022-12-22T13:15:30Z"
+ ""
)
if self.payload == "test_scrape_sensor2":
self.data = (
diff --git a/tests/components/scrape/test_sensor.py b/tests/components/scrape/test_sensor.py
index 83607f1b993..e9b66049e61 100644
--- a/tests/components/scrape/test_sensor.py
+++ b/tests/components/scrape/test_sensor.py
@@ -338,6 +338,119 @@ async def test_scrape_sensor_attribute_and_tag(hass: HomeAssistant) -> None:
assert state2.state == "Trying to get"
+async def test_scrape_sensor_device_date(hass: HomeAssistant) -> None:
+ """Test Scrape sensor with a device of type DATE."""
+ config = {
+ DOMAIN: [
+ return_integration_config(
+ sensors=[
+ {
+ "select": ".release-date",
+ "name": "HA Date",
+ "device_class": "date",
+ "value_template": "{{ strptime(value, '%B %d, %Y').strftime('%Y-%m-%d') }}",
+ }
+ ],
+ ),
+ ]
+ }
+
+ mocker = MockRestData("test_scrape_sensor")
+ with patch(
+ "homeassistant.components.rest.RestData",
+ return_value=mocker,
+ ):
+ assert await async_setup_component(hass, DOMAIN, config)
+ await hass.async_block_till_done()
+
+ state = hass.states.get("sensor.ha_date")
+ assert state.state == "2022-01-17"
+
+
+async def test_scrape_sensor_device_date_errors(hass: HomeAssistant) -> None:
+ """Test Scrape sensor with a device of type DATE."""
+ config = {
+ DOMAIN: [
+ return_integration_config(
+ sensors=[
+ {
+ "select": ".current-version h1",
+ "name": "HA Date",
+ "device_class": "date",
+ }
+ ],
+ ),
+ ]
+ }
+
+ mocker = MockRestData("test_scrape_sensor")
+ with patch(
+ "homeassistant.components.rest.RestData",
+ return_value=mocker,
+ ):
+ assert await async_setup_component(hass, DOMAIN, config)
+ await hass.async_block_till_done()
+
+ state = hass.states.get("sensor.ha_date")
+ assert state.state == STATE_UNKNOWN
+
+
+async def test_scrape_sensor_device_timestamp(hass: HomeAssistant) -> None:
+ """Test Scrape sensor with a device of type TIMESTAMP."""
+ config = {
+ DOMAIN: [
+ return_integration_config(
+ sensors=[
+ {
+ "select": ".utc-time",
+ "name": "HA Timestamp",
+ "device_class": "timestamp",
+ }
+ ],
+ ),
+ ]
+ }
+
+ mocker = MockRestData("test_scrape_sensor")
+ with patch(
+ "homeassistant.components.rest.RestData",
+ return_value=mocker,
+ ):
+ assert await async_setup_component(hass, DOMAIN, config)
+ await hass.async_block_till_done()
+
+ state = hass.states.get("sensor.ha_timestamp")
+ assert state.state == "2022-12-22T13:15:30+00:00"
+
+
+async def test_scrape_sensor_device_timestamp_error(hass: HomeAssistant) -> None:
+ """Test Scrape sensor with a device of type TIMESTAMP."""
+ config = {
+ DOMAIN: [
+ return_integration_config(
+ sensors=[
+ {
+ "select": ".current-time",
+ "name": "HA Timestamp",
+ "device_class": "timestamp",
+ }
+ ],
+ ),
+ ]
+ }
+
+ mocker = MockRestData("test_scrape_sensor")
+ with patch(
+ "homeassistant.components.rest.RestData",
+ return_value=mocker,
+ ):
+ assert await async_setup_component(hass, DOMAIN, config)
+ await hass.async_block_till_done()
+
+ state = hass.states.get("sensor.ha_timestamp")
+ assert state.state == STATE_UNKNOWN
+
+
async def test_scrape_sensor_errors(hass: HomeAssistant) -> None:
"""Test Scrape sensor handle errors."""
config = {
diff --git a/tests/components/script/test_init.py b/tests/components/script/test_init.py
index 09d2c3c70b1..7fbb1f6bfe4 100644
--- a/tests/components/script/test_init.py
+++ b/tests/components/script/test_init.py
@@ -38,6 +38,7 @@ from homeassistant.helpers.script import (
)
from homeassistant.helpers.service import async_get_all_descriptions
from homeassistant.setup import async_setup_component
+from homeassistant.util import yaml
import homeassistant.util.dt as dt_util
from tests.common import async_fire_time_changed, async_mock_service, mock_restore_cache
@@ -166,6 +167,67 @@ async def test_setup_with_invalid_configs(hass, value):
assert len(hass.states.async_entity_ids("script")) == 0
+@pytest.mark.parametrize(
+ "object_id, broken_config, problem, details",
+ (
+ (
+ "Bad Script",
+ {},
+ "has invalid object id",
+ "invalid slug Bad Script",
+ ),
+ (
+ "bad_script",
+ {},
+ "could not be validated",
+ "required key not provided @ data['sequence']",
+ ),
+ (
+ "bad_script",
+ {
+ "sequence": {
+ "condition": "state",
+ # The UUID will fail being resolved to en entity_id
+ "entity_id": "abcdabcdabcdabcdabcdabcdabcdabcd",
+ "state": "blah",
+ },
+ },
+ "failed to setup actions",
+ "Unknown entity registry entry abcdabcdabcdabcdabcdabcdabcdabcd.",
+ ),
+ ),
+)
+async def test_bad_config_validation(
+ hass: HomeAssistant, caplog, object_id, broken_config, problem, details
+):
+ """Test bad script configuration which can be detected during validation."""
+ assert await async_setup_component(
+ hass,
+ script.DOMAIN,
+ {
+ script.DOMAIN: {
+ object_id: {"alias": "bad_script", **broken_config},
+ "good_script": {
+ "alias": "good_script",
+ "sequence": {
+ "service": "test.automation",
+ "entity_id": "hello.world",
+ },
+ },
+ }
+ },
+ )
+
+ # Check we get the expected error message
+ assert (
+ f"Script with alias 'bad_script' {problem} and has been disabled: {details}"
+ in caplog.text
+ )
+
+ # Make sure one bad automation does not prevent other automations from setting up
+ assert hass.states.async_entity_ids("script") == ["script.good_script"]
+
+
@pytest.mark.parametrize("running", ["no", "same", "different"])
async def test_reload_service(hass, running):
"""Verify the reload service."""
@@ -1248,3 +1310,81 @@ async def test_script_service_changed_entity_id(hass: HomeAssistant) -> None:
assert len(calls) == 2
assert calls[1].data["entity_id"] == "script.custom_entity_id_2"
+
+
+@pytest.mark.parametrize(
+ "blueprint_inputs, problem, details",
+ (
+ (
+ # No input
+ {},
+ "Failed to generate script from blueprint",
+ "Missing input service_to_call",
+ ),
+ (
+ # Missing input
+ {"a_number": 5},
+ "Failed to generate script from blueprint",
+ "Missing input service_to_call",
+ ),
+ (
+ # Wrong input
+ {
+ "trigger_event": "blueprint_event",
+ "service_to_call": {"dict": "not allowed"},
+ "a_number": 5,
+ },
+ "Blueprint 'Call service' generated invalid script",
+ "value should be a string for dictionary value @ data['sequence'][0]['service']",
+ ),
+ ),
+)
+async def test_blueprint_script_bad_config(
+ hass, caplog, blueprint_inputs, problem, details
+):
+ """Test blueprint script with bad inputs."""
+ assert await async_setup_component(
+ hass,
+ script.DOMAIN,
+ {
+ script.DOMAIN: {
+ "test_script": {
+ "use_blueprint": {
+ "path": "test_service.yaml",
+ "input": blueprint_inputs,
+ }
+ }
+ }
+ },
+ )
+ assert problem in caplog.text
+ assert details in caplog.text
+
+
+async def test_blueprint_script_fails_substitution(hass, caplog):
+ """Test blueprint script with bad inputs."""
+ with patch(
+ "homeassistant.components.blueprint.models.BlueprintInputs.async_substitute",
+ side_effect=yaml.UndefinedSubstitution("blah"),
+ ):
+ assert await async_setup_component(
+ hass,
+ script.DOMAIN,
+ {
+ script.DOMAIN: {
+ "test_script": {
+ "use_blueprint": {
+ "path": "test_service.yaml",
+ "input": {
+ "service_to_call": "test.automation",
+ },
+ }
+ }
+ }
+ },
+ )
+ assert (
+ "Blueprint 'Call service' failed to generate script with inputs "
+ "{'service_to_call': 'test.automation'}: No substitution found for input blah"
+ in caplog.text
+ )
diff --git a/tests/components/season/test_sensor.py b/tests/components/season/test_sensor.py
index 0c2470edb7b..e2f4e6d4dcc 100644
--- a/tests/components/season/test_sensor.py
+++ b/tests/components/season/test_sensor.py
@@ -15,7 +15,8 @@ from homeassistant.components.season.sensor import (
STATE_SUMMER,
STATE_WINTER,
)
-from homeassistant.const import CONF_TYPE, STATE_UNKNOWN
+from homeassistant.components.sensor import ATTR_OPTIONS, SensorDeviceClass
+from homeassistant.const import ATTR_DEVICE_CLASS, CONF_TYPE, STATE_UNKNOWN
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr, entity_registry as er
@@ -92,11 +93,14 @@ async def test_season_northern_hemisphere(
state = hass.states.get("sensor.season")
assert state
assert state.state == expected
+ assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.ENUM
+ assert state.attributes[ATTR_OPTIONS] == ["spring", "summer", "autumn", "winter"]
entity_registry = er.async_get(hass)
entry = entity_registry.async_get("sensor.season")
assert entry
assert entry.unique_id == mock_config_entry.entry_id
+ assert entry.translation_key == "season"
@pytest.mark.parametrize("type,day,expected", SOUTHERN_PARAMETERS, ids=idfn)
@@ -121,11 +125,14 @@ async def test_season_southern_hemisphere(
state = hass.states.get("sensor.season")
assert state
assert state.state == expected
+ assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.ENUM
+ assert state.attributes[ATTR_OPTIONS] == ["spring", "summer", "autumn", "winter"]
entity_registry = er.async_get(hass)
entry = entity_registry.async_get("sensor.season")
assert entry
assert entry.unique_id == mock_config_entry.entry_id
+ assert entry.translation_key == "season"
device_registry = dr.async_get(hass)
assert entry.device_id
diff --git a/tests/components/select/test_device_trigger.py b/tests/components/select/test_device_trigger.py
index c0166d02840..09471068429 100644
--- a/tests/components/select/test_device_trigger.py
+++ b/tests/components/select/test_device_trigger.py
@@ -258,7 +258,6 @@ async def test_get_trigger_capabilities(hass: HomeAssistant) -> None:
"name": "for",
"optional": True,
"type": "positive_time_period_dict",
- "optional": True,
},
]
@@ -288,6 +287,5 @@ async def test_get_trigger_capabilities(hass: HomeAssistant) -> None:
"name": "for",
"optional": True,
"type": "positive_time_period_dict",
- "optional": True,
},
]
diff --git a/tests/components/sensor/test_init.py b/tests/components/sensor/test_init.py
index c9c29d6c99a..d0027a6a07c 100644
--- a/tests/components/sensor/test_init.py
+++ b/tests/components/sensor/test_init.py
@@ -6,7 +6,7 @@ import pytest
from pytest import approx
from homeassistant.components.number import NumberDeviceClass
-from homeassistant.components.sensor import SensorDeviceClass
+from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass
from homeassistant.const import (
ATTR_UNIT_OF_MEASUREMENT,
LENGTH_CENTIMETERS,
@@ -32,8 +32,9 @@ from homeassistant.const import (
VOLUME_CUBIC_METERS,
VOLUME_FLUID_OUNCE,
VOLUME_LITERS,
+ UnitOfTemperature,
)
-from homeassistant.core import State
+from homeassistant.core import HomeAssistant, State
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.restore_state import STORAGE_KEY as RESTORE_STATE_KEY
from homeassistant.setup import async_setup_component
@@ -888,6 +889,14 @@ async def test_unit_conversion_priority_suggested_unit_change(
621,
SensorDeviceClass.DISTANCE,
),
+ (
+ US_CUSTOMARY_SYSTEM,
+ LENGTH_METERS,
+ LENGTH_MILES,
+ 1000000,
+ 621.371,
+ SensorDeviceClass.DISTANCE,
+ ),
],
)
async def test_unit_conversion_priority_legacy_conversion_removed(
@@ -936,3 +945,214 @@ def test_device_classes_aligned():
for device_class in NumberDeviceClass:
assert hasattr(SensorDeviceClass, device_class.name)
assert getattr(SensorDeviceClass, device_class.name).value == device_class.value
+
+
+async def test_value_unknown_in_enumeration(
+ hass: HomeAssistant,
+ caplog: pytest.LogCaptureFixture,
+ enable_custom_integrations: None,
+):
+ """Test warning on invalid enum value."""
+ platform = getattr(hass.components, "test.sensor")
+ platform.init(empty=True)
+ platform.ENTITIES["0"] = platform.MockSensor(
+ name="Test",
+ native_value="invalid_option",
+ device_class=SensorDeviceClass.ENUM,
+ options=["option1", "option2"],
+ )
+
+ assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}})
+ await hass.async_block_till_done()
+
+ assert (
+ "Sensor sensor.test provides state value 'invalid_option', "
+ "which is not in the list of options provided"
+ ) in caplog.text
+
+
+async def test_invalid_enumeration_entity_with_device_class(
+ hass: HomeAssistant,
+ caplog: pytest.LogCaptureFixture,
+ enable_custom_integrations: None,
+):
+ """Test warning on entities that provide an enum with a device class."""
+ platform = getattr(hass.components, "test.sensor")
+ platform.init(empty=True)
+ platform.ENTITIES["0"] = platform.MockSensor(
+ name="Test",
+ native_value=21,
+ device_class=SensorDeviceClass.POWER,
+ options=["option1", "option2"],
+ )
+
+ assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}})
+ await hass.async_block_till_done()
+
+ assert (
+ "Sensor sensor.test is providing enum options, but has device class 'power' "
+ "instead of 'enum'"
+ ) in caplog.text
+
+
+async def test_invalid_enumeration_entity_without_device_class(
+ hass: HomeAssistant,
+ caplog: pytest.LogCaptureFixture,
+ enable_custom_integrations: None,
+):
+ """Test warning on entities that provide an enum without a device class."""
+ platform = getattr(hass.components, "test.sensor")
+ platform.init(empty=True)
+ platform.ENTITIES["0"] = platform.MockSensor(
+ name="Test",
+ native_value=21,
+ options=["option1", "option2"],
+ )
+
+ assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}})
+ await hass.async_block_till_done()
+
+ assert (
+ "Sensor sensor.test is providing enum options, but is missing "
+ "the enum device class"
+ ) in caplog.text
+
+
+@pytest.mark.parametrize(
+ "device_class",
+ (
+ SensorDeviceClass.DATE,
+ SensorDeviceClass.ENUM,
+ SensorDeviceClass.TIMESTAMP,
+ ),
+)
+async def test_non_numeric_device_class_with_state_class(
+ hass: HomeAssistant,
+ caplog: pytest.LogCaptureFixture,
+ enable_custom_integrations: None,
+ device_class: SensorDeviceClass,
+):
+ """Test error on numeric entities that provide an state class."""
+ platform = getattr(hass.components, "test.sensor")
+ platform.init(empty=True)
+ platform.ENTITIES["0"] = platform.MockSensor(
+ name="Test",
+ native_value=None,
+ device_class=device_class,
+ state_class=SensorStateClass.MEASUREMENT,
+ options=["option1", "option2"],
+ )
+
+ assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}})
+ await hass.async_block_till_done()
+
+ assert (
+ "Sensor sensor.test has a state class and thus indicating it has a numeric "
+ f"value; however, it has the non-numeric device class: {device_class}"
+ ) in caplog.text
+
+
+@pytest.mark.parametrize(
+ "device_class",
+ (
+ SensorDeviceClass.DATE,
+ SensorDeviceClass.ENUM,
+ SensorDeviceClass.TIMESTAMP,
+ ),
+)
+async def test_non_numeric_device_class_with_unit_of_measurement(
+ hass: HomeAssistant,
+ caplog: pytest.LogCaptureFixture,
+ enable_custom_integrations: None,
+ device_class: SensorDeviceClass,
+):
+ """Test error on numeric entities that provide an unit of measurement."""
+ platform = getattr(hass.components, "test.sensor")
+ platform.init(empty=True)
+ platform.ENTITIES["0"] = platform.MockSensor(
+ name="Test",
+ native_value=None,
+ device_class=device_class,
+ native_unit_of_measurement=UnitOfTemperature.CELSIUS,
+ options=["option1", "option2"],
+ )
+
+ assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}})
+ await hass.async_block_till_done()
+
+ assert (
+ "Sensor sensor.test has a unit of measurement and thus indicating it has "
+ f"a numeric value; however, it has the non-numeric device class: {device_class}"
+ ) in caplog.text
+
+
+@pytest.mark.parametrize(
+ "device_class",
+ (
+ SensorDeviceClass.APPARENT_POWER,
+ SensorDeviceClass.AQI,
+ SensorDeviceClass.ATMOSPHERIC_PRESSURE,
+ SensorDeviceClass.BATTERY,
+ SensorDeviceClass.CO,
+ SensorDeviceClass.CO2,
+ SensorDeviceClass.CURRENT,
+ SensorDeviceClass.DATA_RATE,
+ SensorDeviceClass.DATA_SIZE,
+ SensorDeviceClass.DISTANCE,
+ SensorDeviceClass.DURATION,
+ SensorDeviceClass.ENERGY,
+ SensorDeviceClass.FREQUENCY,
+ SensorDeviceClass.GAS,
+ SensorDeviceClass.HUMIDITY,
+ SensorDeviceClass.ILLUMINANCE,
+ SensorDeviceClass.IRRADIANCE,
+ SensorDeviceClass.MOISTURE,
+ SensorDeviceClass.NITROGEN_DIOXIDE,
+ SensorDeviceClass.NITROGEN_MONOXIDE,
+ SensorDeviceClass.NITROUS_OXIDE,
+ SensorDeviceClass.OZONE,
+ SensorDeviceClass.PM1,
+ SensorDeviceClass.PM10,
+ SensorDeviceClass.PM25,
+ SensorDeviceClass.POWER_FACTOR,
+ SensorDeviceClass.POWER,
+ SensorDeviceClass.PRECIPITATION_INTENSITY,
+ SensorDeviceClass.PRECIPITATION,
+ SensorDeviceClass.PRESSURE,
+ SensorDeviceClass.REACTIVE_POWER,
+ SensorDeviceClass.SIGNAL_STRENGTH,
+ SensorDeviceClass.SOUND_PRESSURE,
+ SensorDeviceClass.SPEED,
+ SensorDeviceClass.SULPHUR_DIOXIDE,
+ SensorDeviceClass.TEMPERATURE,
+ SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS,
+ SensorDeviceClass.VOLTAGE,
+ SensorDeviceClass.VOLUME,
+ SensorDeviceClass.WATER,
+ SensorDeviceClass.WEIGHT,
+ SensorDeviceClass.WIND_SPEED,
+ ),
+)
+async def test_device_classes_with_invalid_unit_of_measurement(
+ hass: HomeAssistant,
+ caplog: pytest.LogCaptureFixture,
+ enable_custom_integrations: None,
+ device_class: SensorDeviceClass,
+):
+ """Test error when unit of measurement is not valid for used device class."""
+ platform = getattr(hass.components, "test.sensor")
+ platform.init(empty=True)
+ platform.ENTITIES["0"] = platform.MockSensor(
+ name="Test",
+ native_value="1.0",
+ device_class=device_class,
+ native_unit_of_measurement="INVALID!",
+ )
+
+ assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}})
+ await hass.async_block_till_done()
+
+ assert (
+ "is using native unit of measurement 'INVALID!' which is not a valid "
+ f"unit for the device class ('{device_class}') it is using"
+ ) in caplog.text
diff --git a/tests/components/sensor/test_recorder.py b/tests/components/sensor/test_recorder.py
index 1e129b7af92..a252a85b8d7 100644
--- a/tests/components/sensor/test_recorder.py
+++ b/tests/components/sensor/test_recorder.py
@@ -10,7 +10,11 @@ from pytest import approx
from homeassistant import loader
from homeassistant.components.recorder import DOMAIN as RECORDER_DOMAIN, history
-from homeassistant.components.recorder.db_schema import StatisticsMeta
+from homeassistant.components.recorder.db_schema import (
+ StateAttributes,
+ States,
+ StatisticsMeta,
+)
from homeassistant.components.recorder.models import (
StatisticData,
StatisticMetaData,
@@ -22,11 +26,14 @@ from homeassistant.components.recorder.statistics import (
list_statistic_ids,
)
from homeassistant.components.recorder.util import get_instance, session_scope
-from homeassistant.const import STATE_UNAVAILABLE
+from homeassistant.components.sensor import ATTR_OPTIONS, DOMAIN
+from homeassistant.const import ATTR_FRIENDLY_NAME, STATE_UNAVAILABLE
+from homeassistant.core import HomeAssistant, State
from homeassistant.setup import async_setup_component, setup_component
import homeassistant.util.dt as dt_util
from homeassistant.util.unit_system import METRIC_SYSTEM, US_CUSTOMARY_SYSTEM
+from tests.common import async_fire_time_changed
from tests.components.recorder.common import (
async_recorder_block_till_done,
async_wait_recording_done,
@@ -4050,7 +4057,7 @@ async def test_validate_statistics_unit_change_equivalent_units(
@pytest.mark.parametrize(
"attributes, unit1, unit2, supported_unit",
[
- (NONE_SENSOR_ATTRIBUTES, "m³", "m3", "L, fl. oz., ft³, gal, mL, m³"),
+ (NONE_SENSOR_ATTRIBUTES, "m³", "m3", "CCF, L, fl. oz., ft³, gal, mL, m³"),
],
)
async def test_validate_statistics_unit_change_equivalent_units_2(
@@ -4320,3 +4327,27 @@ def record_states_partially_unavailable(hass, zero, entity_id, attributes):
)
return four, states
+
+
+async def test_exclude_attributes(recorder_mock: None, hass: HomeAssistant) -> None:
+ """Test sensor attributes to be excluded."""
+ await async_setup_component(hass, DOMAIN, {DOMAIN: {"platform": "demo"}})
+ await hass.async_block_till_done()
+ async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5))
+ await hass.async_block_till_done()
+ await async_wait_recording_done(hass)
+
+ def _fetch_states() -> list[State]:
+ with session_scope(hass=hass) as session:
+ native_states = []
+ for db_state, db_state_attributes in session.query(States, StateAttributes):
+ state = db_state.to_native()
+ state.attributes = db_state_attributes.to_native()
+ native_states.append(state)
+ return native_states
+
+ states: list[State] = await hass.async_add_executor_job(_fetch_states)
+ assert len(states) > 1
+ for state in states:
+ assert ATTR_OPTIONS not in state.attributes
+ assert ATTR_FRIENDLY_NAME in state.attributes
diff --git a/tests/components/shelly/conftest.py b/tests/components/shelly/conftest.py
index 80e53ac6796..e2ba5fc767a 100644
--- a/tests/components/shelly/conftest.py
+++ b/tests/components/shelly/conftest.py
@@ -124,6 +124,7 @@ MOCK_BLOCKS = [
MOCK_CONFIG = {
"input:0": {"id": 0, "type": "button"},
+ "light:0": {"name": "test light_0"},
"switch:0": {"name": "test switch_0"},
"cover:0": {"name": "test cover_0"},
"sys": {
@@ -169,6 +170,7 @@ MOCK_STATUS_COAP = {
MOCK_STATUS_RPC = {
"switch:0": {"output": True},
+ "light:0": {"output": True, "brightness": 53.0},
"cloud": {"connected": False},
"cover:0": {
"state": "stopped",
@@ -299,8 +301,14 @@ async def mock_rpc_device():
{}, UpdateType.EVENT
)
+ def disconnected():
+ rpc_device_mock.return_value.subscribe_updates.call_args[0][0](
+ {}, UpdateType.DISCONNECTED
+ )
+
device = _mock_rpc_device("0.12.0")
rpc_device_mock.return_value = device
+ rpc_device_mock.return_value.mock_disconnected = Mock(side_effect=disconnected)
rpc_device_mock.return_value.mock_update = Mock(side_effect=update)
rpc_device_mock.return_value.mock_event = Mock(side_effect=event)
diff --git a/tests/components/shelly/test_config_flow.py b/tests/components/shelly/test_config_flow.py
index 1a1acea16a3..6795049a207 100644
--- a/tests/components/shelly/test_config_flow.py
+++ b/tests/components/shelly/test_config_flow.py
@@ -37,6 +37,15 @@ DISCOVERY_INFO = zeroconf.ZeroconfServiceInfo(
properties={zeroconf.ATTR_PROPERTIES_ID: "shelly1pm-12345"},
type="mock_type",
)
+DISCOVERY_INFO_WITH_MAC = zeroconf.ZeroconfServiceInfo(
+ host="1.1.1.1",
+ addresses=["1.1.1.1"],
+ hostname="mock_hostname",
+ name="shelly1pm-AABBCCDDEEFF",
+ port=None,
+ properties={zeroconf.ATTR_PROPERTIES_ID: "shelly1pm-AABBCCDDEEFF"},
+ type="mock_type",
+)
MOCK_CONFIG = {
"sys": {
"device": {"name": "Test name"},
@@ -1064,3 +1073,67 @@ async def test_options_flow_pre_ble_device(hass, mock_pre_ble_rpc_device):
assert result["reason"] == "ble_unsupported"
await hass.config_entries.async_unload(entry.entry_id)
+
+
+async def test_zeroconf_already_configured_triggers_refresh_mac_in_name(
+ hass, mock_rpc_device, monkeypatch
+):
+ """Test zeroconf discovery triggers refresh when the mac is in the device name."""
+ entry = MockConfigEntry(
+ domain="shelly",
+ unique_id="AABBCCDDEEFF",
+ data={"host": "1.1.1.1", "gen": 2, "sleep_period": 0, "model": "SHSW-1"},
+ )
+ entry.add_to_hass(hass)
+ await hass.config_entries.async_setup(entry.entry_id)
+ await hass.async_block_till_done()
+ assert len(mock_rpc_device.initialize.mock_calls) == 1
+
+ with patch(
+ "aioshelly.common.get_info",
+ return_value={"mac": "", "type": "SHSW-1", "auth": False},
+ ):
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ data=DISCOVERY_INFO_WITH_MAC,
+ context={"source": config_entries.SOURCE_ZEROCONF},
+ )
+ assert result["type"] == data_entry_flow.FlowResultType.ABORT
+ assert result["reason"] == "already_configured"
+
+ monkeypatch.setattr(mock_rpc_device, "connected", False)
+ mock_rpc_device.mock_disconnected()
+ await hass.async_block_till_done()
+ assert len(mock_rpc_device.initialize.mock_calls) == 2
+
+
+async def test_zeroconf_already_configured_triggers_refresh(
+ hass, mock_rpc_device, monkeypatch
+):
+ """Test zeroconf discovery triggers refresh when the mac is obtained via get_info."""
+ entry = MockConfigEntry(
+ domain="shelly",
+ unique_id="AABBCCDDEEFF",
+ data={"host": "1.1.1.1", "gen": 2, "sleep_period": 0, "model": "SHSW-1"},
+ )
+ entry.add_to_hass(hass)
+ await hass.config_entries.async_setup(entry.entry_id)
+ await hass.async_block_till_done()
+ assert len(mock_rpc_device.initialize.mock_calls) == 1
+
+ with patch(
+ "aioshelly.common.get_info",
+ return_value={"mac": "AABBCCDDEEFF", "type": "SHSW-1", "auth": False},
+ ):
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ data=DISCOVERY_INFO,
+ context={"source": config_entries.SOURCE_ZEROCONF},
+ )
+ assert result["type"] == data_entry_flow.FlowResultType.ABORT
+ assert result["reason"] == "already_configured"
+
+ monkeypatch.setattr(mock_rpc_device, "connected", False)
+ mock_rpc_device.mock_disconnected()
+ await hass.async_block_till_done()
+ assert len(mock_rpc_device.initialize.mock_calls) == 2
diff --git a/tests/components/shelly/test_device_trigger.py b/tests/components/shelly/test_device_trigger.py
index ec74745da15..13ccf3f843e 100644
--- a/tests/components/shelly/test_device_trigger.py
+++ b/tests/components/shelly/test_device_trigger.py
@@ -324,7 +324,7 @@ async def test_validate_trigger_rpc_device_not_ready(
assert calls[0].data["some"] == "test_trigger_single_push"
-async def test_validate_trigger_invalid_triggers(hass, mock_block_device):
+async def test_validate_trigger_invalid_triggers(hass, mock_block_device, caplog):
"""Test for click_event with invalid triggers."""
entry = await init_integration(hass, 1)
dev_reg = async_get_dev_reg(hass)
@@ -352,8 +352,4 @@ async def test_validate_trigger_invalid_triggers(hass, mock_block_device):
},
)
- assert len(notifications := hass.states.async_all("persistent_notification")) == 1
- assert (
- "The following integrations and platforms could not be set up"
- in notifications[0].attributes["message"]
- )
+ assert "Invalid (type,subtype): ('single', 'button3')" in caplog.text
diff --git a/tests/components/shelly/test_diagnostics.py b/tests/components/shelly/test_diagnostics.py
index ccac9bcc1b0..fb78cdd5316 100644
--- a/tests/components/shelly/test_diagnostics.py
+++ b/tests/components/shelly/test_diagnostics.py
@@ -1,12 +1,19 @@
"""Tests for Shelly diagnostics platform."""
+from unittest.mock import ANY
+
from aiohttp import ClientSession
+from aioshelly.ble.const import BLE_SCAN_RESULT_EVENT
from homeassistant.components.diagnostics import REDACTED
-from homeassistant.components.shelly.const import DOMAIN
+from homeassistant.components.shelly.const import (
+ CONF_BLE_SCANNER_MODE,
+ DOMAIN,
+ BLEScannerMode,
+)
from homeassistant.components.shelly.diagnostics import TO_REDACT
from homeassistant.core import HomeAssistant
-from . import init_integration
+from . import init_integration, inject_rpc_device_event
from .conftest import MOCK_STATUS_COAP
from tests.components.diagnostics import get_diagnostics_for_config_entry
@@ -30,6 +37,7 @@ async def test_block_config_entry_diagnostics(
assert result == {
"entry": entry_dict,
+ "bluetooth": "not initialized",
"device_info": {
"name": "Test name",
"model": "SHSW-25",
@@ -44,9 +52,35 @@ async def test_rpc_config_entry_diagnostics(
hass: HomeAssistant,
hass_client: ClientSession,
mock_rpc_device,
+ monkeypatch,
):
"""Test config entry diagnostics for rpc device."""
- await init_integration(hass, 2)
+ await init_integration(
+ hass, 2, options={CONF_BLE_SCANNER_MODE: BLEScannerMode.ACTIVE}
+ )
+
+ inject_rpc_device_event(
+ monkeypatch,
+ mock_rpc_device,
+ {
+ "events": [
+ {
+ "component": "script:1",
+ "data": [
+ 1,
+ "aa:bb:cc:dd:ee:ff",
+ -62,
+ "AgEGCf9ZANH7O3TIkA==",
+ "EQcbxdWlAgC4n+YRTSIADaLLBhYADUgQYQ==",
+ ],
+ "event": BLE_SCAN_RESULT_EVENT,
+ "id": 1,
+ "ts": 1668522399.2,
+ }
+ ],
+ "ts": 1668522399.2,
+ },
+ )
entry = hass.config_entries.async_entries(DOMAIN)[0]
entry_dict = entry.as_dict()
@@ -58,6 +92,48 @@ async def test_rpc_config_entry_diagnostics(
assert result == {
"entry": entry_dict,
+ "bluetooth": {
+ "scanner": {
+ "connectable": False,
+ "discovered_device_timestamps": {"AA:BB:CC:DD:EE:FF": ANY},
+ "discovered_devices_and_advertisement_data": [
+ {
+ "address": "AA:BB:CC:DD:EE:FF",
+ "advertisement_data": [
+ None,
+ {
+ "89": {
+ "__type": "",
+ "repr": "b'\\xd1\\xfb;t\\xc8\\x90'",
+ }
+ },
+ {
+ "00000d00-0000-1000-8000-00805f9b34fb": {
+ "__type": "",
+ "repr": "b'H\\x10a'",
+ }
+ },
+ ["cba20d00-224d-11e6-9fb8-0002a5d5c51b"],
+ -127,
+ -62,
+ [],
+ ],
+ "details": {"source": "12:34:56:78:9A:BC"},
+ "name": None,
+ "rssi": -62,
+ }
+ ],
+ "last_detection": ANY,
+ "monotonic_time": ANY,
+ "name": "Mock Title (12:34:56:78:9A:BC)",
+ "scanning": True,
+ "start_time": ANY,
+ "source": "12:34:56:78:9A:BC",
+ "storage": None,
+ "time_since_last_device_detection": {"AA:BB:CC:DD:EE:FF": ANY},
+ "type": "ShellyBLEScanner",
+ }
+ },
"device_info": {
"name": "Test name",
"model": "SHSW-25",
diff --git a/tests/components/shelly/test_init.py b/tests/components/shelly/test_init.py
index 48ea978e5d9..0697c6fb613 100644
--- a/tests/components/shelly/test_init.py
+++ b/tests/components/shelly/test_init.py
@@ -211,3 +211,30 @@ async def test_entry_unload_not_connected(hass, mock_rpc_device, monkeypatch):
assert not mock_stop_scanner.call_count
assert entry.state is ConfigEntryState.LOADED
+
+
+async def test_entry_unload_not_connected_but_we_with_we_are(
+ hass, mock_rpc_device, monkeypatch
+):
+ """Test entry unload when not connected but we think we are still connected."""
+ with patch(
+ "homeassistant.components.shelly.coordinator.async_stop_scanner",
+ side_effect=DeviceConnectionError,
+ ) as mock_stop_scanner:
+
+ entry = await init_integration(
+ hass, 2, options={CONF_BLE_SCANNER_MODE: BLEScannerMode.ACTIVE}
+ )
+ entity_id = "switch.test_switch_0"
+
+ assert entry.state is ConfigEntryState.LOADED
+ assert hass.states.get(entity_id).state is STATE_ON
+ assert not mock_stop_scanner.call_count
+
+ monkeypatch.setattr(mock_rpc_device, "connected", False)
+
+ await hass.config_entries.async_reload(entry.entry_id)
+ await hass.async_block_till_done()
+
+ assert not mock_stop_scanner.call_count
+ assert entry.state is ConfigEntryState.LOADED
diff --git a/tests/components/shelly/test_light.py b/tests/components/shelly/test_light.py
index 5f8d49fa8aa..7b7376548c8 100644
--- a/tests/components/shelly/test_light.py
+++ b/tests/components/shelly/test_light.py
@@ -383,3 +383,60 @@ async def test_rpc_device_switch_type_lights_mode(hass, mock_rpc_device, monkeyp
)
mock_rpc_device.mock_update()
assert hass.states.get("light.test_switch_0").state == STATE_OFF
+
+
+async def test_rpc_light(hass, mock_rpc_device, monkeypatch):
+ """Test RPC light."""
+ entity_id = f"{LIGHT_DOMAIN}.test_light_0"
+ monkeypatch.delitem(mock_rpc_device.status, "switch:0")
+ await init_integration(hass, 2)
+
+ # Turn on
+ await hass.services.async_call(
+ LIGHT_DOMAIN,
+ SERVICE_TURN_ON,
+ {ATTR_ENTITY_ID: entity_id},
+ blocking=True,
+ )
+
+ mock_rpc_device.call_rpc.assert_called_once_with("Light.Set", {"id": 0, "on": True})
+ state = hass.states.get(entity_id)
+ assert state.state == STATE_ON
+ assert state.attributes[ATTR_BRIGHTNESS] == 135
+
+ # Turn off
+ mock_rpc_device.call_rpc.reset_mock()
+ mutate_rpc_device_status(monkeypatch, mock_rpc_device, "light:0", "output", False)
+ await hass.services.async_call(
+ LIGHT_DOMAIN,
+ SERVICE_TURN_OFF,
+ {ATTR_ENTITY_ID: entity_id},
+ blocking=True,
+ )
+
+ mock_rpc_device.mock_update()
+ mock_rpc_device.call_rpc.assert_called_once_with(
+ "Light.Set", {"id": 0, "on": False}
+ )
+ state = hass.states.get(entity_id)
+ assert state.state == STATE_OFF
+
+ # Turn on, brightness = 33
+ mock_rpc_device.call_rpc.reset_mock()
+ await hass.services.async_call(
+ LIGHT_DOMAIN,
+ SERVICE_TURN_ON,
+ {ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 33},
+ blocking=True,
+ )
+
+ mutate_rpc_device_status(monkeypatch, mock_rpc_device, "light:0", "output", True)
+ mutate_rpc_device_status(monkeypatch, mock_rpc_device, "light:0", "brightness", 13)
+ mock_rpc_device.mock_update()
+
+ mock_rpc_device.call_rpc.assert_called_once_with(
+ "Light.Set", {"id": 0, "on": True, "brightness": 13}
+ )
+ state = hass.states.get(entity_id)
+ assert state.state == STATE_ON
+ assert state.attributes[ATTR_BRIGHTNESS] == 33
diff --git a/tests/components/shopping_list/test_init.py b/tests/components/shopping_list/test_init.py
index 515d0818460..018a6470e8f 100644
--- a/tests/components/shopping_list/test_init.py
+++ b/tests/components/shopping_list/test_init.py
@@ -1,12 +1,16 @@
"""Test shopping list component."""
from http import HTTPStatus
+import pytest
+
+from homeassistant.components.shopping_list import NoMatchingShoppingListItem
from homeassistant.components.shopping_list.const import (
DOMAIN,
EVENT_SHOPPING_LIST_UPDATED,
SERVICE_ADD_ITEM,
SERVICE_CLEAR_COMPLETED_ITEMS,
SERVICE_COMPLETE_ITEM,
+ SERVICE_REMOVE_ITEM,
)
from homeassistant.components.websocket_api.const import (
ERR_INVALID_FORMAT,
@@ -29,6 +33,32 @@ async def test_add_item(hass, sl_setup):
assert response.speech["plain"]["speech"] == "I've added beer to your shopping list"
+async def test_remove_item(hass, sl_setup):
+ """Test removiung list items."""
+ await intent.async_handle(
+ hass, "test", "HassShoppingListAddItem", {"item": {"value": "beer"}}
+ )
+
+ await intent.async_handle(
+ hass, "test", "HassShoppingListAddItem", {"item": {"value": "cheese"}}
+ )
+
+ assert len(hass.data[DOMAIN].items) == 2
+
+ # Remove a single item
+ item_id = hass.data[DOMAIN].items[0]["id"]
+ await hass.data[DOMAIN].async_remove(item_id)
+
+ assert len(hass.data[DOMAIN].items) == 1
+
+ item = hass.data[DOMAIN].items[0]
+ assert item["name"] == "cheese"
+
+ # Trying to remove the same item twice should fail
+ with pytest.raises(NoMatchingShoppingListItem):
+ await hass.data[DOMAIN].async_remove(item_id)
+
+
async def test_update_list(hass, sl_setup):
"""Test updating all list items."""
await intent.async_handle(
@@ -414,6 +444,47 @@ async def test_ws_add_item_fail(hass, hass_ws_client, sl_setup):
assert len(hass.data["shopping_list"].items) == 0
+async def test_ws_remove_item(hass, hass_ws_client, sl_setup):
+ """Test removing shopping_list item websocket command."""
+ client = await hass_ws_client(hass)
+ events = async_capture_events(hass, EVENT_SHOPPING_LIST_UPDATED)
+ await client.send_json({"id": 5, "type": "shopping_list/items/add", "name": "soda"})
+ msg = await client.receive_json()
+ first_item_id = msg["result"]["id"]
+ await client.send_json(
+ {"id": 6, "type": "shopping_list/items/add", "name": "cheese"}
+ )
+ msg = await client.receive_json()
+ assert len(events) == 2
+
+ items = hass.data["shopping_list"].items
+ assert len(items) == 2
+
+ await client.send_json(
+ {"id": 7, "type": "shopping_list/items/remove", "item_id": first_item_id}
+ )
+ msg = await client.receive_json()
+ assert len(events) == 3
+ assert msg["success"] is True
+
+ items = hass.data["shopping_list"].items
+ assert len(items) == 1
+ assert items[0]["name"] == "cheese"
+
+
+async def test_ws_remove_item_fail(hass, hass_ws_client, sl_setup):
+ """Test removing shopping_list item failure websocket command."""
+ client = await hass_ws_client(hass)
+ events = async_capture_events(hass, EVENT_SHOPPING_LIST_UPDATED)
+ await client.send_json({"id": 5, "type": "shopping_list/items/add", "name": "soda"})
+ msg = await client.receive_json()
+ await client.send_json({"id": 6, "type": "shopping_list/items/remove"})
+ msg = await client.receive_json()
+ assert msg["success"] is False
+ assert len(events) == 1
+ assert len(hass.data["shopping_list"].items) == 1
+
+
async def test_ws_reorder_items(hass, hass_ws_client, sl_setup):
"""Test reordering shopping_list items websocket command."""
await intent.async_handle(
@@ -558,6 +629,40 @@ async def test_add_item_service(hass, sl_setup):
assert len(events) == 1
+async def test_remove_item_service(hass, sl_setup):
+ """Test removing shopping_list item service."""
+ events = async_capture_events(hass, EVENT_SHOPPING_LIST_UPDATED)
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_ADD_ITEM,
+ {ATTR_NAME: "beer"},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_ADD_ITEM,
+ {ATTR_NAME: "cheese"},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+
+ assert len(hass.data[DOMAIN].items) == 2
+ assert len(events) == 2
+
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_REMOVE_ITEM,
+ {ATTR_NAME: "beer"},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+
+ assert len(hass.data[DOMAIN].items) == 1
+ assert hass.data[DOMAIN].items[0]["name"] == "cheese"
+ assert len(events) == 3
+
+
async def test_clear_completed_items_service(hass, sl_setup):
"""Test clearing completed shopping_list items service."""
events = async_capture_events(hass, EVENT_SHOPPING_LIST_UPDATED)
diff --git a/tests/components/snmp/test_sensor.py b/tests/components/snmp/test_sensor.py
index 9f3a555c2d9..965bc0d3ae9 100644
--- a/tests/components/snmp/test_sensor.py
+++ b/tests/components/snmp/test_sensor.py
@@ -1,5 +1,6 @@
"""SNMP sensor tests."""
+import asyncio
from unittest.mock import MagicMock, Mock, patch
import pytest
@@ -15,9 +16,11 @@ def hlapi_mock():
"""Mock out 3rd party API."""
mock_data = MagicMock()
mock_data.prettyPrint = Mock(return_value="hello")
+ future = asyncio.get_event_loop().create_future()
+ future.set_result((None, None, None, [[mock_data]]))
with patch(
"homeassistant.components.snmp.sensor.getCmd",
- return_value=(None, None, None, [[mock_data]]),
+ return_value=future,
):
yield
@@ -57,7 +60,7 @@ async def test_entity_config(hass: HomeAssistant) -> None:
"name": "{{'SNMP' + ' ' + 'Sensor'}}",
"state_class": "measurement",
"unique_id": "very_unique",
- "unit_of_measurement": "beardsecond",
+ "unit_of_measurement": "°C",
},
}
@@ -75,5 +78,5 @@ async def test_entity_config(hass: HomeAssistant) -> None:
"friendly_name": "SNMP Sensor",
"icon": "mdi:one_two_three",
"state_class": "measurement",
- "unit_of_measurement": "beardsecond",
+ "unit_of_measurement": "°C",
}
diff --git a/tests/components/speedtestdotnet/test_config_flow.py b/tests/components/speedtestdotnet/test_config_flow.py
index 0344c09631b..00269c55ec3 100644
--- a/tests/components/speedtestdotnet/test_config_flow.py
+++ b/tests/components/speedtestdotnet/test_config_flow.py
@@ -1,17 +1,15 @@
"""Tests for SpeedTest config flow."""
-from datetime import timedelta
from unittest.mock import MagicMock
-from homeassistant import config_entries, data_entry_flow
+from homeassistant import config_entries
from homeassistant.components import speedtestdotnet
from homeassistant.components.speedtestdotnet.const import (
- CONF_MANUAL,
CONF_SERVER_ID,
CONF_SERVER_NAME,
DOMAIN,
)
-from homeassistant.const import CONF_SCAN_INTERVAL
from homeassistant.core import HomeAssistant
+from homeassistant.data_entry_flow import FlowResultType
from tests.common import MockConfigEntry
@@ -21,13 +19,13 @@ async def test_flow_works(hass: HomeAssistant) -> None:
result = await hass.config_entries.flow.async_init(
speedtestdotnet.DOMAIN, context={"source": config_entries.SOURCE_USER}
)
- assert result["type"] == data_entry_flow.FlowResultType.FORM
+ assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "user"
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={}
)
- assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
+ assert result["type"] == FlowResultType.CREATE_ENTRY
async def test_options(hass: HomeAssistant, mock_api: MagicMock) -> None:
@@ -42,68 +40,41 @@ async def test_options(hass: HomeAssistant, mock_api: MagicMock) -> None:
await hass.async_block_till_done()
result = await hass.config_entries.options.async_init(entry.entry_id)
- assert result["type"] == data_entry_flow.FlowResultType.FORM
+ assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "init"
result = await hass.config_entries.options.async_configure(
result["flow_id"],
user_input={
CONF_SERVER_NAME: "Country1 - Sponsor1 - Server1",
- CONF_SCAN_INTERVAL: 30,
- CONF_MANUAL: True,
},
)
- assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
+ assert result["type"] == FlowResultType.CREATE_ENTRY
assert result["data"] == {
CONF_SERVER_NAME: "Country1 - Sponsor1 - Server1",
CONF_SERVER_ID: "1",
- CONF_SCAN_INTERVAL: 30,
- CONF_MANUAL: True,
}
await hass.async_block_till_done()
- assert hass.data[DOMAIN].update_interval is None
-
# test setting server name to "*Auto Detect"
result = await hass.config_entries.options.async_init(entry.entry_id)
- assert result["type"] == data_entry_flow.FlowResultType.FORM
+ assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "init"
result = await hass.config_entries.options.async_configure(
result["flow_id"],
user_input={
CONF_SERVER_NAME: "*Auto Detect",
- CONF_SCAN_INTERVAL: 30,
- CONF_MANUAL: True,
},
)
- assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
+ assert result["type"] == FlowResultType.CREATE_ENTRY
assert result["data"] == {
CONF_SERVER_NAME: "*Auto Detect",
CONF_SERVER_ID: None,
- CONF_SCAN_INTERVAL: 30,
- CONF_MANUAL: True,
}
- # test setting the option to update periodically
- result2 = await hass.config_entries.options.async_init(entry.entry_id)
- assert result2["type"] == data_entry_flow.FlowResultType.FORM
- assert result2["step_id"] == "init"
-
- result2 = await hass.config_entries.options.async_configure(
- result2["flow_id"],
- user_input={
- CONF_SERVER_NAME: "Country1 - Sponsor1 - Server1",
- CONF_SCAN_INTERVAL: 30,
- CONF_MANUAL: False,
- },
- )
- await hass.async_block_till_done()
-
- assert hass.data[DOMAIN].update_interval == timedelta(minutes=30)
-
async def test_integration_already_configured(hass: HomeAssistant) -> None:
"""Test integration is already configured."""
@@ -114,5 +85,5 @@ async def test_integration_already_configured(hass: HomeAssistant) -> None:
result = await hass.config_entries.flow.async_init(
speedtestdotnet.DOMAIN, context={"source": config_entries.SOURCE_USER}
)
- assert result["type"] == data_entry_flow.FlowResultType.ABORT
+ assert result["type"] == FlowResultType.ABORT
assert result["reason"] == "single_instance_allowed"
diff --git a/tests/components/speedtestdotnet/test_init.py b/tests/components/speedtestdotnet/test_init.py
index 4b8071ad1ea..da19fd85dd3 100644
--- a/tests/components/speedtestdotnet/test_init.py
+++ b/tests/components/speedtestdotnet/test_init.py
@@ -6,13 +6,12 @@ from unittest.mock import MagicMock
import speedtest
from homeassistant.components.speedtestdotnet.const import (
- CONF_MANUAL,
CONF_SERVER_ID,
CONF_SERVER_NAME,
DOMAIN,
)
from homeassistant.config_entries import ConfigEntryState
-from homeassistant.const import CONF_SCAN_INTERVAL, STATE_UNAVAILABLE
+from homeassistant.const import STATE_UNAVAILABLE
from homeassistant.core import HomeAssistant
import homeassistant.util.dt as dt_util
@@ -28,8 +27,6 @@ async def test_successful_config_entry(hass: HomeAssistant) -> None:
options={
CONF_SERVER_NAME: "Country1 - Sponsor1 - Server1",
CONF_SERVER_ID: "1",
- CONF_SCAN_INTERVAL: 30,
- CONF_MANUAL: False,
},
)
entry.add_to_hass(hass)
@@ -75,10 +72,7 @@ async def test_server_not_found(hass: HomeAssistant, mock_api: MagicMock) -> Non
entry = MockConfigEntry(
domain=DOMAIN,
- options={
- CONF_MANUAL: False,
- CONF_SCAN_INTERVAL: 60,
- },
+ options={},
)
entry.add_to_hass(hass)
@@ -89,7 +83,7 @@ async def test_server_not_found(hass: HomeAssistant, mock_api: MagicMock) -> Non
mock_api.return_value.get_servers.side_effect = speedtest.NoMatchedServers
async_fire_time_changed(
hass,
- dt_util.utcnow() + timedelta(minutes=entry.options[CONF_SCAN_INTERVAL] + 1),
+ dt_util.utcnow() + timedelta(minutes=61),
)
await hass.async_block_till_done()
state = hass.states.get("sensor.speedtest_ping")
diff --git a/tests/components/speedtestdotnet/test_sensor.py b/tests/components/speedtestdotnet/test_sensor.py
index 06802a6cae7..68f14c64a69 100644
--- a/tests/components/speedtestdotnet/test_sensor.py
+++ b/tests/components/speedtestdotnet/test_sensor.py
@@ -1,13 +1,10 @@
"""Tests for SpeedTest sensors."""
from unittest.mock import MagicMock
-from homeassistant.components import speedtestdotnet
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
-from homeassistant.components.speedtestdotnet.const import (
- CONF_MANUAL,
- DEFAULT_NAME,
- SENSOR_TYPES,
-)
+from homeassistant.components.speedtestdotnet import DOMAIN
+from homeassistant.components.speedtestdotnet.const import DEFAULT_NAME
+from homeassistant.components.speedtestdotnet.sensor import SENSOR_TYPES
from homeassistant.core import HomeAssistant, State
from . import MOCK_RESULTS, MOCK_SERVERS, MOCK_STATES
@@ -19,7 +16,7 @@ async def test_speedtestdotnet_sensors(
hass: HomeAssistant, mock_api: MagicMock
) -> None:
"""Test sensors created for speedtestdotnet integration."""
- entry = MockConfigEntry(domain=speedtestdotnet.DOMAIN, data={})
+ entry = MockConfigEntry(domain=DOMAIN, data={})
entry.add_to_hass(hass)
mock_api.return_value.get_best_server.return_value = MOCK_SERVERS[1][0]
@@ -45,9 +42,7 @@ async def test_restore_last_state(hass: HomeAssistant, mock_api: MagicMock) -> N
for sensor, state in MOCK_STATES.items()
],
)
- entry = MockConfigEntry(
- domain=speedtestdotnet.DOMAIN, data={}, options={CONF_MANUAL: True}
- )
+ entry = MockConfigEntry(domain=DOMAIN)
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
diff --git a/tests/components/ssdp/test_init.py b/tests/components/ssdp/test_init.py
index 4076ef4685d..959a35dba78 100644
--- a/tests/components/ssdp/test_init.py
+++ b/tests/components/ssdp/test_init.py
@@ -11,7 +11,6 @@ from async_upnp_client.ssdp_listener import SsdpListener
from async_upnp_client.utils import CaseInsensitiveDict
import pytest
-import homeassistant
from homeassistant import config_entries
from homeassistant.components import ssdp
from homeassistant.const import (
@@ -19,6 +18,7 @@ from homeassistant.const import (
EVENT_HOMEASSISTANT_STOP,
MATCH_ALL,
)
+from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component
import homeassistant.util.dt as dt_util
@@ -31,7 +31,7 @@ def _ssdp_headers(headers):
return ssdp_headers
-async def init_ssdp_component(hass: homeassistant) -> SsdpListener:
+async def init_ssdp_component(hass: HomeAssistant) -> SsdpListener:
"""Initialize ssdp component and get SsdpListener."""
await async_setup_component(hass, ssdp.DOMAIN, {ssdp.DOMAIN: {}})
await hass.async_block_till_done()
@@ -55,7 +55,7 @@ async def test_ssdp_flow_dispatched_on_st(mock_get_ssdp, hass, caplog, mock_flow
}
)
ssdp_listener = await init_ssdp_component(hass)
- await ssdp_listener._on_search(mock_ssdp_search_response)
+ ssdp_listener._on_search(mock_ssdp_search_response)
await hass.async_block_till_done()
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
await hass.async_block_till_done()
@@ -98,7 +98,7 @@ async def test_ssdp_flow_dispatched_on_manufacturer_url(
}
)
ssdp_listener = await init_ssdp_component(hass)
- await ssdp_listener._on_search(mock_ssdp_search_response)
+ ssdp_listener._on_search(mock_ssdp_search_response)
await hass.async_block_till_done()
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
await hass.async_block_till_done()
@@ -148,7 +148,7 @@ async def test_scan_match_upnp_devicedesc_manufacturer(
}
)
ssdp_listener = await init_ssdp_component(hass)
- await ssdp_listener._on_search(mock_ssdp_search_response)
+ ssdp_listener._on_search(mock_ssdp_search_response)
await hass.async_block_till_done()
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
await hass.async_block_till_done()
@@ -189,7 +189,7 @@ async def test_scan_match_upnp_devicedesc_devicetype(
}
)
ssdp_listener = await init_ssdp_component(hass)
- await ssdp_listener._on_search(mock_ssdp_search_response)
+ ssdp_listener._on_search(mock_ssdp_search_response)
await hass.async_block_till_done()
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
@@ -237,7 +237,7 @@ async def test_scan_not_all_present(
}
)
ssdp_listener = await init_ssdp_component(hass)
- await ssdp_listener._on_search(mock_ssdp_search_response)
+ ssdp_listener._on_search(mock_ssdp_search_response)
await hass.async_block_till_done()
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
await hass.async_block_till_done()
@@ -278,7 +278,7 @@ async def test_scan_not_all_match(mock_get_ssdp, hass, aioclient_mock, mock_flow
}
)
ssdp_listener = await init_ssdp_component(hass)
- await ssdp_listener._on_search(mock_ssdp_search_response)
+ ssdp_listener._on_search(mock_ssdp_search_response)
await hass.async_block_till_done()
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
await hass.async_block_till_done()
@@ -317,7 +317,7 @@ async def test_flow_start_only_alive(
"usn": "uuid:mock-udn::mock-st",
}
)
- await ssdp_listener._on_search(mock_ssdp_search_response)
+ ssdp_listener._on_search(mock_ssdp_search_response)
await hass.async_block_till_done()
mock_flow_init.assert_awaited_once_with(
@@ -334,7 +334,7 @@ async def test_flow_start_only_alive(
"nts": "ssdp:alive",
}
)
- await ssdp_listener._on_alive(mock_ssdp_advertisement)
+ ssdp_listener._on_alive(mock_ssdp_advertisement)
await hass.async_block_till_done()
mock_flow_init.assert_awaited_once_with(
"mock-domain", context={"source": config_entries.SOURCE_SSDP}, data=ANY
@@ -343,14 +343,14 @@ async def test_flow_start_only_alive(
# ssdp:byebye advertisement should not start a flow
mock_flow_init.reset_mock()
mock_ssdp_advertisement["nts"] = "ssdp:byebye"
- await ssdp_listener._on_byebye(mock_ssdp_advertisement)
+ ssdp_listener._on_byebye(mock_ssdp_advertisement)
await hass.async_block_till_done()
mock_flow_init.assert_not_called()
# ssdp:update advertisement should start a flow
mock_flow_init.reset_mock()
mock_ssdp_advertisement["nts"] = "ssdp:update"
- await ssdp_listener._on_update(mock_ssdp_advertisement)
+ ssdp_listener._on_update(mock_ssdp_advertisement)
await hass.async_block_till_done()
mock_flow_init.assert_awaited_once_with(
"mock-domain", context={"source": config_entries.SOURCE_SSDP}, data=ANY
@@ -388,7 +388,7 @@ async def test_discovery_from_advertisement_sets_ssdp_st(
"usn": "uuid:mock-udn::mock-st",
}
)
- await ssdp_listener._on_alive(mock_ssdp_advertisement)
+ ssdp_listener._on_alive(mock_ssdp_advertisement)
await hass.async_block_till_done()
discovery_info = await ssdp.async_get_discovery_info_by_udn(hass, "uuid:mock-udn")
@@ -469,34 +469,35 @@ async def test_scan_with_registered_callback(
hass, async_integration_callback, {"st": "mock-st"}
)
- async_integration_match_all_callback1 = AsyncMock()
+ async_integration_match_all_callback = AsyncMock()
await ssdp.async_register_callback(
- hass, async_integration_match_all_callback1, {"x-rincon-bootseq": MATCH_ALL}
+ hass, async_integration_match_all_callback, {"x-rincon-bootseq": MATCH_ALL}
)
- async_integration_match_all_not_present_callback1 = AsyncMock()
+ async_integration_match_all_not_present_callback = AsyncMock()
await ssdp.async_register_callback(
hass,
- async_integration_match_all_not_present_callback1,
+ async_integration_match_all_not_present_callback,
{"x-not-there": MATCH_ALL},
)
- async_not_matching_integration_callback1 = AsyncMock()
+ async_not_matching_integration_callback = AsyncMock()
await ssdp.async_register_callback(
- hass, async_not_matching_integration_callback1, {"st": "not-match-mock-st"}
+ hass, async_not_matching_integration_callback, {"st": "not-match-mock-st"}
)
- async_match_any_callback1 = AsyncMock()
- await ssdp.async_register_callback(hass, async_match_any_callback1)
+ async_match_any_callback = AsyncMock()
+ await ssdp.async_register_callback(hass, async_match_any_callback)
await hass.async_block_till_done()
- await ssdp_listener._on_search(mock_ssdp_search_response)
+ ssdp_listener._on_search(mock_ssdp_search_response)
+ await hass.async_block_till_done()
assert async_integration_callback.call_count == 1
- assert async_integration_match_all_callback1.call_count == 1
- assert async_integration_match_all_not_present_callback1.call_count == 0
- assert async_match_any_callback1.call_count == 1
- assert async_not_matching_integration_callback1.call_count == 0
+ assert async_integration_match_all_callback.call_count == 1
+ assert async_integration_match_all_not_present_callback.call_count == 0
+ assert async_match_any_callback.call_count == 1
+ assert async_not_matching_integration_callback.call_count == 0
assert async_integration_callback.call_args[0][1] == ssdp.SsdpChange.ALIVE
mock_call_data: ssdp.SsdpServiceInfo = async_integration_callback.call_args[0][0]
assert mock_call_data.ssdp_ext == ""
@@ -552,7 +553,7 @@ async def test_getting_existing_headers(
}
)
ssdp_listener = await init_ssdp_component(hass)
- await ssdp_listener._on_search(mock_ssdp_search_response)
+ ssdp_listener._on_search(mock_ssdp_search_response)
discovery_info_by_st = await ssdp.async_get_discovery_info_by_st(hass, "mock-st")
discovery_info_by_st = discovery_info_by_st[0]
diff --git a/tests/components/switchbee/test_config_flow.py b/tests/components/switchbee/test_config_flow.py
index 8b9637a7695..1745d6c5d40 100644
--- a/tests/components/switchbee/test_config_flow.py
+++ b/tests/components/switchbee/test_config_flow.py
@@ -25,15 +25,15 @@ async def test_form(hass):
assert result["errors"] == {}
with patch(
- "switchbee.api.CentralUnitAPI.get_configuration",
+ "switchbee.api.polling.CentralUnitPolling.get_configuration",
return_value=coordinator_data,
), patch(
"homeassistant.components.switchbee.async_setup_entry",
return_value=True,
), patch(
- "switchbee.api.CentralUnitAPI.fetch_states", return_value=None
+ "switchbee.api.polling.CentralUnitPolling.fetch_states", return_value=None
), patch(
- "switchbee.api.CentralUnitAPI._login", return_value=None
+ "switchbee.api.polling.CentralUnitPolling._login", return_value=None
):
result2 = await hass.config_entries.flow.async_configure(
@@ -62,7 +62,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None:
)
with patch(
- "switchbee.api.CentralUnitAPI._login",
+ "switchbee.api.polling.CentralUnitPolling._login",
side_effect=SwitchBeeError(MOCK_FAILED_TO_LOGIN_MSG),
):
result2 = await hass.config_entries.flow.async_configure(
@@ -86,7 +86,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None:
)
with patch(
- "switchbee.api.CentralUnitAPI._login",
+ "switchbee.api.polling.CentralUnitPolling._login",
side_effect=SwitchBeeError(MOCK_INVALID_TOKEN_MGS),
):
result2 = await hass.config_entries.flow.async_configure(
@@ -109,7 +109,7 @@ async def test_form_unknown_error(hass):
)
with patch(
- "switchbee.api.CentralUnitAPI._login",
+ "switchbee.api.polling.CentralUnitPolling._login",
side_effect=Exception,
):
form_result = await hass.config_entries.flow.async_configure(
@@ -144,14 +144,16 @@ async def test_form_entry_exists(hass):
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
- with patch("switchbee.api.CentralUnitAPI._login", return_value=None), patch(
+ with patch(
+ "switchbee.api.polling.CentralUnitPolling._login", return_value=None
+ ), patch(
"homeassistant.components.switchbee.async_setup_entry",
return_value=True,
), patch(
- "switchbee.api.CentralUnitAPI.get_configuration",
+ "switchbee.api.polling.CentralUnitPolling.get_configuration",
return_value=coordinator_data,
), patch(
- "switchbee.api.CentralUnitAPI.fetch_states", return_value=None
+ "switchbee.api.polling.CentralUnitPolling.fetch_states", return_value=None
):
form_result = await hass.config_entries.flow.async_configure(
result["flow_id"],
diff --git a/tests/components/switchbot/__init__.py b/tests/components/switchbot/__init__.py
index d5216cf6262..ce39579915f 100644
--- a/tests/components/switchbot/__init__.py
+++ b/tests/components/switchbot/__init__.py
@@ -168,6 +168,26 @@ WOSENSORTH_SERVICE_INFO = BluetoothServiceInfoBleak(
connectable=False,
)
+
+WOLOCK_SERVICE_INFO = BluetoothServiceInfoBleak(
+ name="WoLock",
+ manufacturer_data={2409: b"\xf1\t\x9fE\x1a]\xda\x83\x00 "},
+ service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"o\x80d"},
+ service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"],
+ address="aa:bb:cc:dd:ee:ff",
+ rssi=-60,
+ source="local",
+ advertisement=generate_advertisement_data(
+ local_name="WoLock",
+ manufacturer_data={2409: b"\xf1\t\x9fE\x1a]\xda\x83\x00 "},
+ service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"o\x80d"},
+ service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"],
+ ),
+ device=BLEDevice("aa:bb:cc:dd:ee:ff", "WoLock"),
+ time=0,
+ connectable=True,
+)
+
NOT_SWITCHBOT_INFO = BluetoothServiceInfoBleak(
name="unknown",
service_uuids=[],
diff --git a/tests/components/switchbot/test_config_flow.py b/tests/components/switchbot/test_config_flow.py
index 66d0874809f..1a3db48f192 100644
--- a/tests/components/switchbot/test_config_flow.py
+++ b/tests/components/switchbot/test_config_flow.py
@@ -2,9 +2,21 @@
from unittest.mock import patch
-from homeassistant.components.switchbot.const import CONF_RETRY_COUNT
+from switchbot import SwitchbotAccountConnectionError, SwitchbotAuthenticationError
+
+from homeassistant.components.switchbot.const import (
+ CONF_ENCRYPTION_KEY,
+ CONF_KEY_ID,
+ CONF_RETRY_COUNT,
+)
from homeassistant.config_entries import SOURCE_BLUETOOTH, SOURCE_USER
-from homeassistant.const import CONF_ADDRESS, CONF_NAME, CONF_PASSWORD, CONF_SENSOR_TYPE
+from homeassistant.const import (
+ CONF_ADDRESS,
+ CONF_NAME,
+ CONF_PASSWORD,
+ CONF_SENSOR_TYPE,
+ CONF_USERNAME,
+)
from homeassistant.data_entry_flow import FlowResultType
from . import (
@@ -15,6 +27,7 @@ from . import (
WOHAND_SERVICE_ALT_ADDRESS_INFO,
WOHAND_SERVICE_INFO,
WOHAND_SERVICE_INFO_NOT_CONNECTABLE,
+ WOLOCK_SERVICE_INFO,
WOSENSORTH_SERVICE_INFO,
init_integration,
patch_async_setup_entry,
@@ -80,6 +93,66 @@ async def test_bluetooth_discovery_requires_password(hass):
assert len(mock_setup_entry.mock_calls) == 1
+async def test_bluetooth_discovery_lock_key(hass):
+ """Test discovery via bluetooth with a lock."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": SOURCE_BLUETOOTH},
+ data=WOLOCK_SERVICE_INFO,
+ )
+ assert result["type"] == FlowResultType.MENU
+ assert result["step_id"] == "lock_choose_method"
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"], user_input={"next_step_id": "lock_key"}
+ )
+ await hass.async_block_till_done()
+ assert result["type"] == FlowResultType.FORM
+ assert result["step_id"] == "lock_key"
+ assert result["errors"] == {}
+
+ with patch(
+ "homeassistant.components.switchbot.config_flow.SwitchbotLock.verify_encryption_key",
+ return_value=False,
+ ):
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {
+ CONF_KEY_ID: "",
+ CONF_ENCRYPTION_KEY: "",
+ },
+ )
+ await hass.async_block_till_done()
+
+ assert result["type"] == FlowResultType.FORM
+ assert result["step_id"] == "lock_key"
+ assert result["errors"] == {"base": "encryption_key_invalid"}
+
+ with patch_async_setup_entry() as mock_setup_entry, patch(
+ "homeassistant.components.switchbot.config_flow.SwitchbotLock.verify_encryption_key",
+ return_value=True,
+ ):
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {
+ CONF_KEY_ID: "ff",
+ CONF_ENCRYPTION_KEY: "ffffffffffffffffffffffffffffffff",
+ },
+ )
+ await hass.async_block_till_done()
+
+ assert result["type"] == FlowResultType.CREATE_ENTRY
+ assert result["title"] == "Lock EEFF"
+ assert result["data"] == {
+ CONF_ADDRESS: "aa:bb:cc:dd:ee:ff",
+ CONF_KEY_ID: "ff",
+ CONF_ENCRYPTION_KEY: "ffffffffffffffffffffffffffffffff",
+ CONF_SENSOR_TYPE: "lock",
+ }
+
+ assert len(mock_setup_entry.mock_calls) == 1
+
+
async def test_bluetooth_discovery_already_setup(hass):
"""Test discovery via bluetooth with a valid device when already setup."""
entry = MockConfigEntry(
@@ -322,6 +395,232 @@ async def test_user_setup_single_bot_with_password(hass):
assert len(mock_setup_entry.mock_calls) == 1
+async def test_user_setup_wolock_key(hass):
+ """Test the user initiated form for a lock."""
+
+ with patch(
+ "homeassistant.components.switchbot.config_flow.async_discovered_service_info",
+ return_value=[WOLOCK_SERVICE_INFO],
+ ):
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": SOURCE_USER}
+ )
+ assert result["type"] == FlowResultType.MENU
+ assert result["step_id"] == "lock_choose_method"
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"], user_input={"next_step_id": "lock_key"}
+ )
+ await hass.async_block_till_done()
+ assert result["type"] == FlowResultType.FORM
+ assert result["step_id"] == "lock_key"
+ assert result["errors"] == {}
+
+ with patch(
+ "homeassistant.components.switchbot.config_flow.SwitchbotLock.verify_encryption_key",
+ return_value=False,
+ ):
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {
+ CONF_KEY_ID: "",
+ CONF_ENCRYPTION_KEY: "",
+ },
+ )
+ await hass.async_block_till_done()
+
+ assert result["type"] == FlowResultType.FORM
+ assert result["step_id"] == "lock_key"
+ assert result["errors"] == {"base": "encryption_key_invalid"}
+
+ with patch_async_setup_entry() as mock_setup_entry, patch(
+ "homeassistant.components.switchbot.config_flow.SwitchbotLock.verify_encryption_key",
+ return_value=True,
+ ):
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {
+ CONF_KEY_ID: "ff",
+ CONF_ENCRYPTION_KEY: "ffffffffffffffffffffffffffffffff",
+ },
+ )
+ await hass.async_block_till_done()
+
+ assert result["type"] == FlowResultType.CREATE_ENTRY
+ assert result["title"] == "Lock EEFF"
+ assert result["data"] == {
+ CONF_ADDRESS: "aa:bb:cc:dd:ee:ff",
+ CONF_KEY_ID: "ff",
+ CONF_ENCRYPTION_KEY: "ffffffffffffffffffffffffffffffff",
+ CONF_SENSOR_TYPE: "lock",
+ }
+
+ assert len(mock_setup_entry.mock_calls) == 1
+
+
+async def test_user_setup_wolock_auth(hass):
+ """Test the user initiated form for a lock."""
+
+ with patch(
+ "homeassistant.components.switchbot.config_flow.async_discovered_service_info",
+ return_value=[WOLOCK_SERVICE_INFO],
+ ):
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": SOURCE_USER}
+ )
+ assert result["type"] == FlowResultType.MENU
+ assert result["step_id"] == "lock_choose_method"
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"], user_input={"next_step_id": "lock_auth"}
+ )
+ await hass.async_block_till_done()
+ assert result["type"] == FlowResultType.FORM
+ assert result["step_id"] == "lock_auth"
+ assert result["errors"] == {}
+
+ with patch(
+ "homeassistant.components.switchbot.config_flow.SwitchbotLock.retrieve_encryption_key",
+ side_effect=SwitchbotAuthenticationError,
+ ):
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {
+ CONF_USERNAME: "",
+ CONF_PASSWORD: "",
+ },
+ )
+ await hass.async_block_till_done()
+ assert result["type"] == FlowResultType.FORM
+ assert result["step_id"] == "lock_auth"
+ assert result["errors"] == {"base": "auth_failed"}
+
+ with patch_async_setup_entry() as mock_setup_entry, patch(
+ "homeassistant.components.switchbot.config_flow.SwitchbotLock.verify_encryption_key",
+ return_value=True,
+ ), patch(
+ "homeassistant.components.switchbot.config_flow.SwitchbotLock.retrieve_encryption_key",
+ return_value={
+ CONF_KEY_ID: "ff",
+ CONF_ENCRYPTION_KEY: "ffffffffffffffffffffffffffffffff",
+ },
+ ):
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {
+ CONF_USERNAME: "username",
+ CONF_PASSWORD: "password",
+ },
+ )
+ await hass.async_block_till_done()
+
+ assert result["type"] == FlowResultType.CREATE_ENTRY
+ assert result["title"] == "Lock EEFF"
+ assert result["data"] == {
+ CONF_ADDRESS: "aa:bb:cc:dd:ee:ff",
+ CONF_KEY_ID: "ff",
+ CONF_ENCRYPTION_KEY: "ffffffffffffffffffffffffffffffff",
+ CONF_SENSOR_TYPE: "lock",
+ }
+
+ assert len(mock_setup_entry.mock_calls) == 1
+
+
+async def test_user_setup_wolock_auth_switchbot_api_down(hass):
+ """Test the user initiated form for a lock when the switchbot api is down."""
+
+ with patch(
+ "homeassistant.components.switchbot.config_flow.async_discovered_service_info",
+ return_value=[WOLOCK_SERVICE_INFO],
+ ):
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": SOURCE_USER}
+ )
+ assert result["type"] == FlowResultType.MENU
+ assert result["step_id"] == "lock_choose_method"
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"], user_input={"next_step_id": "lock_auth"}
+ )
+ await hass.async_block_till_done()
+ assert result["type"] == FlowResultType.FORM
+ assert result["step_id"] == "lock_auth"
+ assert result["errors"] == {}
+
+ with patch(
+ "homeassistant.components.switchbot.config_flow.SwitchbotLock.retrieve_encryption_key",
+ side_effect=SwitchbotAccountConnectionError,
+ ):
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {
+ CONF_USERNAME: "",
+ CONF_PASSWORD: "",
+ },
+ )
+ await hass.async_block_till_done()
+ assert result["type"] == FlowResultType.ABORT
+ assert result["reason"] == "cannot_connect"
+
+
+async def test_user_setup_wolock_or_bot(hass):
+ """Test the user initiated form for a lock."""
+
+ with patch(
+ "homeassistant.components.switchbot.config_flow.async_discovered_service_info",
+ return_value=[
+ WOLOCK_SERVICE_INFO,
+ WOHAND_SERVICE_ALT_ADDRESS_INFO,
+ ],
+ ):
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": SOURCE_USER}
+ )
+ assert result["type"] == FlowResultType.FORM
+ assert result["step_id"] == "user"
+ assert result["errors"] == {}
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ USER_INPUT,
+ )
+ await hass.async_block_till_done()
+ assert result["type"] == FlowResultType.MENU
+ assert result["step_id"] == "lock_choose_method"
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"], user_input={"next_step_id": "lock_key"}
+ )
+ await hass.async_block_till_done()
+ assert result["type"] == FlowResultType.FORM
+ assert result["step_id"] == "lock_key"
+ assert result["errors"] == {}
+
+ with patch_async_setup_entry() as mock_setup_entry, patch(
+ "homeassistant.components.switchbot.config_flow.SwitchbotLock.verify_encryption_key",
+ return_value=True,
+ ):
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {
+ CONF_KEY_ID: "ff",
+ CONF_ENCRYPTION_KEY: "ffffffffffffffffffffffffffffffff",
+ },
+ )
+ await hass.async_block_till_done()
+
+ assert result["type"] == FlowResultType.CREATE_ENTRY
+ assert result["title"] == "Lock EEFF"
+ assert result["data"] == {
+ CONF_ADDRESS: "aa:bb:cc:dd:ee:ff",
+ CONF_KEY_ID: "ff",
+ CONF_ENCRYPTION_KEY: "ffffffffffffffffffffffffffffffff",
+ CONF_SENSOR_TYPE: "lock",
+ }
+
+ assert len(mock_setup_entry.mock_calls) == 1
+
+
async def test_user_setup_wosensor(hass):
"""Test the user initiated form with password and valid mac."""
with patch(
diff --git a/tests/components/tag/test_trigger.py b/tests/components/tag/test_trigger.py
index 3976c3193d1..379319be502 100644
--- a/tests/components/tag/test_trigger.py
+++ b/tests/components/tag/test_trigger.py
@@ -101,7 +101,7 @@ async def test_exception_bad_trigger(hass, calls, caplog):
},
)
await hass.async_block_till_done()
- assert "Invalid config for [automation]" in caplog.text
+ assert "Unnamed automation could not be validated" in caplog.text
async def test_multiple_tags_and_devices_trigger(hass, tag_setup, calls):
diff --git a/tests/components/tankerkoenig/test_config_flow.py b/tests/components/tankerkoenig/test_config_flow.py
index 600bfd98c73..266f9b67376 100644
--- a/tests/components/tankerkoenig/test_config_flow.py
+++ b/tests/components/tankerkoenig/test_config_flow.py
@@ -8,7 +8,7 @@ from homeassistant.components.tankerkoenig.const import (
CONF_STATIONS,
DOMAIN,
)
-from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_REAUTH, SOURCE_USER
+from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER
from homeassistant.const import (
CONF_API_KEY,
CONF_LATITUDE,
@@ -47,18 +47,6 @@ MOCK_OPTIONS_DATA = {
],
}
-MOCK_IMPORT_DATA = {
- CONF_API_KEY: "269534f6-xxxx-xxxx-xxxx-yyyyzzzzxxxx",
- CONF_FUEL_TYPES: ["e5"],
- CONF_LOCATION: {CONF_LATITUDE: 51.0, CONF_LONGITUDE: 13.0},
- CONF_RADIUS: 2.0,
- CONF_STATIONS: [
- "3bcd61da-yyyy-yyyy-yyyy-19d5523a7ae8",
- "36b4b812-yyyy-yyyy-yyyy-c51735325858",
- ],
- CONF_SHOW_ON_MAP: True,
-}
-
MOCK_NEARVY_STATIONS_OK = {
"ok": True,
"stations": [
@@ -187,37 +175,6 @@ async def test_user_no_stations(hass: HomeAssistant):
assert result["errors"][CONF_RADIUS] == "no_stations"
-async def test_import(hass: HomeAssistant):
- """Test starting a flow by import."""
- with patch(
- "homeassistant.components.tankerkoenig.async_setup_entry", return_value=True
- ) as mock_setup_entry, patch(
- "homeassistant.components.tankerkoenig.config_flow.getNearbyStations",
- return_value=MOCK_NEARVY_STATIONS_OK,
- ):
- result = await hass.config_entries.flow.async_init(
- DOMAIN, context={"source": SOURCE_IMPORT}, data=MOCK_IMPORT_DATA
- )
- assert result["type"] == FlowResultType.CREATE_ENTRY
- assert result["type"] == FlowResultType.CREATE_ENTRY
- assert result["data"][CONF_NAME] == "Home"
- assert result["data"][CONF_API_KEY] == "269534f6-xxxx-xxxx-xxxx-yyyyzzzzxxxx"
- assert result["data"][CONF_FUEL_TYPES] == ["e5"]
- assert result["data"][CONF_LOCATION] == {"latitude": 51.0, "longitude": 13.0}
- assert result["data"][CONF_RADIUS] == 2.0
- assert result["data"][CONF_STATIONS] == [
- "3bcd61da-xxxx-xxxx-xxxx-19d5523a7ae8",
- "36b4b812-xxxx-xxxx-xxxx-c51735325858",
- "3bcd61da-yyyy-yyyy-yyyy-19d5523a7ae8",
- "36b4b812-yyyy-yyyy-yyyy-c51735325858",
- ]
- assert result["options"][CONF_SHOW_ON_MAP]
-
- await hass.async_block_till_done()
-
- assert mock_setup_entry.called
-
-
async def test_reauth(hass: HomeAssistant):
"""Test starting a flow by user to re-auth."""
diff --git a/tests/components/tasmota/test_fan.py b/tests/components/tasmota/test_fan.py
index 8c54e913f7c..2d2cb93406b 100644
--- a/tests/components/tasmota/test_fan.py
+++ b/tests/components/tasmota/test_fan.py
@@ -154,6 +154,33 @@ async def test_sending_mqtt_commands(hass, mqtt_mock, setup_tasmota):
"tasmota_49A3BC/cmnd/FanSpeed", "3", 0, False
)
+ # Test the last known fan speed is restored
+ # First, get a fan speed update
+ async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"FanSpeed":3}')
+ state = hass.states.get("fan.tasmota")
+ assert state.state == STATE_ON
+ assert state.attributes["percentage"] == 100
+ mqtt_mock.async_publish.reset_mock()
+
+ # Then turn the fan off and get a fan state update
+ await common.async_turn_off(hass, "fan.tasmota")
+ mqtt_mock.async_publish.assert_called_once_with(
+ "tasmota_49A3BC/cmnd/FanSpeed", "0", 0, False
+ )
+ mqtt_mock.async_publish.reset_mock()
+ async_fire_mqtt_message(hass, "tasmota_49A3BC/stat/RESULT", '{"FanSpeed":0}')
+ state = hass.states.get("fan.tasmota")
+ assert state.state == STATE_OFF
+ assert state.attributes["percentage"] == 0
+ mqtt_mock.async_publish.reset_mock()
+
+ # Finally, turn the fan on again and verify MQTT message is sent with last known speed
+ await common.async_turn_on(hass, "fan.tasmota")
+ mqtt_mock.async_publish.assert_called_once_with(
+ "tasmota_49A3BC/cmnd/FanSpeed", "3", 0, False
+ )
+ mqtt_mock.async_publish.reset_mock()
+
async def test_invalid_fan_speed_percentage(hass, mqtt_mock, setup_tasmota):
"""Test the sending MQTT commands."""
diff --git a/tests/components/template/test_sensor.py b/tests/components/template/test_sensor.py
index 44fa96cfc6a..19d43f08d2b 100644
--- a/tests/components/template/test_sensor.py
+++ b/tests/components/template/test_sensor.py
@@ -292,6 +292,7 @@ async def test_template_attribute_missing(hass, start_ha):
"sensors": {
"test1": {
"value_template": "{{ states.sensor.test_sensor.state }}",
+ "unit_of_measurement": "°C",
"device_class": "temperature",
},
"test2": {
diff --git a/tests/components/tibber/test_diagnostics.py b/tests/components/tibber/test_diagnostics.py
index 38b5eb91a2f..78c0b6e321f 100644
--- a/tests/components/tibber/test_diagnostics.py
+++ b/tests/components/tibber/test_diagnostics.py
@@ -25,7 +25,7 @@ async def test_entry_diagnostics(recorder_mock, hass, hass_client, config_entry)
result = await get_diagnostics_for_config_entry(hass, hass_client, config_entry)
assert result == {
- "homes": {},
+ "homes": [],
}
with patch(
@@ -35,13 +35,13 @@ async def test_entry_diagnostics(recorder_mock, hass, hass_client, config_entry)
result = await get_diagnostics_for_config_entry(hass, hass_client, config_entry)
assert result == {
- "homes": {
- "home_id": {
+ "homes": [
+ {
"last_data_timestamp": "2016-01-01T12:48:57",
"has_active_subscription": True,
"has_real_time_consumption": False,
"last_cons_data_timestamp": "2016-01-01T12:44:57",
"country": "NO",
}
- },
+ ],
}
diff --git a/tests/components/todoist/test_calendar.py b/tests/components/todoist/test_calendar.py
index 3458a7bf5d3..b08d96a6b92 100644
--- a/tests/components/todoist/test_calendar.py
+++ b/tests/components/todoist/test_calendar.py
@@ -57,7 +57,7 @@ def mock_state() -> dict[str, Any]:
return {
"collaborators": [],
"labels": [{"name": "label1", "id": 1}],
- "projects": [{"id": 12345, "name": "Name"}],
+ "projects": [{"id": "12345", "name": "Name"}],
}
@@ -80,7 +80,7 @@ async def test_calendar_entity_unique_id(todoist_api, hass, state):
registry = entity_registry.async_get(hass)
entity = registry.async_get("calendar.name")
- assert 12345 == entity.unique_id
+ assert "12345" == entity.unique_id
@patch("homeassistant.components.todoist.calendar.TodoistAPI")
diff --git a/tests/components/tomorrowio/test_sensor.py b/tests/components/tomorrowio/test_sensor.py
index 7721e5d36ac..a93a551ae03 100644
--- a/tests/components/tomorrowio/test_sensor.py
+++ b/tests/components/tomorrowio/test_sensor.py
@@ -192,7 +192,7 @@ async def test_v4_sensor_imperial(hass: HomeAssistant) -> None:
check_sensor_state(hass, TREE_POLLEN, "none")
check_sensor_state(hass, FEELS_LIKE, "214.3")
check_sensor_state(hass, DEW_POINT, "163.08")
- check_sensor_state(hass, PRESSURE_SURFACE_LEVEL, "29.47")
+ check_sensor_state(hass, PRESSURE_SURFACE_LEVEL, "0.427")
check_sensor_state(hass, GHI, "0.0")
check_sensor_state(hass, CLOUD_BASE, "0.46")
check_sensor_state(hass, CLOUD_COVER, "100")
diff --git a/tests/components/tradfri/test_sensor.py b/tests/components/tradfri/test_sensor.py
index 1cd7a3672bc..6408613f4e3 100644
--- a/tests/components/tradfri/test_sensor.py
+++ b/tests/components/tradfri/test_sensor.py
@@ -71,6 +71,7 @@ async def test_cover_battery_sensor(hass, mock_gateway, mock_api_factory):
assert sensor_1.state == "42"
assert sensor_1.attributes["unit_of_measurement"] == "%"
assert sensor_1.attributes["device_class"] == "battery"
+ assert sensor_1.attributes["state_class"] == "measurement"
async def test_air_quality_sensor(hass, mock_gateway, mock_api_factory):
@@ -91,6 +92,7 @@ async def test_air_quality_sensor(hass, mock_gateway, mock_api_factory):
assert sensor_1.state == "42"
assert sensor_1.attributes["unit_of_measurement"] == "µg/m³"
assert sensor_1.attributes["device_class"] == "aqi"
+ assert sensor_1.attributes["state_class"] == "measurement"
async def test_filter_time_left_sensor(hass, mock_gateway, mock_api_factory):
diff --git a/tests/components/transmission/test_config_flow.py b/tests/components/transmission/test_config_flow.py
index 44edc4b28a9..7b1a1a29696 100644
--- a/tests/components/transmission/test_config_flow.py
+++ b/tests/components/transmission/test_config_flow.py
@@ -2,7 +2,7 @@
from unittest.mock import MagicMock, patch
import pytest
-from transmissionrpc.error import TransmissionError
+from transmission_rpc.error import TransmissionError
from homeassistant import config_entries
from homeassistant.components import transmission
@@ -18,7 +18,7 @@ from tests.common import MockConfigEntry
@pytest.fixture(autouse=True)
def mock_api():
"""Mock an api."""
- with patch("transmissionrpc.Client") as api:
+ with patch("transmission_rpc.Client") as api:
yield api
diff --git a/tests/components/transmission/test_init.py b/tests/components/transmission/test_init.py
index 60e2d67d75c..da5e6859544 100644
--- a/tests/components/transmission/test_init.py
+++ b/tests/components/transmission/test_init.py
@@ -3,7 +3,7 @@
from unittest.mock import MagicMock, patch
import pytest
-from transmissionrpc.error import TransmissionError
+from transmission_rpc.error import TransmissionError
from homeassistant.components.transmission.const import DOMAIN
from homeassistant.config_entries import ConfigEntryState
@@ -17,7 +17,7 @@ from tests.common import MockConfigEntry
@pytest.fixture(autouse=True)
def mock_api():
"""Mock an api."""
- with patch("transmissionrpc.Client") as api:
+ with patch("transmission_rpc.Client") as api:
yield api
diff --git a/tests/components/unifi/test_sensor.py b/tests/components/unifi/test_sensor.py
index 100918a93da..ebdea40fe73 100644
--- a/tests/components/unifi/test_sensor.py
+++ b/tests/components/unifi/test_sensor.py
@@ -13,10 +13,8 @@ from homeassistant.components.unifi.const import (
CONF_ALLOW_UPTIME_SENSORS,
CONF_TRACK_CLIENTS,
CONF_TRACK_DEVICES,
- DOMAIN as UNIFI_DOMAIN,
)
from homeassistant.helpers import entity_registry as er
-from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.entity import EntityCategory
import homeassistant.util.dt as dt_util
@@ -106,37 +104,6 @@ async def test_bandwidth_sensors(hass, aioclient_mock, mock_unifi_websocket):
assert hass.states.get("sensor.wired_client_rx") is None
assert hass.states.get("sensor.wired_client_tx") is None
- # Enable option
-
- options[CONF_ALLOW_BANDWIDTH_SENSORS] = True
- hass.config_entries.async_update_entry(config_entry, options=options.copy())
- await hass.async_block_till_done()
-
- assert len(hass.states.async_all()) == 5
- assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 4
- assert hass.states.get("sensor.wireless_client_rx")
- assert hass.states.get("sensor.wireless_client_tx")
- assert hass.states.get("sensor.wired_client_rx")
- assert hass.states.get("sensor.wired_client_tx")
-
- # Try to add the sensors again, using a signal
-
- clients_connected = {wired_client["mac"], wireless_client["mac"]}
- devices_connected = set()
-
- controller = hass.data[UNIFI_DOMAIN][config_entry.entry_id]
-
- async_dispatcher_send(
- hass,
- controller.signal_update,
- clients_connected,
- devices_connected,
- )
- await hass.async_block_till_done()
-
- assert len(hass.states.async_all()) == 5
- assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 4
-
@pytest.mark.parametrize(
"initial_uptime,event_uptime,new_uptime",
@@ -220,35 +187,6 @@ async def test_uptime_sensors(
assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 0
assert hass.states.get("sensor.client1_uptime") is None
- # Enable option
-
- options[CONF_ALLOW_UPTIME_SENSORS] = True
- with patch("homeassistant.util.dt.now", return_value=now):
- hass.config_entries.async_update_entry(config_entry, options=options.copy())
- await hass.async_block_till_done()
-
- assert len(hass.states.async_all()) == 2
- assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 1
- assert hass.states.get("sensor.client1_uptime")
-
- # Try to add the sensors again, using a signal
-
- clients_connected = {uptime_client["mac"]}
- devices_connected = set()
-
- controller = hass.data[UNIFI_DOMAIN][config_entry.entry_id]
-
- async_dispatcher_send(
- hass,
- controller.signal_update,
- clients_connected,
- devices_connected,
- )
- await hass.async_block_till_done()
-
- assert len(hass.states.async_all()) == 2
- assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 1
-
async def test_remove_sensors(hass, aioclient_mock, mock_unifi_websocket):
"""Verify removing of clients work as expected."""
diff --git a/tests/components/unifiprotect/test_repairs.py b/tests/components/unifiprotect/test_repairs.py
index 3ffd2ea4a43..1d1d7315aac 100644
--- a/tests/components/unifiprotect/test_repairs.py
+++ b/tests/components/unifiprotect/test_repairs.py
@@ -4,10 +4,11 @@ from __future__ import annotations
from copy import copy
from http import HTTPStatus
-from unittest.mock import Mock
+from unittest.mock import Mock, patch
-from pyunifiprotect.data import Version
+from pyunifiprotect.data import Camera, Version
+from homeassistant.components.automation import DOMAIN as AUTOMATION_DOMAIN
from homeassistant.components.repairs.issue_handler import (
async_process_repairs_platforms,
)
@@ -15,8 +16,12 @@ from homeassistant.components.repairs.websocket_api import (
RepairsFlowIndexView,
RepairsFlowResourceView,
)
+from homeassistant.components.script import DOMAIN as SCRIPT_DOMAIN
from homeassistant.components.unifiprotect.const import DOMAIN
+from homeassistant.const import SERVICE_RELOAD, Platform
from homeassistant.core import HomeAssistant
+from homeassistant.helpers import entity_registry as er
+from homeassistant.setup import async_setup_component
from .utils import MockUFPFixture, init_entry
@@ -124,3 +129,198 @@ async def test_ea_warning_fix(
data = await resp.json()
assert data["type"] == "create_entry"
+
+
+async def test_deprecate_smart_default(
+ hass: HomeAssistant, ufp: MockUFPFixture, hass_ws_client, doorbell: Camera
+):
+ """Test Deprecate Sensor repair does not exist by default (new installs)."""
+
+ await init_entry(hass, ufp, [doorbell])
+
+ await async_process_repairs_platforms(hass)
+ ws_client = await hass_ws_client(hass)
+
+ await ws_client.send_json({"id": 1, "type": "repairs/list_issues"})
+ msg = await ws_client.receive_json()
+
+ assert msg["success"]
+ issue = None
+ for i in msg["result"]["issues"]:
+ if i["issue_id"] == "deprecate_smart_sensor":
+ issue = i
+ assert issue is None
+
+
+async def test_deprecate_smart_no_automations(
+ hass: HomeAssistant, ufp: MockUFPFixture, hass_ws_client, doorbell: Camera
+):
+ """Test Deprecate Sensor repair exists for existing installs."""
+
+ registry = er.async_get(hass)
+ registry.async_get_or_create(
+ Platform.SENSOR,
+ DOMAIN,
+ f"{doorbell.mac}_detected_object",
+ config_entry=ufp.entry,
+ )
+
+ await init_entry(hass, ufp, [doorbell])
+
+ await async_process_repairs_platforms(hass)
+ ws_client = await hass_ws_client(hass)
+
+ await ws_client.send_json({"id": 1, "type": "repairs/list_issues"})
+ msg = await ws_client.receive_json()
+
+ assert msg["success"]
+ issue = None
+ for i in msg["result"]["issues"]:
+ if i["issue_id"] == "deprecate_smart_sensor":
+ issue = i
+ assert issue is None
+
+
+async def _load_automation(hass: HomeAssistant, entity_id: str):
+ assert await async_setup_component(
+ hass,
+ AUTOMATION_DOMAIN,
+ {
+ AUTOMATION_DOMAIN: [
+ {
+ "alias": "test1",
+ "trigger": [
+ {"platform": "state", "entity_id": entity_id},
+ {
+ "platform": "event",
+ "event_type": "state_changed",
+ "event_data": {"entity_id": entity_id},
+ },
+ ],
+ "condition": {
+ "condition": "state",
+ "entity_id": entity_id,
+ "state": "on",
+ },
+ "action": [
+ {
+ "service": "test.script",
+ "data": {"entity_id": entity_id},
+ },
+ ],
+ },
+ ]
+ },
+ )
+
+
+async def test_deprecate_smart_automation(
+ hass: HomeAssistant, ufp: MockUFPFixture, hass_ws_client, doorbell: Camera
+):
+ """Test Deprecate Sensor repair exists for existing installs."""
+
+ registry = er.async_get(hass)
+ entry = registry.async_get_or_create(
+ Platform.SENSOR,
+ DOMAIN,
+ f"{doorbell.mac}_detected_object",
+ config_entry=ufp.entry,
+ )
+ await _load_automation(hass, entry.entity_id)
+ await init_entry(hass, ufp, [doorbell])
+
+ await async_process_repairs_platforms(hass)
+ ws_client = await hass_ws_client(hass)
+
+ await ws_client.send_json({"id": 1, "type": "repairs/list_issues"})
+ msg = await ws_client.receive_json()
+
+ assert msg["success"]
+ issue = None
+ for i in msg["result"]["issues"]:
+ if i["issue_id"] == "deprecate_smart_sensor":
+ issue = i
+ assert issue is not None
+
+ with patch(
+ "homeassistant.config.load_yaml_config_file",
+ autospec=True,
+ return_value={AUTOMATION_DOMAIN: []},
+ ):
+ await hass.services.async_call(AUTOMATION_DOMAIN, SERVICE_RELOAD, blocking=True)
+ await hass.async_block_till_done()
+
+ await ws_client.send_json({"id": 2, "type": "repairs/list_issues"})
+ msg = await ws_client.receive_json()
+
+ assert msg["success"]
+ issue = None
+ for i in msg["result"]["issues"]:
+ if i["issue_id"] == "deprecate_smart_sensor":
+ issue = i
+ assert issue is None
+
+
+async def _load_script(hass: HomeAssistant, entity_id: str):
+ assert await async_setup_component(
+ hass,
+ SCRIPT_DOMAIN,
+ {
+ SCRIPT_DOMAIN: {
+ "test": {
+ "sequence": {
+ "service": "test.script",
+ "data": {"entity_id": entity_id},
+ }
+ }
+ },
+ },
+ )
+
+
+async def test_deprecate_smart_script(
+ hass: HomeAssistant, ufp: MockUFPFixture, hass_ws_client, doorbell: Camera
+):
+ """Test Deprecate Sensor repair exists for existing installs."""
+
+ registry = er.async_get(hass)
+ entry = registry.async_get_or_create(
+ Platform.SENSOR,
+ DOMAIN,
+ f"{doorbell.mac}_detected_object",
+ config_entry=ufp.entry,
+ )
+ await _load_script(hass, entry.entity_id)
+ await init_entry(hass, ufp, [doorbell])
+
+ await async_process_repairs_platforms(hass)
+ ws_client = await hass_ws_client(hass)
+
+ await ws_client.send_json({"id": 1, "type": "repairs/list_issues"})
+ msg = await ws_client.receive_json()
+
+ assert msg["success"]
+ issue = None
+ for i in msg["result"]["issues"]:
+ if i["issue_id"] == "deprecate_smart_sensor":
+ issue = i
+ assert issue is not None
+
+ with patch(
+ "homeassistant.config.load_yaml_config_file",
+ autospec=True,
+ return_value={SCRIPT_DOMAIN: {}},
+ ):
+ await hass.services.async_call(SCRIPT_DOMAIN, SERVICE_RELOAD, blocking=True)
+ await hass.config_entries.async_reload(ufp.entry.entry_id)
+ await hass.async_block_till_done()
+
+ await ws_client.send_json({"id": 2, "type": "repairs/list_issues"})
+ msg = await ws_client.receive_json()
+
+ assert msg["success"]
+ issue = None
+ for i in msg["result"]["issues"]:
+ if i["issue_id"] == "deprecate_smart_sensor":
+ issue = i
+ assert issue is None
diff --git a/tests/components/unifiprotect/test_text.py b/tests/components/unifiprotect/test_text.py
new file mode 100644
index 00000000000..17fe3ee7bc2
--- /dev/null
+++ b/tests/components/unifiprotect/test_text.py
@@ -0,0 +1,92 @@
+"""Test the UniFi Protect text platform."""
+# pylint: disable=protected-access
+from __future__ import annotations
+
+from unittest.mock import AsyncMock, Mock
+
+from pyunifiprotect.data import Camera, DoorbellMessageType, LCDMessage
+
+from homeassistant.components.unifiprotect.const import DEFAULT_ATTRIBUTION
+from homeassistant.components.unifiprotect.text import CAMERA
+from homeassistant.const import ATTR_ATTRIBUTION, ATTR_ENTITY_ID, Platform
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers import entity_registry as er
+
+from .utils import (
+ MockUFPFixture,
+ adopt_devices,
+ assert_entity_counts,
+ ids_from_device_description,
+ init_entry,
+ remove_entities,
+)
+
+
+async def test_text_camera_remove(
+ hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera, unadopted_camera: Camera
+):
+ """Test removing and re-adding a camera device."""
+
+ ufp.api.bootstrap.nvr.system_info.ustorage = None
+ await init_entry(hass, ufp, [doorbell, unadopted_camera])
+ assert_entity_counts(hass, Platform.TEXT, 1, 1)
+ await remove_entities(hass, ufp, [doorbell, unadopted_camera])
+ assert_entity_counts(hass, Platform.TEXT, 0, 0)
+ await adopt_devices(hass, ufp, [doorbell, unadopted_camera])
+ assert_entity_counts(hass, Platform.TEXT, 1, 1)
+
+
+async def test_text_camera_setup(
+ hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera
+):
+ """Test text entity setup for camera devices."""
+
+ doorbell.lcd_message = LCDMessage(
+ type=DoorbellMessageType.CUSTOM_MESSAGE, text="Test"
+ )
+ await init_entry(hass, ufp, [doorbell])
+ assert_entity_counts(hass, Platform.TEXT, 1, 1)
+
+ entity_registry = er.async_get(hass)
+
+ description = CAMERA[0]
+ unique_id, entity_id = ids_from_device_description(
+ Platform.TEXT, doorbell, description
+ )
+
+ entity = entity_registry.async_get(entity_id)
+ assert entity
+ assert entity.unique_id == unique_id
+
+ state = hass.states.get(entity_id)
+ assert state
+ assert state.state == "Test"
+ assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION
+
+
+async def test_text_camera_set(
+ hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera
+):
+ """Test text entity setting value camera devices."""
+
+ await init_entry(hass, ufp, [doorbell])
+ assert_entity_counts(hass, Platform.TEXT, 1, 1)
+
+ description = CAMERA[0]
+ unique_id, entity_id = ids_from_device_description(
+ Platform.TEXT, doorbell, description
+ )
+
+ doorbell.__fields__["set_lcd_text"] = Mock(final=False)
+ doorbell.set_lcd_text = AsyncMock()
+
+ await hass.services.async_call(
+ "text",
+ "set_value",
+ {ATTR_ENTITY_ID: entity_id, "value": "Test test"},
+ blocking=True,
+ )
+
+ doorbell.set_lcd_text.assert_called_once_with(
+ DoorbellMessageType.CUSTOM_MESSAGE, text="Test test"
+ )
diff --git a/tests/components/uptimerobot/test_sensor.py b/tests/components/uptimerobot/test_sensor.py
index 3e833af9bd4..68b64f70e5a 100644
--- a/tests/components/uptimerobot/test_sensor.py
+++ b/tests/components/uptimerobot/test_sensor.py
@@ -4,6 +4,7 @@ from unittest.mock import patch
from pyuptimerobot import UptimeRobotAuthenticationException
+from homeassistant.components.sensor import SensorDeviceClass
from homeassistant.components.uptimerobot.const import COORDINATOR_UPDATE_INTERVAL
from homeassistant.const import STATE_UNAVAILABLE
from homeassistant.core import HomeAssistant
@@ -30,6 +31,14 @@ async def test_presentation(hass: HomeAssistant) -> None:
assert entity.state == STATE_UP
assert entity.attributes["icon"] == SENSOR_ICON
assert entity.attributes["target"] == MOCK_UPTIMEROBOT_MONITOR["url"]
+ assert entity.attributes["device_class"] == SensorDeviceClass.ENUM
+ assert entity.attributes["options"] == [
+ "down",
+ "not_checked_yet",
+ "pause",
+ "seems_down",
+ "up",
+ ]
async def test_unaviable_on_update_failure(hass: HomeAssistant) -> None:
diff --git a/tests/components/vallox/conftest.py b/tests/components/vallox/conftest.py
index 0c14f359b5f..60d8bfc0562 100644
--- a/tests/components/vallox/conftest.py
+++ b/tests/components/vallox/conftest.py
@@ -39,13 +39,36 @@ def patch_metrics(metrics: dict[str, Any]):
)
+def patch_profile(profile: PROFILE):
+ """Patch the Vallox metrics response."""
+ return patch(
+ "homeassistant.components.vallox.Vallox.get_profile",
+ return_value=profile,
+ )
+
+
+def patch_profile_set():
+ """Patch the Vallox metrics set values."""
+ return patch("homeassistant.components.vallox.Vallox.set_profile")
+
+
def patch_metrics_set():
"""Patch the Vallox metrics set values."""
return patch("homeassistant.components.vallox.Vallox.set_values")
@pytest.fixture(autouse=True)
-def patch_profile_home():
+def patch_empty_metrics():
+ """Patch the Vallox profile response."""
+ with patch(
+ "homeassistant.components.vallox.Vallox.fetch_metrics",
+ return_value={},
+ ):
+ yield
+
+
+@pytest.fixture(autouse=True)
+def patch_default_profile():
"""Patch the Vallox profile response."""
with patch(
"homeassistant.components.vallox.Vallox.get_profile",
diff --git a/tests/components/vallox/test_config_flow.py b/tests/components/vallox/test_config_flow.py
index b0c951383b8..39de026bdbb 100644
--- a/tests/components/vallox/test_config_flow.py
+++ b/tests/components/vallox/test_config_flow.py
@@ -1,7 +1,7 @@
"""Test the Vallox integration config flow."""
from unittest.mock import patch
-from vallox_websocket_api.exceptions import ValloxApiException
+from vallox_websocket_api import ValloxApiException, ValloxWebsocketException
from homeassistant.components.vallox.const import DOMAIN
from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER
@@ -95,7 +95,7 @@ async def test_form_os_error_cannot_connect(hass: HomeAssistant) -> None:
with patch(
"homeassistant.components.vallox.config_flow.Vallox.get_info",
- side_effect=OSError,
+ side_effect=ValloxWebsocketException,
):
result = await hass.config_entries.flow.async_configure(
init["flow_id"],
@@ -243,7 +243,7 @@ async def test_import_cannot_connect_os_error(hass: HomeAssistant) -> None:
with patch(
"homeassistant.components.vallox.config_flow.Vallox.get_info",
- side_effect=OSError,
+ side_effect=ValloxWebsocketException,
):
result = await hass.config_entries.flow.async_init(
DOMAIN,
diff --git a/tests/components/vallox/test_fan.py b/tests/components/vallox/test_fan.py
new file mode 100644
index 00000000000..beb9a2647ef
--- /dev/null
+++ b/tests/components/vallox/test_fan.py
@@ -0,0 +1,259 @@
+"""Tests for Vallox fan platform."""
+from unittest.mock import call
+
+import pytest
+from vallox_websocket_api import PROFILE, ValloxApiException
+
+from homeassistant.components.fan import (
+ ATTR_PERCENTAGE,
+ ATTR_PRESET_MODE,
+ DOMAIN as FAN_DOMAIN,
+ SERVICE_SET_PERCENTAGE,
+ SERVICE_SET_PRESET_MODE,
+)
+from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON
+from homeassistant.core import HomeAssistant
+from homeassistant.exceptions import HomeAssistantError
+
+from .conftest import patch_metrics, patch_metrics_set, patch_profile, patch_profile_set
+
+from tests.common import MockConfigEntry
+
+
+@pytest.mark.parametrize(
+ "metrics, expected_state", [({"A_CYC_MODE": 0}, "on"), ({"A_CYC_MODE": 5}, "off")]
+)
+async def test_fan_state(
+ metrics: dict[str, int],
+ expected_state: str,
+ mock_entry: MockConfigEntry,
+ hass: HomeAssistant,
+) -> None:
+ """Test fan on/off state."""
+
+ # Act
+ with patch_metrics(metrics=metrics):
+ await hass.config_entries.async_setup(mock_entry.entry_id)
+ await hass.async_block_till_done()
+
+ # Assert
+ sensor = hass.states.get("fan.vallox")
+ assert sensor
+ assert sensor.state == expected_state
+
+
+@pytest.mark.parametrize(
+ "profile, expected_preset",
+ [
+ (PROFILE.HOME, "Home"),
+ (PROFILE.AWAY, "Away"),
+ (PROFILE.BOOST, "Boost"),
+ (PROFILE.FIREPLACE, "Fireplace"),
+ ],
+)
+async def test_fan_profile(
+ profile: PROFILE,
+ expected_preset: str,
+ mock_entry: MockConfigEntry,
+ hass: HomeAssistant,
+) -> None:
+ """Test fan profile."""
+
+ # Act
+ with patch_profile(profile):
+ await hass.config_entries.async_setup(mock_entry.entry_id)
+ await hass.async_block_till_done()
+
+ # Assert
+ sensor = hass.states.get("fan.vallox")
+ assert sensor
+ assert sensor.attributes["preset_mode"] == expected_preset
+
+
+@pytest.mark.parametrize(
+ "service, initial_metrics, expected_called_with",
+ [
+ (SERVICE_TURN_ON, {"A_CYC_MODE": 5}, {"A_CYC_MODE": 0}),
+ (SERVICE_TURN_OFF, {"A_CYC_MODE": 0}, {"A_CYC_MODE": 5}),
+ ],
+)
+async def test_turn_on_off(
+ service: str,
+ initial_metrics: dict[str, int],
+ expected_called_with: dict[str, int],
+ mock_entry: MockConfigEntry,
+ hass: HomeAssistant,
+) -> None:
+ """Test turn on/off."""
+ with patch_metrics(metrics=initial_metrics), patch_metrics_set() as metrics_set:
+ await hass.config_entries.async_setup(mock_entry.entry_id)
+ await hass.async_block_till_done()
+ await hass.services.async_call(
+ FAN_DOMAIN,
+ service,
+ service_data={ATTR_ENTITY_ID: "fan.vallox"},
+ blocking=True,
+ )
+ metrics_set.assert_called_once_with(expected_called_with)
+
+
+@pytest.mark.parametrize(
+ "initial_metrics, expected_call_args_list",
+ [
+ (
+ {"A_CYC_MODE": 5},
+ [
+ call({"A_CYC_MODE": 0}),
+ call({"A_CYC_AWAY_SPEED_SETTING": 15}),
+ ],
+ ),
+ (
+ {"A_CYC_MODE": 0},
+ [
+ call({"A_CYC_AWAY_SPEED_SETTING": 15}),
+ ],
+ ),
+ ],
+)
+async def test_turn_on_with_parameters(
+ initial_metrics: dict[str, int],
+ expected_call_args_list: list[tuple],
+ mock_entry: MockConfigEntry,
+ hass: HomeAssistant,
+) -> None:
+ """Test turn on/off."""
+ with patch_metrics(
+ metrics=initial_metrics
+ ), patch_metrics_set() as metrics_set, patch_profile_set() as profile_set:
+ await hass.config_entries.async_setup(mock_entry.entry_id)
+ await hass.async_block_till_done()
+ await hass.services.async_call(
+ FAN_DOMAIN,
+ SERVICE_TURN_ON,
+ service_data={
+ ATTR_ENTITY_ID: "fan.vallox",
+ ATTR_PERCENTAGE: "15",
+ ATTR_PRESET_MODE: "Away",
+ },
+ blocking=True,
+ )
+ assert metrics_set.call_args_list == expected_call_args_list
+ profile_set.assert_called_once_with(PROFILE.AWAY)
+
+
+@pytest.mark.parametrize(
+ "preset, initial_profile, expected_call_args_list",
+ [
+ ("Home", PROFILE.AWAY, [call(PROFILE.HOME)]),
+ ("Away", PROFILE.HOME, [call(PROFILE.AWAY)]),
+ ("Boost", PROFILE.HOME, [call(PROFILE.BOOST)]),
+ ("Fireplace", PROFILE.HOME, [call(PROFILE.FIREPLACE)]),
+ ("Home", PROFILE.HOME, []),
+ ],
+)
+async def test_set_preset_mode(
+ preset: str,
+ initial_profile: PROFILE,
+ expected_call_args_list: list[tuple],
+ mock_entry: MockConfigEntry,
+ hass: HomeAssistant,
+) -> None:
+ """Test set preset mode."""
+ with patch_profile(initial_profile), patch_profile_set() as profile_set:
+ await hass.config_entries.async_setup(mock_entry.entry_id)
+ await hass.async_block_till_done()
+ await hass.services.async_call(
+ FAN_DOMAIN,
+ SERVICE_SET_PRESET_MODE,
+ service_data={ATTR_ENTITY_ID: "fan.vallox", ATTR_PRESET_MODE: preset},
+ blocking=True,
+ )
+ assert profile_set.call_args_list == expected_call_args_list
+
+
+async def test_set_invalid_preset_mode(
+ mock_entry: MockConfigEntry,
+ hass: HomeAssistant,
+) -> None:
+ """Test set preset mode."""
+ await hass.config_entries.async_setup(mock_entry.entry_id)
+ await hass.async_block_till_done()
+ with pytest.raises(ValueError):
+ await hass.services.async_call(
+ FAN_DOMAIN,
+ SERVICE_SET_PRESET_MODE,
+ service_data={
+ ATTR_ENTITY_ID: "fan.vallox",
+ ATTR_PRESET_MODE: "Invalid",
+ },
+ blocking=True,
+ )
+
+
+async def test_set_preset_mode_exception(
+ mock_entry: MockConfigEntry,
+ hass: HomeAssistant,
+) -> None:
+ """Test set preset mode."""
+ with patch_profile_set() as profile_set:
+ profile_set.side_effect = ValloxApiException("Fake exception")
+ await hass.config_entries.async_setup(mock_entry.entry_id)
+ await hass.async_block_till_done()
+ with pytest.raises(HomeAssistantError):
+ await hass.services.async_call(
+ FAN_DOMAIN,
+ SERVICE_SET_PRESET_MODE,
+ service_data={ATTR_ENTITY_ID: "fan.vallox", ATTR_PRESET_MODE: "Away"},
+ blocking=True,
+ )
+
+
+@pytest.mark.parametrize(
+ "profile, percentage, expected_call_args_list",
+ [
+ (PROFILE.HOME, 40, [call({"A_CYC_HOME_SPEED_SETTING": 40})]),
+ (PROFILE.AWAY, 30, [call({"A_CYC_AWAY_SPEED_SETTING": 30})]),
+ (PROFILE.BOOST, 60, [call({"A_CYC_BOOST_SPEED_SETTING": 60})]),
+ (PROFILE.HOME, 0, [call({"A_CYC_MODE": 5})]),
+ ],
+)
+async def test_set_fan_speed(
+ profile: PROFILE,
+ percentage: int,
+ expected_call_args_list: list[tuple],
+ mock_entry: MockConfigEntry,
+ hass: HomeAssistant,
+) -> None:
+ """Test set fan speed percentage."""
+ with patch_profile(profile), patch_metrics_set() as metrics_set, patch_metrics(
+ {"A_CYC_MODE": 0}
+ ):
+ await hass.config_entries.async_setup(mock_entry.entry_id)
+ await hass.async_block_till_done()
+ await hass.services.async_call(
+ FAN_DOMAIN,
+ SERVICE_SET_PERCENTAGE,
+ service_data={ATTR_ENTITY_ID: "fan.vallox", ATTR_PERCENTAGE: percentage},
+ blocking=True,
+ )
+ assert metrics_set.call_args_list == expected_call_args_list
+
+
+async def test_set_fan_speed_exception(
+ mock_entry: MockConfigEntry,
+ hass: HomeAssistant,
+) -> None:
+ """Test set fan speed percentage."""
+ with patch_metrics_set() as metrics_set, patch_metrics(
+ {"A_CYC_MODE": 0, "A_CYC_HOME_SPEED_SETTING": 30}
+ ):
+ metrics_set.side_effect = ValloxApiException("Fake failure")
+ await hass.config_entries.async_setup(mock_entry.entry_id)
+ await hass.async_block_till_done()
+ with pytest.raises(HomeAssistantError):
+ await hass.services.async_call(
+ FAN_DOMAIN,
+ SERVICE_SET_PERCENTAGE,
+ service_data={ATTR_ENTITY_ID: "fan.vallox", ATTR_PERCENTAGE: 5},
+ blocking=True,
+ )
diff --git a/tests/components/venstar/__init__.py b/tests/components/venstar/__init__.py
index c7b4815c5bb..fa35dd88379 100644
--- a/tests/components/venstar/__init__.py
+++ b/tests/components/venstar/__init__.py
@@ -43,6 +43,7 @@ class VenstarColorTouchMock:
def update_info(self):
"""Mock update_info."""
+ self.name = "username"
return True
def broken_update_info(self):
diff --git a/tests/components/withings/common.py b/tests/components/withings/common.py
index 598e7ab8c33..96a35d10c40 100644
--- a/tests/components/withings/common.py
+++ b/tests/components/withings/common.py
@@ -24,7 +24,9 @@ from homeassistant.components.withings import async_unload_entry
from homeassistant.components.withings.common import (
ConfigEntryWithingsApi,
DataManager,
+ WithingsEntityDescription,
get_all_data_managers,
+ get_attribute_unique_id,
)
import homeassistant.components.withings.const as const
from homeassistant.config import async_process_ha_core_config
@@ -37,7 +39,7 @@ from homeassistant.const import (
CONF_UNIT_SYSTEM_METRIC,
)
from homeassistant.core import HomeAssistant
-from homeassistant.helpers import config_entry_oauth2_flow
+from homeassistant.helpers import config_entry_oauth2_flow, entity_registry as er
from homeassistant.helpers.config_entry_oauth2_flow import AUTH_CALLBACK_PATH
from homeassistant.setup import async_setup_component
from homeassistant.util import dt as dt_util
@@ -320,3 +322,16 @@ def get_data_manager_by_user_id(
),
None,
)
+
+
+async def async_get_entity_id(
+ hass: HomeAssistant,
+ description: WithingsEntityDescription,
+ user_id: int,
+ platform: str,
+) -> str | None:
+ """Get an entity id for a user's attribute."""
+ entity_registry = er.async_get(hass)
+ unique_id = get_attribute_unique_id(description, user_id)
+
+ return entity_registry.async_get_entity_id(platform, const.DOMAIN, unique_id)
diff --git a/tests/components/withings/test_binary_sensor.py b/tests/components/withings/test_binary_sensor.py
index 9f93e00f4ef..e3205ffa2db 100644
--- a/tests/components/withings/test_binary_sensor.py
+++ b/tests/components/withings/test_binary_sensor.py
@@ -1,17 +1,20 @@
"""Tests for the Withings component."""
from withings_api.common import NotifyAppli
-from homeassistant.components.withings.common import (
- WITHINGS_MEASUREMENTS_MAP,
- async_get_entity_id,
-)
+from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
+from homeassistant.components.withings.binary_sensor import BINARY_SENSORS
+from homeassistant.components.withings.common import WithingsEntityDescription
from homeassistant.components.withings.const import Measurement
from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.entity_registry import EntityRegistry
-from .common import ComponentFactory, new_profile_config
+from .common import ComponentFactory, async_get_entity_id, new_profile_config
+
+WITHINGS_MEASUREMENTS_MAP: dict[Measurement, WithingsEntityDescription] = {
+ attr.measurement: attr for attr in BINARY_SENSORS
+}
async def test_binary_sensor(
@@ -25,15 +28,23 @@ async def test_binary_sensor(
entity_registry: EntityRegistry = er.async_get(hass)
await component_factory.configure_component(profile_configs=(person0, person1))
- assert not await async_get_entity_id(hass, in_bed_attribute, person0.user_id)
- assert not await async_get_entity_id(hass, in_bed_attribute, person1.user_id)
+ assert not await async_get_entity_id(
+ hass, in_bed_attribute, person0.user_id, BINARY_SENSOR_DOMAIN
+ )
+ assert not await async_get_entity_id(
+ hass, in_bed_attribute, person1.user_id, BINARY_SENSOR_DOMAIN
+ )
# person 0
await component_factory.setup_profile(person0.user_id)
await component_factory.setup_profile(person1.user_id)
- entity_id0 = await async_get_entity_id(hass, in_bed_attribute, person0.user_id)
- entity_id1 = await async_get_entity_id(hass, in_bed_attribute, person1.user_id)
+ entity_id0 = await async_get_entity_id(
+ hass, in_bed_attribute, person0.user_id, BINARY_SENSOR_DOMAIN
+ )
+ entity_id1 = await async_get_entity_id(
+ hass, in_bed_attribute, person1.user_id, BINARY_SENSOR_DOMAIN
+ )
assert entity_id0
assert entity_id1
diff --git a/tests/components/withings/test_sensor.py b/tests/components/withings/test_sensor.py
index fbe9c6304bb..a22ba36e59a 100644
--- a/tests/components/withings/test_sensor.py
+++ b/tests/components/withings/test_sensor.py
@@ -17,20 +17,20 @@ from withings_api.common import (
SleepModel,
)
-from homeassistant.components.withings.common import (
- WITHINGS_MEASUREMENTS_MAP,
- WithingsAttribute,
- async_get_entity_id,
- get_platform_attributes,
-)
+from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
+from homeassistant.components.withings.common import WithingsEntityDescription
from homeassistant.components.withings.const import Measurement
-from homeassistant.const import Platform
+from homeassistant.components.withings.sensor import SENSORS
from homeassistant.core import HomeAssistant, State
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.entity_registry import EntityRegistry
from homeassistant.util import dt as dt_util
-from .common import ComponentFactory, new_profile_config
+from .common import ComponentFactory, async_get_entity_id, new_profile_config
+
+WITHINGS_MEASUREMENTS_MAP: dict[Measurement, WithingsEntityDescription] = {
+ attr.measurement: attr for attr in SENSORS
+}
PERSON0 = new_profile_config(
"person0",
@@ -290,14 +290,17 @@ EXPECTED_DATA = (
def async_assert_state_equals(
- entity_id: str, state_obj: State, expected: Any, attribute: WithingsAttribute
+ entity_id: str,
+ state_obj: State,
+ expected: Any,
+ description: WithingsEntityDescription,
) -> None:
"""Assert at given state matches what is expected."""
assert state_obj, f"Expected entity {entity_id} to exist but it did not"
assert state_obj.state == str(expected), (
f"Expected {expected} but was {state_obj.state} "
- f"for measure {attribute.measurement}, {entity_id}"
+ f"for measure {description.measurement}, {entity_id}"
)
@@ -310,15 +313,19 @@ async def test_sensor_default_enabled_entities(
await component_factory.configure_component(profile_configs=(PERSON0,))
# Assert entities should not exist yet.
- for attribute in get_platform_attributes(Platform.SENSOR):
- assert not await async_get_entity_id(hass, attribute, PERSON0.user_id)
+ for attribute in SENSORS:
+ assert not await async_get_entity_id(
+ hass, attribute, PERSON0.user_id, SENSOR_DOMAIN
+ )
# person 0
await component_factory.setup_profile(PERSON0.user_id)
# Assert entities should exist.
- for attribute in get_platform_attributes(Platform.SENSOR):
- entity_id = await async_get_entity_id(hass, attribute, PERSON0.user_id)
+ for attribute in SENSORS:
+ entity_id = await async_get_entity_id(
+ hass, attribute, PERSON0.user_id, SENSOR_DOMAIN
+ )
assert entity_id
assert entity_registry.async_is_registered(entity_id)
@@ -330,10 +337,12 @@ async def test_sensor_default_enabled_entities(
for person, measurement, expected in EXPECTED_DATA:
attribute = WITHINGS_MEASUREMENTS_MAP[measurement]
- entity_id = await async_get_entity_id(hass, attribute, person.user_id)
+ entity_id = await async_get_entity_id(
+ hass, attribute, person.user_id, SENSOR_DOMAIN
+ )
state_obj = hass.states.get(entity_id)
- if attribute.enabled_by_default:
+ if attribute.entity_registry_enabled_default:
async_assert_state_equals(entity_id, state_obj, expected, attribute)
else:
assert state_obj is None
@@ -356,15 +365,19 @@ async def test_all_entities(
await component_factory.configure_component(profile_configs=(PERSON0,))
# Assert entities should not exist yet.
- for attribute in get_platform_attributes(Platform.SENSOR):
- assert not await async_get_entity_id(hass, attribute, PERSON0.user_id)
+ for attribute in SENSORS:
+ assert not await async_get_entity_id(
+ hass, attribute, PERSON0.user_id, SENSOR_DOMAIN
+ )
# person 0
await component_factory.setup_profile(PERSON0.user_id)
# Assert entities should exist.
- for attribute in get_platform_attributes(Platform.SENSOR):
- entity_id = await async_get_entity_id(hass, attribute, PERSON0.user_id)
+ for attribute in SENSORS:
+ entity_id = await async_get_entity_id(
+ hass, attribute, PERSON0.user_id, SENSOR_DOMAIN
+ )
assert entity_id
assert entity_registry.async_is_registered(entity_id)
@@ -376,7 +389,9 @@ async def test_all_entities(
for person, measurement, expected in EXPECTED_DATA:
attribute = WITHINGS_MEASUREMENTS_MAP[measurement]
- entity_id = await async_get_entity_id(hass, attribute, person.user_id)
+ entity_id = await async_get_entity_id(
+ hass, attribute, person.user_id, SENSOR_DOMAIN
+ )
state_obj = hass.states.get(entity_id)
async_assert_state_equals(entity_id, state_obj, expected, attribute)
diff --git a/tests/components/yeelight/__init__.py b/tests/components/yeelight/__init__.py
index ea3c8f4656c..64f6bcdadde 100644
--- a/tests/components/yeelight/__init__.py
+++ b/tests/components/yeelight/__init__.py
@@ -1,5 +1,4 @@
"""Tests for the Yeelight integration."""
-import asyncio
from datetime import timedelta
from unittest.mock import AsyncMock, MagicMock, patch
@@ -164,12 +163,12 @@ def _patched_ssdp_listener(info: CaseInsensitiveDict, *args, **kwargs):
async def _async_callback(*_):
if kwargs["source"][0] == FAIL_TO_BIND_IP:
raise OSError
- await listener.async_connect_callback()
+ listener.connect_callback()
@callback
def _async_search(*_):
if info:
- asyncio.create_task(listener.async_callback(info))
+ listener.callback(info)
listener.async_start = _async_callback
listener.async_search = _async_search
diff --git a/tests/components/zamg/__init__.py b/tests/components/zamg/__init__.py
index 9c6415d7f84..33a9acaddba 100644
--- a/tests/components/zamg/__init__.py
+++ b/tests/components/zamg/__init__.py
@@ -1 +1,18 @@
"""Tests for the ZAMG component."""
+
+from homeassistant import config_entries
+from homeassistant.components.zamg.const import CONF_STATION_ID, DOMAIN as ZAMG_DOMAIN
+
+from .conftest import TEST_STATION_ID, TEST_STATION_NAME
+
+FIXTURE_CONFIG_ENTRY = {
+ "entry_id": "1",
+ "domain": ZAMG_DOMAIN,
+ "title": TEST_STATION_NAME,
+ "data": {
+ CONF_STATION_ID: TEST_STATION_ID,
+ },
+ "options": None,
+ "source": config_entries.SOURCE_USER,
+ "unique_id": TEST_STATION_ID,
+}
diff --git a/tests/components/zamg/conftest.py b/tests/components/zamg/conftest.py
index 62ef191cb48..e3d3d384e85 100644
--- a/tests/components/zamg/conftest.py
+++ b/tests/components/zamg/conftest.py
@@ -14,6 +14,9 @@ from tests.common import MockConfigEntry, load_fixture
TEST_STATION_ID = "11240"
TEST_STATION_NAME = "Graz/Flughafen"
+TEST_STATION_ID_2 = "11035"
+TEST_STATION_NAME_2 = "WIEN/HOHE WARTE"
+
@pytest.fixture
def mock_config_entry() -> MockConfigEntry:
@@ -67,6 +70,27 @@ def mock_zamg(request: pytest.FixtureRequest) -> Generator[None, MagicMock, None
yield zamg
+@pytest.fixture
+def mock_zamg_coordinator(
+ request: pytest.FixtureRequest,
+) -> Generator[None, MagicMock, None]:
+ """Return a mocked Zamg client."""
+
+ with patch(
+ "homeassistant.components.zamg.coordinator.ZamgDevice", autospec=True
+ ) as zamg_mock:
+ zamg = zamg_mock.return_value
+ zamg.update.return_value = {TEST_STATION_ID: {"Name": TEST_STATION_NAME}}
+ zamg.zamg_stations.return_value = {
+ TEST_STATION_ID: (46.99305556, 15.43916667, TEST_STATION_NAME),
+ "11244": (46.8722229, 15.90361118, "BAD GLEICHENBERG"),
+ }
+ zamg.closest_station.return_value = TEST_STATION_ID
+ zamg.get_data.return_value = TEST_STATION_ID
+ zamg.get_station_name = TEST_STATION_NAME
+ yield zamg
+
+
@pytest.fixture
def mock_zamg_stations(
request: pytest.FixtureRequest,
diff --git a/tests/components/zamg/test_config_flow.py b/tests/components/zamg/test_config_flow.py
index dc2eb62f1b9..26939c07f0c 100644
--- a/tests/components/zamg/test_config_flow.py
+++ b/tests/components/zamg/test_config_flow.py
@@ -1,6 +1,8 @@
"""Tests for the Zamg config flow."""
from unittest.mock import MagicMock
+from zamg.exceptions import ZamgApiError, ZamgStationNotFoundError
+
from homeassistant.components.zamg.const import CONF_STATION_ID, DOMAIN, LOGGER
from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER
from homeassistant.const import CONF_NAME
@@ -27,7 +29,7 @@ async def test_full_user_flow_implementation(
assert "flow_id" in result
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
- user_input={CONF_STATION_ID: int(TEST_STATION_ID)},
+ user_input={CONF_STATION_ID: TEST_STATION_ID},
)
assert result.get("type") == FlowResultType.CREATE_ENTRY
assert "data" in result
@@ -36,6 +38,21 @@ async def test_full_user_flow_implementation(
assert result["result"].unique_id == TEST_STATION_ID
+async def test_error_closest_station(
+ hass: HomeAssistant,
+ mock_zamg: MagicMock,
+ mock_setup_entry: None,
+) -> None:
+ """Test with error of reading from Zamg."""
+ mock_zamg.closest_station.side_effect = ZamgApiError
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": SOURCE_USER},
+ )
+ assert result.get("type") == FlowResultType.ABORT
+ assert result.get("reason") == "cannot_connect"
+
+
async def test_error_update(
hass: HomeAssistant,
mock_zamg: MagicMock,
@@ -50,11 +67,11 @@ async def test_error_update(
assert result.get("type") == FlowResultType.FORM
LOGGER.debug(result)
assert result.get("data_schema") != ""
- mock_zamg.update.side_effect = ValueError
+ mock_zamg.update.side_effect = ZamgApiError
assert "flow_id" in result
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
- user_input={CONF_STATION_ID: int(TEST_STATION_ID)},
+ user_input={CONF_STATION_ID: TEST_STATION_ID},
)
assert result.get("type") == FlowResultType.ABORT
assert result.get("reason") == "cannot_connect"
@@ -91,7 +108,7 @@ async def test_user_flow_duplicate(
assert "flow_id" in result
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
- user_input={CONF_STATION_ID: int(TEST_STATION_ID)},
+ user_input={CONF_STATION_ID: TEST_STATION_ID},
)
assert result.get("type") == FlowResultType.CREATE_ENTRY
assert "data" in result
@@ -107,7 +124,7 @@ async def test_user_flow_duplicate(
assert result.get("type") == FlowResultType.FORM
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
- user_input={CONF_STATION_ID: int(TEST_STATION_ID)},
+ user_input={CONF_STATION_ID: TEST_STATION_ID},
)
assert result.get("type") == FlowResultType.ABORT
assert result.get("reason") == "already_configured"
@@ -129,7 +146,7 @@ async def test_import_flow_duplicate(
assert "flow_id" in result
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
- user_input={CONF_STATION_ID: int(TEST_STATION_ID)},
+ user_input={CONF_STATION_ID: TEST_STATION_ID},
)
assert result.get("type") == FlowResultType.CREATE_ENTRY
assert "data" in result
@@ -162,7 +179,7 @@ async def test_import_flow_duplicate_after_position(
assert "flow_id" in result
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
- user_input={CONF_STATION_ID: int(TEST_STATION_ID)},
+ user_input={CONF_STATION_ID: TEST_STATION_ID},
)
assert result.get("type") == FlowResultType.CREATE_ENTRY
assert "data" in result
@@ -184,7 +201,7 @@ async def test_import_flow_no_name(
mock_zamg: MagicMock,
mock_setup_entry: None,
) -> None:
- """Test the full import flow from start to finish."""
+ """Test import flow without any name."""
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_IMPORT},
@@ -192,3 +209,35 @@ async def test_import_flow_no_name(
)
assert result.get("type") == FlowResultType.CREATE_ENTRY
assert result.get("data") == {CONF_STATION_ID: TEST_STATION_ID}
+
+
+async def test_import_flow_invalid_station(
+ hass: HomeAssistant,
+ mock_zamg: MagicMock,
+ mock_setup_entry: None,
+) -> None:
+ """Test import flow with invalid station."""
+ mock_zamg.closest_station.side_effect = ZamgStationNotFoundError
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": SOURCE_IMPORT},
+ data={CONF_STATION_ID: ""},
+ )
+ assert result.get("type") == FlowResultType.ABORT
+ assert result.get("reason") == "station_not_found"
+
+
+async def test_import_flow_zamg_error(
+ hass: HomeAssistant,
+ mock_zamg: MagicMock,
+ mock_setup_entry: None,
+) -> None:
+ """Test import flow with error on getting zamg stations."""
+ mock_zamg.zamg_stations.side_effect = ZamgApiError
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": SOURCE_IMPORT},
+ data={CONF_STATION_ID: ""},
+ )
+ assert result.get("type") == FlowResultType.ABORT
+ assert result.get("reason") == "cannot_connect"
diff --git a/tests/components/zamg/test_init.py b/tests/components/zamg/test_init.py
new file mode 100644
index 00000000000..f70d406f2be
--- /dev/null
+++ b/tests/components/zamg/test_init.py
@@ -0,0 +1,203 @@
+"""Test Zamg component init."""
+from unittest.mock import MagicMock
+
+import pytest
+
+from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
+from homeassistant.components.weather import DOMAIN as WEATHER_DOMAIN
+from homeassistant.components.zamg.const import CONF_STATION_ID, DOMAIN as ZAMG_DOMAIN
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers import entity_registry as er
+
+from . import FIXTURE_CONFIG_ENTRY
+from .conftest import (
+ TEST_STATION_ID,
+ TEST_STATION_ID_2,
+ TEST_STATION_NAME,
+ TEST_STATION_NAME_2,
+)
+
+from tests.common import MockConfigEntry
+
+
+@pytest.mark.parametrize(
+ "entitydata,old_unique_id,new_unique_id,station_id",
+ [
+ (
+ {
+ "domain": WEATHER_DOMAIN,
+ "platform": ZAMG_DOMAIN,
+ "unique_id": f"{TEST_STATION_NAME}_{TEST_STATION_ID}",
+ "suggested_object_id": f"Zamg {TEST_STATION_NAME}",
+ "disabled_by": None,
+ },
+ f"{TEST_STATION_NAME}_{TEST_STATION_ID}",
+ TEST_STATION_ID,
+ TEST_STATION_ID,
+ ),
+ (
+ {
+ "domain": WEATHER_DOMAIN,
+ "platform": ZAMG_DOMAIN,
+ "unique_id": f"{TEST_STATION_NAME_2}_{TEST_STATION_ID_2}",
+ "suggested_object_id": f"Zamg {TEST_STATION_NAME_2}",
+ "disabled_by": None,
+ },
+ f"{TEST_STATION_NAME_2}_{TEST_STATION_ID_2}",
+ TEST_STATION_ID_2,
+ TEST_STATION_ID_2,
+ ),
+ (
+ {
+ "domain": SENSOR_DOMAIN,
+ "platform": ZAMG_DOMAIN,
+ "unique_id": f"{TEST_STATION_NAME_2}_{TEST_STATION_ID_2}_temperature",
+ "suggested_object_id": f"Zamg {TEST_STATION_NAME_2}",
+ "disabled_by": None,
+ },
+ f"{TEST_STATION_NAME_2}_{TEST_STATION_ID_2}_temperature",
+ f"{TEST_STATION_NAME_2}_{TEST_STATION_ID_2}_temperature",
+ TEST_STATION_ID_2,
+ ),
+ ],
+)
+async def test_migrate_unique_ids(
+ hass: HomeAssistant,
+ mock_zamg_coordinator: MagicMock,
+ entitydata: dict,
+ old_unique_id: str,
+ new_unique_id: str,
+ station_id: str,
+) -> None:
+ """Test successful migration of entity unique_ids."""
+ FIXTURE_CONFIG_ENTRY["data"][CONF_STATION_ID] = station_id
+ mock_config_entry = MockConfigEntry(**FIXTURE_CONFIG_ENTRY)
+ mock_config_entry.add_to_hass(hass)
+
+ entity_registry = er.async_get(hass)
+ entity: er.RegistryEntry = entity_registry.async_get_or_create(
+ **entitydata,
+ config_entry=mock_config_entry,
+ )
+
+ assert entity.unique_id == old_unique_id
+
+ assert await hass.config_entries.async_setup(mock_config_entry.entry_id)
+ await hass.async_block_till_done()
+
+ entity_migrated = entity_registry.async_get(entity.entity_id)
+ assert entity_migrated
+ assert entity_migrated.unique_id == new_unique_id
+
+
+@pytest.mark.parametrize(
+ "entitydata,old_unique_id,new_unique_id,station_id",
+ [
+ (
+ {
+ "domain": WEATHER_DOMAIN,
+ "platform": ZAMG_DOMAIN,
+ "unique_id": f"{TEST_STATION_NAME}_{TEST_STATION_ID}",
+ "suggested_object_id": f"Zamg {TEST_STATION_NAME}",
+ "disabled_by": None,
+ },
+ f"{TEST_STATION_NAME}_{TEST_STATION_ID}",
+ TEST_STATION_ID,
+ TEST_STATION_ID,
+ ),
+ ],
+)
+async def test_dont_migrate_unique_ids(
+ hass: HomeAssistant,
+ mock_zamg_coordinator: MagicMock,
+ entitydata: dict,
+ old_unique_id: str,
+ new_unique_id: str,
+ station_id: str,
+) -> None:
+ """Test successful migration of entity unique_ids."""
+ FIXTURE_CONFIG_ENTRY["data"][CONF_STATION_ID] = station_id
+ mock_config_entry = MockConfigEntry(**FIXTURE_CONFIG_ENTRY)
+ mock_config_entry.add_to_hass(hass)
+
+ entity_registry = er.async_get(hass)
+
+ # create existing entry with new_unique_id
+ existing_entity = entity_registry.async_get_or_create(
+ WEATHER_DOMAIN,
+ ZAMG_DOMAIN,
+ unique_id=TEST_STATION_ID,
+ suggested_object_id=f"Zamg {TEST_STATION_NAME}",
+ config_entry=mock_config_entry,
+ )
+
+ entity: er.RegistryEntry = entity_registry.async_get_or_create(
+ **entitydata,
+ config_entry=mock_config_entry,
+ )
+
+ assert entity.unique_id == old_unique_id
+
+ assert await hass.config_entries.async_setup(mock_config_entry.entry_id)
+ await hass.async_block_till_done()
+
+ entity_migrated = entity_registry.async_get(entity.entity_id)
+ assert entity_migrated
+ assert entity_migrated.unique_id == old_unique_id
+
+ entity_not_changed = entity_registry.async_get(existing_entity.entity_id)
+ assert entity_not_changed
+ assert entity_not_changed.unique_id == new_unique_id
+
+ assert entity_migrated != entity_not_changed
+
+
+@pytest.mark.parametrize(
+ "entitydata,unique_id",
+ [
+ (
+ {
+ "domain": WEATHER_DOMAIN,
+ "platform": ZAMG_DOMAIN,
+ "unique_id": TEST_STATION_ID,
+ "suggested_object_id": f"Zamg {TEST_STATION_NAME}",
+ "disabled_by": None,
+ },
+ TEST_STATION_ID,
+ ),
+ ],
+)
+async def test_unload_entry(
+ hass: HomeAssistant,
+ mock_zamg_coordinator: MagicMock,
+ entitydata: dict,
+ unique_id: str,
+) -> None:
+ """Test unload entity unique_ids."""
+ mock_config_entry = MockConfigEntry(**FIXTURE_CONFIG_ENTRY)
+ mock_config_entry.add_to_hass(hass)
+
+ entity_registry = er.async_get(hass)
+
+ entity_registry.async_get_or_create(
+ WEATHER_DOMAIN,
+ ZAMG_DOMAIN,
+ unique_id=TEST_STATION_ID,
+ suggested_object_id=f"Zamg {TEST_STATION_NAME}",
+ config_entry=mock_config_entry,
+ )
+
+ entity: er.RegistryEntry = entity_registry.async_get_or_create(
+ **entitydata,
+ config_entry=mock_config_entry,
+ )
+
+ assert entity.unique_id == unique_id
+
+ assert await hass.config_entries.async_setup(mock_config_entry.entry_id)
+ await hass.async_block_till_done()
+
+ assert await hass.config_entries.async_remove(mock_config_entry.entry_id)
+ await hass.async_block_till_done()
+
+ assert hass.config_entries.async_get_entry(unique_id) is None
diff --git a/tests/components/zha/test_device_trigger.py b/tests/components/zha/test_device_trigger.py
index 49eeacc0e42..0081ba04d16 100644
--- a/tests/components/zha/test_device_trigger.py
+++ b/tests/components/zha/test_device_trigger.py
@@ -327,7 +327,7 @@ async def test_exception_no_triggers(hass, mock_devices, calls, caplog):
},
)
await hass.async_block_till_done()
- assert "Invalid config for [automation]" in caplog.text
+ assert "Invalid trigger configuration" in caplog.text
async def test_exception_bad_trigger(hass, mock_devices, calls, caplog):
@@ -369,7 +369,7 @@ async def test_exception_bad_trigger(hass, mock_devices, calls, caplog):
},
)
await hass.async_block_till_done()
- assert "Invalid config for [automation]" in caplog.text
+ assert "Invalid trigger configuration" in caplog.text
@pytest.mark.skip(reason="Temporarily disabled until automation validation is improved")
@@ -408,4 +408,4 @@ async def test_exception_no_device(hass, mock_devices, calls, caplog):
},
)
await hass.async_block_till_done()
- assert "Invalid config for [automation]" in caplog.text
+ assert "Invalid trigger configuration" in caplog.text
diff --git a/tests/components/zha/test_sensor.py b/tests/components/zha/test_sensor.py
index 55ea9833caa..dec065936a1 100644
--- a/tests/components/zha/test_sensor.py
+++ b/tests/components/zha/test_sensor.py
@@ -129,7 +129,7 @@ async def async_test_pressure(hass, cluster, entity_id):
async def async_test_illuminance(hass, cluster, entity_id):
"""Test illuminance sensor."""
await send_attributes_report(hass, cluster, {1: 1, 0: 10, 2: 20})
- assert_state(hass, entity_id, "1.0", LIGHT_LUX)
+ assert_state(hass, entity_id, "1", LIGHT_LUX)
async def async_test_metering(hass, cluster, entity_id):
diff --git a/tests/components/zodiac/test_sensor.py b/tests/components/zodiac/test_sensor.py
index 90b19e73b04..b0894dfde7d 100644
--- a/tests/components/zodiac/test_sensor.py
+++ b/tests/components/zodiac/test_sensor.py
@@ -4,6 +4,7 @@ from unittest.mock import patch
import pytest
+from homeassistant.components.sensor import ATTR_OPTIONS, SensorDeviceClass
from homeassistant.components.zodiac.const import (
ATTR_ELEMENT,
ATTR_MODALITY,
@@ -17,6 +18,8 @@ from homeassistant.components.zodiac.const import (
SIGN_SCORPIO,
SIGN_TAURUS,
)
+from homeassistant.const import ATTR_DEVICE_CLASS
+from homeassistant.helpers import entity_registry as er
from homeassistant.setup import async_setup_component
import homeassistant.util.dt as dt_util
@@ -48,3 +51,24 @@ async def test_zodiac_day(hass, now, sign, element, modality):
assert state.attributes
assert state.attributes[ATTR_ELEMENT] == element
assert state.attributes[ATTR_MODALITY] == modality
+ assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.ENUM
+ assert state.attributes[ATTR_OPTIONS] == [
+ "aquarius",
+ "aries",
+ "cancer",
+ "capricorn",
+ "gemini",
+ "leo",
+ "libra",
+ "pisces",
+ "sagittarius",
+ "scorpio",
+ "taurus",
+ "virgo",
+ ]
+
+ entity_registry = er.async_get(hass)
+ entry = entity_registry.async_get("sensor.zodiac")
+ assert entry
+ assert entry.unique_id == "zodiac"
+ assert entry.translation_key == "sign"
diff --git a/tests/components/zwave_js/test_api.py b/tests/components/zwave_js/test_api.py
index caea283e25c..4ea0669f0cb 100644
--- a/tests/components/zwave_js/test_api.py
+++ b/tests/components/zwave_js/test_api.py
@@ -87,6 +87,26 @@ def get_device(hass, node):
return dev_reg.async_get_device({device_id})
+async def test_no_driver(
+ hass, client, multisensor_6, controller_state, integration, hass_ws_client
+):
+ """Test driver missing results in error."""
+ entry = integration
+ ws_client = await hass_ws_client(hass)
+ client.driver = None
+
+ # Try API call with entry ID
+ await ws_client.send_json(
+ {
+ ID: 1,
+ TYPE: "zwave_js/network_status",
+ ENTRY_ID: entry.entry_id,
+ }
+ )
+ msg = await ws_client.receive_json()
+ assert not msg["success"]
+
+
async def test_network_status(
hass, multisensor_6, controller_state, integration, hass_ws_client
):
diff --git a/tests/components/zwave_js/test_cover.py b/tests/components/zwave_js/test_cover.py
index 0ca2e36d853..d2c8fc32a73 100644
--- a/tests/components/zwave_js/test_cover.py
+++ b/tests/components/zwave_js/test_cover.py
@@ -267,6 +267,31 @@ async def test_fibaro_FGR222_shutter_cover(
}
assert args["value"] == 0
+ # Test some tilt
+ event = Event(
+ type="value updated",
+ data={
+ "source": "node",
+ "event": "value updated",
+ "nodeId": 42,
+ "args": {
+ "commandClassName": "Manufacturer Proprietary",
+ "commandClass": 145,
+ "endpoint": 0,
+ "property": "fibaro",
+ "propertyKey": "venetianBlindsTilt",
+ "newValue": 99,
+ "prevValue": 0,
+ "propertyName": "fibaro",
+ "propertyKeyName": "venetianBlindsTilt",
+ },
+ },
+ )
+ fibaro_fgr222_shutter.receive_event(event)
+ state = hass.states.get(FIBARO_SHUTTER_COVER_ENTITY)
+ assert state
+ assert state.attributes[ATTR_CURRENT_TILT_POSITION] == 100
+
async def test_aeotec_nano_shutter_cover(
hass, client, aeotec_nano_shutter, integration
diff --git a/tests/components/zwave_js/test_device_trigger.py b/tests/components/zwave_js/test_device_trigger.py
index e9bc319fe4d..336923d83f7 100644
--- a/tests/components/zwave_js/test_device_trigger.py
+++ b/tests/components/zwave_js/test_device_trigger.py
@@ -788,7 +788,6 @@ async def test_if_central_scene_value_notification_fires(
"property": "scene",
"propertyName": "scene",
"propertyKey": "001",
- "propertyKey": "001",
"value": 0,
"metadata": {
"type": "number",
diff --git a/tests/components/zwave_js/test_number.py b/tests/components/zwave_js/test_number.py
index e36bd081b18..ff551ce554b 100644
--- a/tests/components/zwave_js/test_number.py
+++ b/tests/components/zwave_js/test_number.py
@@ -1,11 +1,17 @@
"""Test the Z-Wave JS number platform."""
+from unittest.mock import patch
+
+import pytest
from zwave_js_server.event import Event
from homeassistant.const import STATE_UNKNOWN
+from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_registry as er
from .common import BASIC_NUMBER_ENTITY
+from tests.common import MockConfigEntry
+
NUMBER_ENTITY = "number.thermostat_hvac_valve_control"
VOLUME_NUMBER_ENTITY = "number.indoor_siren_6_default_volume_2"
@@ -63,6 +69,66 @@ async def test_number(hass, client, aeotec_radiator_thermostat, integration):
assert state.state == "99.0"
+@pytest.fixture(name="no_target_value")
+def mock_client_fixture():
+ """Mock no target_value."""
+
+ with patch(
+ "homeassistant.components.zwave_js.number.ZwaveNumberEntity.get_zwave_value",
+ return_value=None,
+ ):
+ yield
+
+
+async def test_number_no_target_value(
+ hass, client, no_target_value, aeotec_radiator_thermostat, integration
+):
+ """Test the number entity with no target value."""
+ # Test turn on setting value fails
+ with pytest.raises(HomeAssistantError):
+ await hass.services.async_call(
+ "number",
+ "set_value",
+ {"entity_id": NUMBER_ENTITY, "value": 30},
+ blocking=True,
+ )
+
+
+async def test_number_writeable(hass, client, aeotec_radiator_thermostat):
+ """Test the number entity where current value is writeable."""
+ aeotec_radiator_thermostat.values["4-38-0-currentValue"].metadata.data[
+ "writeable"
+ ] = True
+ aeotec_radiator_thermostat.values.pop("4-38-0-targetValue")
+
+ # set up config entry
+ entry = MockConfigEntry(domain="zwave_js", data={"url": "ws://test.org"})
+ entry.add_to_hass(hass)
+ await hass.config_entries.async_setup(entry.entry_id)
+ await hass.async_block_till_done()
+
+ # Test turn on setting value
+ await hass.services.async_call(
+ "number",
+ "set_value",
+ {"entity_id": NUMBER_ENTITY, "value": 30},
+ blocking=True,
+ )
+
+ assert len(client.async_send_command.call_args_list) == 1
+ args = client.async_send_command.call_args[0][0]
+ assert args["command"] == "node.set_value"
+ assert args["nodeId"] == 4
+ assert args["valueId"] == {
+ "commandClass": 38,
+ "endpoint": 0,
+ "property": "currentValue",
+ }
+ assert args["value"] == 30.0
+
+ client.async_send_command.reset_mock()
+
+
async def test_volume_number(hass, client, aeotec_zw164_siren, integration):
"""Test the volume number entity."""
node = aeotec_zw164_siren
diff --git a/tests/components/zwave_js/test_sensor.py b/tests/components/zwave_js/test_sensor.py
index a32537b1d0d..59ca814a197 100644
--- a/tests/components/zwave_js/test_sensor.py
+++ b/tests/components/zwave_js/test_sensor.py
@@ -24,12 +24,13 @@ from homeassistant.const import (
ATTR_ENTITY_ID,
ATTR_ICON,
ATTR_UNIT_OF_MEASUREMENT,
- ELECTRIC_CURRENT_AMPERE,
- ELECTRIC_POTENTIAL_VOLT,
- ENERGY_KILO_WATT_HOUR,
- POWER_WATT,
+ PERCENTAGE,
STATE_UNAVAILABLE,
- TEMP_CELSIUS,
+ UnitOfElectricCurrent,
+ UnitOfElectricPotential,
+ UnitOfEnergy,
+ UnitOfPower,
+ UnitOfTemperature,
)
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.entity import EntityCategory
@@ -49,21 +50,25 @@ from .common import (
)
-async def test_numeric_sensor(hass, multisensor_6, integration):
+async def test_numeric_sensor(
+ hass, multisensor_6, express_controls_ezmultipli, integration
+):
"""Test the numeric sensor."""
state = hass.states.get(AIR_TEMPERATURE_SENSOR)
assert state
assert state.state == "9.0"
- assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == TEMP_CELSIUS
+ assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfTemperature.CELSIUS
assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.TEMPERATURE
+ assert state.attributes[ATTR_STATE_CLASS] == SensorStateClass.MEASUREMENT
state = hass.states.get(BATTERY_SENSOR)
assert state
assert state.state == "100.0"
- assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == "%"
+ assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == PERCENTAGE
assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.BATTERY
+ assert state.attributes[ATTR_STATE_CLASS] == SensorStateClass.MEASUREMENT
ent_reg = er.async_get(hass)
entity_entry = ent_reg.async_get(BATTERY_SENSOR)
@@ -74,8 +79,27 @@ async def test_numeric_sensor(hass, multisensor_6, integration):
assert state
assert state.state == "65.0"
- assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == "%"
+ assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == PERCENTAGE
assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.HUMIDITY
+ assert state.attributes[ATTR_STATE_CLASS] == SensorStateClass.MEASUREMENT
+
+ state = hass.states.get("sensor.multisensor_6_ultraviolet")
+
+ assert state
+ assert state.state == "0.0"
+ # TODO: Add UV_INDEX unit of measurement to this sensor
+ assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes
+ assert ATTR_DEVICE_CLASS not in state.attributes
+ # TODO: Add measurement state class to this sensor
+ assert ATTR_STATE_CLASS not in state.attributes
+
+ state = hass.states.get("sensor.hsm200_illuminance")
+
+ assert state
+ assert state.state == "61.0"
+ assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == PERCENTAGE
+ assert ATTR_DEVICE_CLASS not in state.attributes
+ assert state.attributes[ATTR_STATE_CLASS] == SensorStateClass.MEASUREMENT
async def test_energy_sensors(hass, hank_binary_switch, integration):
@@ -84,7 +108,7 @@ async def test_energy_sensors(hass, hank_binary_switch, integration):
assert state
assert state.state == "0.0"
- assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == POWER_WATT
+ assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfPower.WATT
assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.POWER
assert state.attributes[ATTR_STATE_CLASS] is SensorStateClass.MEASUREMENT
@@ -92,7 +116,7 @@ async def test_energy_sensors(hass, hank_binary_switch, integration):
assert state
assert state.state == "0.16"
- assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == ENERGY_KILO_WATT_HOUR
+ assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfEnergy.KILO_WATT_HOUR
assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.ENERGY
assert state.attributes[ATTR_STATE_CLASS] is SensorStateClass.TOTAL_INCREASING
@@ -100,14 +124,14 @@ async def test_energy_sensors(hass, hank_binary_switch, integration):
assert state
assert state.state == "122.96"
- assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == ELECTRIC_POTENTIAL_VOLT
+ assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfElectricPotential.VOLT
assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.VOLTAGE
state = hass.states.get(CURRENT_SENSOR)
assert state
assert state.state == "0.0"
- assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == ELECTRIC_CURRENT_AMPERE
+ assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfElectricCurrent.AMPERE
assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.CURRENT
@@ -401,7 +425,8 @@ async def test_unit_change(hass, zp3111, client, integration):
state = hass.states.get(entity_id)
assert state
assert state.state == "21.98"
- assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == TEMP_CELSIUS
+ assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfTemperature.CELSIUS
+ assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.TEMPERATURE
event = Event(
"metadata updated",
{
@@ -431,7 +456,8 @@ async def test_unit_change(hass, zp3111, client, integration):
state = hass.states.get(entity_id)
assert state
assert state.state == "21.98"
- assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == TEMP_CELSIUS
+ assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfTemperature.CELSIUS
+ assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.TEMPERATURE
event = Event(
"value updated",
{
@@ -454,4 +480,5 @@ async def test_unit_change(hass, zp3111, client, integration):
state = hass.states.get(entity_id)
assert state
assert state.state == "100.0"
- assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == TEMP_CELSIUS
+ assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfTemperature.CELSIUS
+ assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.TEMPERATURE
diff --git a/tests/conftest.py b/tests/conftest.py
index 4dc988db8fc..072ce9e112b 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -7,14 +7,13 @@ from contextlib import asynccontextmanager
import functools
import gc
import itertools
-from json import JSONDecoder, loads
+from json import JSONDecoder
import logging
import sqlite3
import ssl
import threading
from typing import Any
from unittest.mock import AsyncMock, MagicMock, Mock, patch
-import warnings
from aiohttp import client
from aiohttp.pytest_plugin import AiohttpClient
@@ -46,6 +45,7 @@ from homeassistant.components.websocket_api.http import URL
from homeassistant.const import HASSIO_USER_NAME
from homeassistant.core import CoreState, HomeAssistant
from homeassistant.helpers import config_entry_oauth2_flow, recorder as recorder_helper
+from homeassistant.helpers.json import json_loads
from homeassistant.helpers.typing import ConfigType
from homeassistant.setup import async_setup_component
from homeassistant.util import dt as dt_util, location
@@ -212,14 +212,14 @@ def verify_cleanup(event_loop: asyncio.AbstractEventLoop):
# before moving on to the next test.
tasks = asyncio.all_tasks(event_loop) - tasks_before
for task in tasks:
- warnings.warn(f"Linger task after test {task}")
+ _LOGGER.warning("Linger task after test %r", task)
task.cancel()
if tasks:
event_loop.run_until_complete(asyncio.wait(tasks))
for handle in event_loop._scheduled: # pylint: disable=protected-access
if not handle.cancelled():
- warnings.warn(f"Lingering timer after test {handle}")
+ _LOGGER.warning("Lingering timer after test %r", handle)
handle.cancel()
# Make sure garbage collect run in same test as allocation
@@ -272,7 +272,7 @@ class CoalescingResponse(client.ClientWebSocketResponse):
async def receive_json(
self,
*,
- loads: JSONDecoder = loads,
+ loads: JSONDecoder = json_loads,
timeout: float | None = None,
) -> Any:
"""receive_json or from buffer."""
@@ -1093,6 +1093,9 @@ async def mock_enable_bluetooth(
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
+ yield
+ await hass.config_entries.async_unload(entry.entry_id)
+ await hass.async_block_till_done()
@pytest.fixture(name="mock_bluetooth_adapters")
diff --git a/tests/fixtures/homematicip_cloud.json b/tests/fixtures/homematicip_cloud.json
index 3e3536a2f42..c54327069a2 100644
--- a/tests/fixtures/homematicip_cloud.json
+++ b/tests/fixtures/homematicip_cloud.json
@@ -6977,6 +6977,148 @@
"permanentlyReachable": true,
"supported": true,
"type": "EXTERNAL"
+ },
+ "3014F711A000DIN_RAIL_DIMMER3": {
+ "availableFirmwareVersion": "1.2.0",
+ "connectionType": "HMIP_RF",
+ "firmwareVersion": "1.2.0",
+ "firmwareVersionInteger": 66048,
+ "functionalChannels": {
+ "0": {
+ "busConfigMismatch": null,
+ "coProFaulty": false,
+ "coProRestartNeeded": false,
+ "coProUpdateFailure": false,
+ "configPending": false,
+ "deviceId": "3014F711A000DIN_RAIL_DIMMER3",
+ "deviceOverheated": false,
+ "deviceOverloaded": false,
+ "devicePowerFailureDetected": false,
+ "deviceUndervoltage": false,
+ "dutyCycle": false,
+ "functionalChannelType": "DEVICE_BASE",
+ "groupIndex": 0,
+ "groups": ["49bf77e1-a07b-4a3c-8151-e0a7aca331bb"],
+ "index": 0,
+ "label": "",
+ "lowBat": null,
+ "mountingOrientation": null,
+ "multicastRoutingEnabled": false,
+ "particulateMatterSensorCommunicationError": null,
+ "particulateMatterSensorError": null,
+ "powerShortCircuit": null,
+ "profilePeriodLimitReached": null,
+ "routerModuleEnabled": false,
+ "routerModuleSupported": false,
+ "rssiDeviceValue": -64,
+ "rssiPeerValue": -66,
+ "shortCircuitDataLine": null,
+ "supportedOptionalFeatures": {
+ "IFeatureBusConfigMismatch": false,
+ "IFeatureDeviceCoProError": false,
+ "IFeatureDeviceCoProRestart": false,
+ "IFeatureDeviceCoProUpdate": false,
+ "IFeatureDeviceIdentify": true,
+ "IFeatureDeviceOverheated": true,
+ "IFeatureDeviceOverloaded": false,
+ "IFeatureDeviceParticulateMatterSensorCommunicationError": false,
+ "IFeatureDeviceParticulateMatterSensorError": false,
+ "IFeatureDevicePowerFailure": true,
+ "IFeatureDeviceTemperatureHumiditySensorCommunicationError": false,
+ "IFeatureDeviceTemperatureHumiditySensorError": false,
+ "IFeatureDeviceTemperatureOutOfRange": false,
+ "IFeatureDeviceUndervoltage": false,
+ "IFeatureMulticastRouter": false,
+ "IFeaturePowerShortCircuit": false,
+ "IFeatureProfilePeriodLimit": true,
+ "IFeatureRssiValue": true,
+ "IFeatureShortCircuitDataLine": false,
+ "IOptionalFeatureDutyCycle": true,
+ "IOptionalFeatureLowBat": false,
+ "IOptionalFeatureMountingOrientation": false
+ },
+ "temperatureHumiditySensorCommunicationError": null,
+ "temperatureHumiditySensorError": null,
+ "temperatureOutOfRange": false,
+ "unreach": false
+ },
+ "1": {
+ "binaryBehaviorType": "NORMALLY_CLOSE",
+ "coProFaulty": true,
+ "coProRestartNeeded": false,
+ "deviceId": "3014F711A000DIN_RAIL_DIMMER3",
+ "deviceOverheated": false,
+ "deviceOverloaded": false,
+ "dimLevel": 0.1,
+ "functionalChannelType": "MULTI_MODE_INPUT_DIMMER_CHANNEL",
+ "groupIndex": 1,
+ "groups": [],
+ "index": 1,
+ "label": "",
+ "multiModeInputMode": "KEY_BEHAVIOR",
+ "on": true,
+ "profileMode": "AUTOMATIC",
+ "supportedOptionalFeatures": {
+ "IFeatureDeviceOverloaded": true
+ },
+ "userDesiredProfileMode": "AUTOMATIC"
+ },
+ "2": {
+ "binaryBehaviorType": "NORMALLY_CLOSE",
+ "coProFaulty": true,
+ "coProRestartNeeded": false,
+ "deviceId": "3014F711A000DIN_RAIL_DIMMER3",
+ "deviceOverheated": false,
+ "deviceOverloaded": false,
+ "dimLevel": 0.2,
+ "functionalChannelType": "MULTI_MODE_INPUT_DIMMER_CHANNEL",
+ "groupIndex": 2,
+ "groups": [],
+ "index": 2,
+ "label": "",
+ "multiModeInputMode": "KEY_BEHAVIOR",
+ "on": true,
+ "profileMode": "AUTOMATIC",
+ "supportedOptionalFeatures": {
+ "IFeatureDeviceOverloaded": true
+ },
+ "userDesiredProfileMode": "AUTOMATIC"
+ },
+ "3": {
+ "binaryBehaviorType": "NORMALLY_CLOSE",
+ "coProFaulty": false,
+ "coProRestartNeeded": false,
+ "deviceId": "3014F711A000DIN_RAIL_DIMMER3",
+ "deviceOverheated": false,
+ "deviceOverloaded": false,
+ "dimLevel": 0.3,
+ "functionalChannelType": "MULTI_MODE_INPUT_DIMMER_CHANNEL",
+ "groupIndex": 3,
+ "groups": [],
+ "index": 3,
+ "label": "Esstisch",
+ "multiModeInputMode": "KEY_BEHAVIOR",
+ "on": true,
+ "profileMode": "AUTOMATIC",
+ "supportedOptionalFeatures": {
+ "IFeatureDeviceOverloaded": true
+ },
+ "userDesiredProfileMode": "AUTOMATIC"
+ }
+ },
+ "homeId": "00000000-0000-0000-0000-000000000001",
+ "id": "3014F711A000DIN_RAIL_DIMMER3",
+ "label": "3-Dimmer",
+ "lastStatusUpdate": 1618223872902,
+ "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED",
+ "manufacturerCode": 1,
+ "modelId": 407,
+ "modelType": "HmIP-DRDI3",
+ "oem": "eQ-3",
+ "permanentlyReachable": true,
+ "serializedGlobalTradeItemNumber": "3014F711A000DIN_RAIL_DIMMER3",
+ "type": "DIN_RAIL_DIMMER_3",
+ "updateState": "UP_TO_DATE"
}
},
"groups": {
diff --git a/tests/hassfest/test_requirements.py b/tests/hassfest/test_requirements.py
index a529c0769d6..fc366db74fa 100644
--- a/tests/hassfest/test_requirements.py
+++ b/tests/hassfest/test_requirements.py
@@ -12,7 +12,7 @@ def integration():
"""Fixture for hassfest integration model."""
integration = Integration(
path=Path("homeassistant/components/test"),
- manifest={
+ _manifest={
"domain": "test",
"documentation": "https://example.com",
"name": "test",
diff --git a/tests/helpers/test_area_registry.py b/tests/helpers/test_area_registry.py
index 7dca029987e..ea42417aef9 100644
--- a/tests/helpers/test_area_registry.py
+++ b/tests/helpers/test_area_registry.py
@@ -4,7 +4,7 @@ import pytest
from homeassistant.core import callback
from homeassistant.helpers import area_registry
-from tests.common import flush_store, mock_area_registry
+from tests.common import ANY, flush_store, mock_area_registry
@pytest.fixture
@@ -38,17 +38,39 @@ async def test_list_areas(registry):
async def test_create_area(hass, registry, update_events):
"""Make sure that we can create an area."""
+ # Create area with only mandatory parameters
area = registry.async_create("mock")
- assert area.id == "mock"
- assert area.name == "mock"
+ assert area == area_registry.AreaEntry(
+ name="mock", normalized_name=ANY, aliases=set(), id=ANY, picture=None
+ )
assert len(registry.areas) == 1
await hass.async_block_till_done()
assert len(update_events) == 1
- assert update_events[0]["action"] == "create"
- assert update_events[0]["area_id"] == area.id
+ assert update_events[-1]["action"] == "create"
+ assert update_events[-1]["area_id"] == area.id
+
+ # Create area with all parameters
+ area = registry.async_create(
+ "mock 2", aliases={"alias_1", "alias_2"}, picture="/image/example.png"
+ )
+
+ assert area == area_registry.AreaEntry(
+ name="mock 2",
+ normalized_name=ANY,
+ aliases={"alias_1", "alias_2"},
+ id=ANY,
+ picture="/image/example.png",
+ )
+ assert len(registry.areas) == 2
+
+ await hass.async_block_till_done()
+
+ assert len(update_events) == 2
+ assert update_events[-1]["action"] == "create"
+ assert update_events[-1]["area_id"] == area.id
async def test_create_area_with_name_already_in_use(hass, registry, update_events):
@@ -70,7 +92,7 @@ async def test_create_area_with_id_already_in_use(registry):
"""Make sure that we can't create an area with a name already in use."""
area1 = registry.async_create("mock")
- updated_area1 = registry.async_update(area1.id, "New Name")
+ updated_area1 = registry.async_update(area1.id, name="New Name")
assert updated_area1.id == area1.id
area2 = registry.async_create("mock")
@@ -108,10 +130,21 @@ async def test_update_area(hass, registry, update_events):
"""Make sure that we can read areas."""
area = registry.async_create("mock")
- updated_area = registry.async_update(area.id, name="mock1")
+ updated_area = registry.async_update(
+ area.id,
+ aliases={"alias_1", "alias_2"},
+ name="mock1",
+ picture="/image/example.png",
+ )
assert updated_area != area
- assert updated_area.name == "mock1"
+ assert updated_area == area_registry.AreaEntry(
+ name="mock1",
+ normalized_name=ANY,
+ aliases={"alias_1", "alias_2"},
+ id=ANY,
+ picture="/image/example.png",
+ )
assert len(registry.areas) == 1
await hass.async_block_till_done()
@@ -196,8 +229,18 @@ async def test_load_area(hass, registry):
async def test_loading_area_from_storage(hass, hass_storage):
"""Test loading stored areas on start."""
hass_storage[area_registry.STORAGE_KEY] = {
- "version": area_registry.STORAGE_VERSION,
- "data": {"areas": [{"id": "12345A", "name": "mock"}]},
+ "version": area_registry.STORAGE_VERSION_MAJOR,
+ "minor_version": area_registry.STORAGE_VERSION_MINOR,
+ "data": {
+ "areas": [
+ {
+ "aliases": ["alias_1", "alias_2"],
+ "id": "12345A",
+ "name": "mock",
+ "picture": "blah",
+ }
+ ]
+ },
}
await area_registry.async_load(hass)
@@ -206,6 +249,33 @@ async def test_loading_area_from_storage(hass, hass_storage):
assert len(registry.areas) == 1
+@pytest.mark.parametrize("load_registries", [False])
+async def test_migration_from_1_1(hass, hass_storage):
+ """Test migration from version 1.1."""
+ hass_storage[area_registry.STORAGE_KEY] = {
+ "version": 1,
+ "data": {"areas": [{"id": "12345A", "name": "mock"}]},
+ }
+
+ await area_registry.async_load(hass)
+ registry = area_registry.async_get(hass)
+
+ # Test data was loaded
+ entry = registry.async_get_or_create("mock")
+ assert entry.id == "12345A"
+
+ # Check we store migrated data
+ await flush_store(registry._store)
+ assert hass_storage[area_registry.STORAGE_KEY] == {
+ "version": area_registry.STORAGE_VERSION_MAJOR,
+ "minor_version": area_registry.STORAGE_VERSION_MINOR,
+ "key": area_registry.STORAGE_KEY,
+ "data": {
+ "areas": [{"aliases": [], "id": "12345A", "name": "mock", "picture": None}]
+ },
+ }
+
+
async def test_async_get_or_create(hass, registry):
"""Make sure we can get the area by name."""
area = registry.async_get_or_create("Mock1")
diff --git a/tests/helpers/test_entity.py b/tests/helpers/test_entity.py
index 698d3cfe98a..d359efd6325 100644
--- a/tests/helpers/test_entity.py
+++ b/tests/helpers/test_entity.py
@@ -934,3 +934,23 @@ async def test_friendly_name(
assert len(hass.states.async_entity_ids()) == 1
state = hass.states.async_all()[0]
assert state.attributes.get(ATTR_FRIENDLY_NAME) == expected_friendly_name
+
+
+async def test_translation_key(hass):
+ """Test translation key property."""
+ mock_entity1 = entity.Entity()
+ mock_entity1.hass = hass
+ mock_entity1.entity_description = entity.EntityDescription(
+ key="abc", translation_key="from_entity_description"
+ )
+ mock_entity1.entity_id = "hello.world"
+ mock_entity1._attr_translation_key = "from_attr"
+ assert mock_entity1.translation_key == "from_attr"
+
+ mock_entity2 = entity.Entity()
+ mock_entity2.hass = hass
+ mock_entity2.entity_description = entity.EntityDescription(
+ key="abc", translation_key="from_entity_description"
+ )
+ mock_entity2.entity_id = "hello.world"
+ assert mock_entity2.translation_key == "from_entity_description"
diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py
index b2af85ca631..c7721f7f78c 100644
--- a/tests/helpers/test_entity_platform.py
+++ b/tests/helpers/test_entity_platform.py
@@ -1227,6 +1227,7 @@ async def test_entity_info_added_to_entity_registry(hass):
icon="nice:icon",
name="best name",
supported_features=5,
+ translation_key="my_translation_key",
unique_id="default",
unit_of_measurement=PERCENTAGE,
)
@@ -1251,6 +1252,7 @@ async def test_entity_info_added_to_entity_registry(hass):
original_icon="nice:icon",
original_name="best name",
supported_features=5,
+ translation_key="my_translation_key",
unit_of_measurement=PERCENTAGE,
)
diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py
index 5538950260c..b8e66255c48 100644
--- a/tests/helpers/test_entity_registry.py
+++ b/tests/helpers/test_entity_registry.py
@@ -84,6 +84,7 @@ def test_get_or_create_updates_data(registry):
original_icon="initial-original_icon",
original_name="initial-original_name",
supported_features=5,
+ translation_key="initial-translation_key",
unit_of_measurement="initial-unit_of_measurement",
)
@@ -106,6 +107,7 @@ def test_get_or_create_updates_data(registry):
original_icon="initial-original_icon",
original_name="initial-original_name",
supported_features=5,
+ translation_key="initial-translation_key",
unit_of_measurement="initial-unit_of_measurement",
)
@@ -126,6 +128,7 @@ def test_get_or_create_updates_data(registry):
original_icon="updated-original_icon",
original_name="updated-original_name",
supported_features=10,
+ translation_key="updated-translation_key",
unit_of_measurement="updated-unit_of_measurement",
)
@@ -133,6 +136,7 @@ def test_get_or_create_updates_data(registry):
"light.hue_5678",
"5678",
"hue",
+ aliases=set(),
area_id=None,
capabilities={"new-max": 150},
config_entry_id=new_config_entry.entry_id,
@@ -149,6 +153,7 @@ def test_get_or_create_updates_data(registry):
original_icon="updated-original_icon",
original_name="updated-original_name",
supported_features=10,
+ translation_key="updated-translation_key",
unit_of_measurement="updated-unit_of_measurement",
)
@@ -167,6 +172,7 @@ def test_get_or_create_updates_data(registry):
original_icon=None,
original_name=None,
supported_features=None,
+ translation_key=None,
unit_of_measurement=None,
)
@@ -174,6 +180,7 @@ def test_get_or_create_updates_data(registry):
"light.hue_5678",
"5678",
"hue",
+ aliases=set(),
area_id=None,
capabilities=None,
config_entry_id=None,
@@ -190,6 +197,7 @@ def test_get_or_create_updates_data(registry):
original_icon=None,
original_name=None,
supported_features=0, # supported_features is stored as an int
+ translation_key=None,
unit_of_measurement=None,
)
@@ -242,10 +250,12 @@ async def test_loading_saving_data(hass, registry):
original_icon="hass:original-icon",
original_name="Original Name",
supported_features=5,
+ translation_key="initial-translation_key",
unit_of_measurement="initial-unit_of_measurement",
)
registry.async_update_entity(
orig_entry2.entity_id,
+ aliases={"initial_alias_1", "initial_alias_2"},
area_id="mock-area-id",
device_class="user-class",
name="User Name",
@@ -287,6 +297,7 @@ async def test_loading_saving_data(hass, registry):
assert new_entry2.original_icon == "hass:original-icon"
assert new_entry2.original_name == "Original Name"
assert new_entry2.supported_features == 5
+ assert new_entry2.translation_key == "initial-translation_key"
assert new_entry2.unit_of_measurement == "initial-unit_of_measurement"
@@ -655,9 +666,10 @@ async def test_update_entity(registry):
)
for attr_name, new_value in (
- ("name", "new name"),
- ("icon", "new icon"),
+ ("aliases", {"alias_1", "alias_2"}),
("disabled_by", er.RegistryEntryDisabler.USER),
+ ("icon", "new icon"),
+ ("name", "new name"),
):
changes = {attr_name: new_value}
updated_entry = registry.async_update_entity(entry.entity_id, **changes)
diff --git a/tests/helpers/test_schema_config_entry_flow.py b/tests/helpers/test_schema_config_entry_flow.py
index b01da8d3d2a..00935677a21 100644
--- a/tests/helpers/test_schema_config_entry_flow.py
+++ b/tests/helpers/test_schema_config_entry_flow.py
@@ -1,6 +1,7 @@
"""Tests for the schema based data entry flows."""
from __future__ import annotations
+from collections.abc import Mapping
from typing import Any
from unittest.mock import patch
@@ -8,7 +9,7 @@ import pytest
import voluptuous as vol
from homeassistant import config_entries, data_entry_flow
-from homeassistant.core import HomeAssistant
+from homeassistant.core import HomeAssistant, callback
from homeassistant.data_entry_flow import FlowResultType
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.schema_config_entry_flow import (
@@ -27,6 +28,17 @@ from tests.common import MockConfigEntry, mock_platform
TEST_DOMAIN = "test"
+class TestSchemaConfigFlowHandler(SchemaConfigFlowHandler):
+ """Bare minimum SchemaConfigFlowHandler."""
+
+ config_flow = {}
+
+ @callback
+ def async_config_entry_title(self, options: Mapping[str, Any]) -> str:
+ """Return config entry title."""
+ return "title"
+
+
@pytest.fixture(name="manager")
def manager_fixture():
"""Return a flow manager."""
@@ -116,7 +128,7 @@ async def test_config_flow_advanced_option(
}
@manager.mock_reg_handler("test")
- class TestFlow(SchemaConfigFlowHandler):
+ class TestFlow(TestSchemaConfigFlowHandler):
config_flow = CONFIG_FLOW
# Start flow in basic mode
@@ -210,7 +222,7 @@ async def test_options_flow_advanced_option(
"init": SchemaFlowFormStep(OPTIONS_SCHEMA)
}
- class TestFlow(SchemaConfigFlowHandler, domain="test"):
+ class TestFlow(TestSchemaConfigFlowHandler, domain="test"):
config_flow = {}
options_flow = OPTIONS_FLOW
@@ -314,7 +326,7 @@ async def test_menu_step(hass: HomeAssistant) -> None:
"option4": SchemaFlowFormStep(vol.Schema({})),
}
- class TestConfigFlow(SchemaConfigFlowHandler, domain=TEST_DOMAIN):
+ class TestConfigFlow(TestSchemaConfigFlowHandler, domain=TEST_DOMAIN):
"""Handle a config or options flow for Derivative."""
config_flow = CONFIG_FLOW
@@ -363,7 +375,7 @@ async def test_schema_none(hass: HomeAssistant) -> None:
"option3": SchemaFlowFormStep(vol.Schema({})),
}
- class TestConfigFlow(SchemaConfigFlowHandler, domain=TEST_DOMAIN):
+ class TestConfigFlow(TestSchemaConfigFlowHandler, domain=TEST_DOMAIN):
"""Handle a config or options flow for Derivative."""
config_flow = CONFIG_FLOW
@@ -397,7 +409,7 @@ async def test_last_step(hass: HomeAssistant) -> None:
"step3": SchemaFlowFormStep(vol.Schema({}), next_step=None),
}
- class TestConfigFlow(SchemaConfigFlowHandler, domain=TEST_DOMAIN):
+ class TestConfigFlow(TestSchemaConfigFlowHandler, domain=TEST_DOMAIN):
"""Handle a config or options flow for Derivative."""
config_flow = CONFIG_FLOW
@@ -440,7 +452,7 @@ async def test_next_step_function(hass: HomeAssistant) -> None:
"step2": SchemaFlowFormStep(vol.Schema({}), next_step=_step2_next_step),
}
- class TestConfigFlow(SchemaConfigFlowHandler, domain=TEST_DOMAIN):
+ class TestConfigFlow(TestSchemaConfigFlowHandler, domain=TEST_DOMAIN):
"""Handle a config or options flow for Derivative."""
config_flow = CONFIG_FLOW
@@ -497,7 +509,7 @@ async def test_suggested_values(
),
}
- class TestFlow(SchemaConfigFlowHandler, domain="test"):
+ class TestFlow(TestSchemaConfigFlowHandler, domain="test"):
config_flow = {}
options_flow = OPTIONS_FLOW
@@ -608,7 +620,7 @@ async def test_options_flow_state(hass: HomeAssistant) -> None:
),
}
- class TestFlow(SchemaConfigFlowHandler, domain="test"):
+ class TestFlow(TestSchemaConfigFlowHandler, domain="test"):
config_flow = {}
options_flow = OPTIONS_FLOW
diff --git a/tests/pylint/test_enforce_type_hints.py b/tests/pylint/test_enforce_type_hints.py
index 9abd2c89a74..665a4d6594c 100644
--- a/tests/pylint/test_enforce_type_hints.py
+++ b/tests/pylint/test_enforce_type_hints.py
@@ -898,7 +898,7 @@ def test_invalid_device_class(
pylint.testutils.MessageTest(
msg_id="hass-return-type",
node=prop_node,
- args=(["CoverDeviceClass", "str", None], "device_class"),
+ args=(["CoverDeviceClass", None], "device_class"),
line=12,
col_offset=4,
end_line=12,
diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py
index b1e3fd760d5..994c220adc4 100644
--- a/tests/test_config_entries.py
+++ b/tests/test_config_entries.py
@@ -903,7 +903,7 @@ async def test_setup_raise_not_ready(hass, caplog):
p_hass, p_wait_time, p_setup = mock_call.mock_calls[0][1]
assert p_hass is hass
- assert p_wait_time == 5
+ assert 5 <= p_wait_time <= 5.5
assert entry.state is config_entries.ConfigEntryState.SETUP_RETRY
assert entry.reason == "The internet connection is offline"
diff --git a/tests/testing_config/custom_components/test/sensor.py b/tests/testing_config/custom_components/test/sensor.py
index 9584e47ba0b..9b3911313a6 100644
--- a/tests/testing_config/custom_components/test/sensor.py
+++ b/tests/testing_config/custom_components/test/sensor.py
@@ -13,6 +13,7 @@ from homeassistant.const import (
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
CONCENTRATION_PARTS_PER_MILLION,
FREQUENCY_GIGAHERTZ,
+ LIGHT_LUX,
PERCENTAGE,
POWER_VOLT_AMPERE,
POWER_VOLT_AMPERE_REACTIVE,
@@ -31,7 +32,7 @@ UNITS_OF_MEASUREMENT = {
SensorDeviceClass.CO: CONCENTRATION_PARTS_PER_MILLION, # ppm of CO concentration
SensorDeviceClass.CO2: CONCENTRATION_PARTS_PER_MILLION, # ppm of CO2 concentration
SensorDeviceClass.HUMIDITY: PERCENTAGE, # % of humidity in the air
- SensorDeviceClass.ILLUMINANCE: "lm", # current light level (lx/lm)
+ SensorDeviceClass.ILLUMINANCE: LIGHT_LUX, # current light level lx
SensorDeviceClass.MOISTURE: PERCENTAGE, # % of water in a substance
SensorDeviceClass.NITROGEN_DIOXIDE: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, # µg/m³ of nitrogen dioxide
SensorDeviceClass.NITROGEN_MONOXIDE: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, # µg/m³ of nitrogen monoxide
@@ -70,7 +71,7 @@ def init(empty=False):
name=f"{device_class} sensor",
unique_id=f"unique_{device_class}",
device_class=device_class,
- unit_of_measurement=UNITS_OF_MEASUREMENT.get(device_class),
+ native_unit_of_measurement=UNITS_OF_MEASUREMENT.get(device_class),
)
for device_class in DEVICE_CLASSES
}
@@ -107,6 +108,11 @@ class MockSensor(MockEntity, SensorEntity):
"""Return the native value of this sensor."""
return self._handle("native_value")
+ @property
+ def options(self):
+ """Return the options for this sensor."""
+ return self._handle("options")
+
@property
def state_class(self):
"""Return the state class of this sensor."""
diff --git a/tests/util/test_unit_conversion.py b/tests/util/test_unit_conversion.py
index b2b99d6f8c0..83aaf6224b5 100644
--- a/tests/util/test_unit_conversion.py
+++ b/tests/util/test_unit_conversion.py
@@ -2,7 +2,10 @@
import pytest
from homeassistant.const import (
+ UnitOfDataRate,
+ UnitOfElectricCurrent,
UnitOfEnergy,
+ UnitOfInformation,
UnitOfLength,
UnitOfMass,
UnitOfPower,
@@ -15,8 +18,11 @@ from homeassistant.const import (
from homeassistant.exceptions import HomeAssistantError
from homeassistant.util.unit_conversion import (
BaseUnitConverter,
+ DataRateConverter,
DistanceConverter,
+ ElectricCurrentConverter,
EnergyConverter,
+ InformationConverter,
MassConverter,
PowerConverter,
PressureConverter,
@@ -31,6 +37,7 @@ INVALID_SYMBOL = "bob"
@pytest.mark.parametrize(
"converter,valid_unit",
[
+ (DataRateConverter, UnitOfDataRate.GIBIBYTES_PER_SECOND),
(DistanceConverter, UnitOfLength.KILOMETERS),
(DistanceConverter, UnitOfLength.METERS),
(DistanceConverter, UnitOfLength.CENTIMETERS),
@@ -39,10 +46,13 @@ INVALID_SYMBOL = "bob"
(DistanceConverter, UnitOfLength.YARDS),
(DistanceConverter, UnitOfLength.FEET),
(DistanceConverter, UnitOfLength.INCHES),
+ (ElectricCurrentConverter, UnitOfElectricCurrent.AMPERE),
+ (ElectricCurrentConverter, UnitOfElectricCurrent.MILLIAMPERE),
(EnergyConverter, UnitOfEnergy.WATT_HOUR),
(EnergyConverter, UnitOfEnergy.KILO_WATT_HOUR),
(EnergyConverter, UnitOfEnergy.MEGA_WATT_HOUR),
(EnergyConverter, UnitOfEnergy.GIGA_JOULE),
+ (InformationConverter, UnitOfInformation.GIGABYTES),
(MassConverter, UnitOfMass.GRAMS),
(MassConverter, UnitOfMass.KILOGRAMS),
(MassConverter, UnitOfMass.MICROGRAMS),
@@ -85,8 +95,11 @@ def test_convert_same_unit(converter: type[BaseUnitConverter], valid_unit: str)
@pytest.mark.parametrize(
"converter,valid_unit",
[
+ (DataRateConverter, UnitOfDataRate.GIBIBYTES_PER_SECOND),
(DistanceConverter, UnitOfLength.KILOMETERS),
+ (ElectricCurrentConverter, UnitOfElectricCurrent.AMPERE),
(EnergyConverter, UnitOfEnergy.KILO_WATT_HOUR),
+ (InformationConverter, UnitOfInformation.GIBIBYTES),
(MassConverter, UnitOfMass.GRAMS),
(PowerConverter, UnitOfPower.WATT),
(PressureConverter, UnitOfPressure.PA),
@@ -111,8 +124,18 @@ def test_convert_invalid_unit(
@pytest.mark.parametrize(
"converter,from_unit,to_unit",
[
+ (
+ DataRateConverter,
+ UnitOfDataRate.BYTES_PER_SECOND,
+ UnitOfDataRate.BITS_PER_SECOND,
+ ),
(DistanceConverter, UnitOfLength.KILOMETERS, UnitOfLength.METERS),
(EnergyConverter, UnitOfEnergy.WATT_HOUR, UnitOfEnergy.KILO_WATT_HOUR),
+ (
+ InformationConverter,
+ UnitOfInformation.GIBIBYTES,
+ UnitOfInformation.GIGABYTES,
+ ),
(MassConverter, UnitOfMass.GRAMS, UnitOfMass.KILOGRAMS),
(PowerConverter, UnitOfPower.WATT, UnitOfPower.KILO_WATT),
(PressureConverter, UnitOfPressure.HPA, UnitOfPressure.INHG),
@@ -132,8 +155,21 @@ def test_convert_nonnumeric_value(
@pytest.mark.parametrize(
"converter,from_unit,to_unit,expected",
[
+ (
+ DataRateConverter,
+ UnitOfDataRate.BITS_PER_SECOND,
+ UnitOfDataRate.BYTES_PER_SECOND,
+ 8,
+ ),
(DistanceConverter, UnitOfLength.KILOMETERS, UnitOfLength.METERS, 1 / 1000),
+ (
+ ElectricCurrentConverter,
+ UnitOfElectricCurrent.AMPERE,
+ UnitOfElectricCurrent.MILLIAMPERE,
+ 1 / 1000,
+ ),
(EnergyConverter, UnitOfEnergy.WATT_HOUR, UnitOfEnergy.KILO_WATT_HOUR, 1000),
+ (InformationConverter, UnitOfInformation.BITS, UnitOfInformation.BYTES, 8),
(PowerConverter, UnitOfPower.WATT, UnitOfPower.KILO_WATT, 1000),
(
PressureConverter,
@@ -168,6 +204,65 @@ def test_get_unit_ratio(
assert converter.get_unit_ratio(from_unit, to_unit) == expected
+@pytest.mark.parametrize(
+ "value,from_unit,expected,to_unit",
+ [
+ (8e3, UnitOfDataRate.BITS_PER_SECOND, 8, UnitOfDataRate.KILOBITS_PER_SECOND),
+ (8e6, UnitOfDataRate.BITS_PER_SECOND, 8, UnitOfDataRate.MEGABITS_PER_SECOND),
+ (8e9, UnitOfDataRate.BITS_PER_SECOND, 8, UnitOfDataRate.GIGABITS_PER_SECOND),
+ (8, UnitOfDataRate.BITS_PER_SECOND, 1, UnitOfDataRate.BYTES_PER_SECOND),
+ (8e3, UnitOfDataRate.BITS_PER_SECOND, 1, UnitOfDataRate.KILOBYTES_PER_SECOND),
+ (8e6, UnitOfDataRate.BITS_PER_SECOND, 1, UnitOfDataRate.MEGABYTES_PER_SECOND),
+ (8e9, UnitOfDataRate.BITS_PER_SECOND, 1, UnitOfDataRate.GIGABYTES_PER_SECOND),
+ (
+ 8 * 2**10,
+ UnitOfDataRate.BITS_PER_SECOND,
+ 1,
+ UnitOfDataRate.KIBIBYTES_PER_SECOND,
+ ),
+ (
+ 8 * 2**20,
+ UnitOfDataRate.BITS_PER_SECOND,
+ 1,
+ UnitOfDataRate.MEBIBYTES_PER_SECOND,
+ ),
+ (
+ 8 * 2**30,
+ UnitOfDataRate.BITS_PER_SECOND,
+ 1,
+ UnitOfDataRate.GIBIBYTES_PER_SECOND,
+ ),
+ ],
+)
+def test_data_rate_convert(
+ value: float,
+ from_unit: str,
+ expected: float,
+ to_unit: str,
+) -> None:
+ """Test conversion to other units."""
+ assert DataRateConverter.convert(value, from_unit, to_unit) == pytest.approx(
+ expected
+ )
+
+
+@pytest.mark.parametrize(
+ "value,from_unit,expected,to_unit",
+ [
+ (5, UnitOfElectricCurrent.AMPERE, 5000, UnitOfElectricCurrent.MILLIAMPERE),
+ (5, UnitOfElectricCurrent.MILLIAMPERE, 0.005, UnitOfElectricCurrent.AMPERE),
+ ],
+)
+def test_electric_current_convert(
+ value: float,
+ from_unit: str,
+ expected: float,
+ to_unit: str,
+) -> None:
+ """Test conversion to other units."""
+ assert ElectricCurrentConverter.convert(value, from_unit, to_unit) == expected
+
+
@pytest.mark.parametrize(
"value,from_unit,expected,to_unit",
[
@@ -307,6 +402,43 @@ def test_energy_convert(
assert EnergyConverter.convert(value, from_unit, to_unit) == expected
+@pytest.mark.parametrize(
+ "value,from_unit,expected,to_unit",
+ [
+ (8e3, UnitOfInformation.BITS, 8, UnitOfInformation.KILOBITS),
+ (8e6, UnitOfInformation.BITS, 8, UnitOfInformation.MEGABITS),
+ (8e9, UnitOfInformation.BITS, 8, UnitOfInformation.GIGABITS),
+ (8, UnitOfInformation.BITS, 1, UnitOfInformation.BYTES),
+ (8e3, UnitOfInformation.BITS, 1, UnitOfInformation.KILOBYTES),
+ (8e6, UnitOfInformation.BITS, 1, UnitOfInformation.MEGABYTES),
+ (8e9, UnitOfInformation.BITS, 1, UnitOfInformation.GIGABYTES),
+ (8e12, UnitOfInformation.BITS, 1, UnitOfInformation.TERABYTES),
+ (8e15, UnitOfInformation.BITS, 1, UnitOfInformation.PETABYTES),
+ (8e18, UnitOfInformation.BITS, 1, UnitOfInformation.EXABYTES),
+ (8e21, UnitOfInformation.BITS, 1, UnitOfInformation.ZETTABYTES),
+ (8e24, UnitOfInformation.BITS, 1, UnitOfInformation.YOTTABYTES),
+ (8 * 2**10, UnitOfInformation.BITS, 1, UnitOfInformation.KIBIBYTES),
+ (8 * 2**20, UnitOfInformation.BITS, 1, UnitOfInformation.MEBIBYTES),
+ (8 * 2**30, UnitOfInformation.BITS, 1, UnitOfInformation.GIBIBYTES),
+ (8 * 2**40, UnitOfInformation.BITS, 1, UnitOfInformation.TEBIBYTES),
+ (8 * 2**50, UnitOfInformation.BITS, 1, UnitOfInformation.PEBIBYTES),
+ (8 * 2**60, UnitOfInformation.BITS, 1, UnitOfInformation.EXBIBYTES),
+ (8 * 2**70, UnitOfInformation.BITS, 1, UnitOfInformation.ZEBIBYTES),
+ (8 * 2**80, UnitOfInformation.BITS, 1, UnitOfInformation.YOBIBYTES),
+ ],
+)
+def test_information_convert(
+ value: float,
+ from_unit: str,
+ expected: float,
+ to_unit: str,
+) -> None:
+ """Test conversion to other units."""
+ assert InformationConverter.convert(value, from_unit, to_unit) == pytest.approx(
+ expected
+ )
+
+
@pytest.mark.parametrize(
"value,from_unit,expected,to_unit",
[
@@ -345,6 +477,11 @@ def test_energy_convert(
(16, UnitOfMass.OUNCES, 453592.37, UnitOfMass.MILLIGRAMS),
(16, UnitOfMass.OUNCES, 453592370, UnitOfMass.MICROGRAMS),
(16, UnitOfMass.OUNCES, 1, UnitOfMass.POUNDS),
+ (1, UnitOfMass.STONES, pytest.approx(6.350293), UnitOfMass.KILOGRAMS),
+ (1, UnitOfMass.STONES, pytest.approx(6350.293), UnitOfMass.GRAMS),
+ (1, UnitOfMass.STONES, pytest.approx(6350293), UnitOfMass.MILLIGRAMS),
+ (1, UnitOfMass.STONES, pytest.approx(14), UnitOfMass.POUNDS),
+ (1, UnitOfMass.STONES, pytest.approx(224), UnitOfMass.OUNCES),
],
)
def test_mass_convert(
@@ -559,110 +696,45 @@ def test_temperature_convert_with_interval(
@pytest.mark.parametrize(
"value,from_unit,expected,to_unit",
[
- (5, UnitOfVolume.LITERS, pytest.approx(1.32086), UnitOfVolume.GALLONS),
- (5, UnitOfVolume.GALLONS, pytest.approx(18.92706), UnitOfVolume.LITERS),
- (
- 5,
- UnitOfVolume.CUBIC_METERS,
- pytest.approx(176.5733335),
- UnitOfVolume.CUBIC_FEET,
- ),
- (
- 500,
- UnitOfVolume.CUBIC_FEET,
- pytest.approx(14.1584233),
- UnitOfVolume.CUBIC_METERS,
- ),
- (
- 500,
- UnitOfVolume.CUBIC_FEET,
- pytest.approx(14.1584233),
- UnitOfVolume.CUBIC_METERS,
- ),
- (
- 500,
- UnitOfVolume.CUBIC_FEET,
- pytest.approx(478753.2467),
- UnitOfVolume.FLUID_OUNCES,
- ),
- (500, UnitOfVolume.CUBIC_FEET, pytest.approx(3740.25974), UnitOfVolume.GALLONS),
- (
- 500,
- UnitOfVolume.CUBIC_FEET,
- pytest.approx(14158.42329599),
- UnitOfVolume.LITERS,
- ),
- (
- 500,
- UnitOfVolume.CUBIC_FEET,
- pytest.approx(14158423.29599),
- UnitOfVolume.MILLILITERS,
- ),
+ (5, UnitOfVolume.LITERS, 1.32086, UnitOfVolume.GALLONS),
+ (5, UnitOfVolume.GALLONS, 18.92706, UnitOfVolume.LITERS),
+ (5, UnitOfVolume.CUBIC_METERS, 176.5733335, UnitOfVolume.CUBIC_FEET),
+ (500, UnitOfVolume.CUBIC_FEET, 14.1584233, UnitOfVolume.CUBIC_METERS),
+ (500, UnitOfVolume.CUBIC_FEET, 14.1584233, UnitOfVolume.CUBIC_METERS),
+ (500, UnitOfVolume.CUBIC_FEET, 478753.2467, UnitOfVolume.FLUID_OUNCES),
+ (500, UnitOfVolume.CUBIC_FEET, 3740.25974, UnitOfVolume.GALLONS),
+ (500, UnitOfVolume.CUBIC_FEET, 14158.42329599, UnitOfVolume.LITERS),
+ (500, UnitOfVolume.CUBIC_FEET, 14158423.29599, UnitOfVolume.MILLILITERS),
(500, UnitOfVolume.CUBIC_METERS, 500, UnitOfVolume.CUBIC_METERS),
- (
- 500,
- UnitOfVolume.CUBIC_METERS,
- pytest.approx(16907011.35),
- UnitOfVolume.FLUID_OUNCES,
- ),
- (
- 500,
- UnitOfVolume.CUBIC_METERS,
- pytest.approx(132086.02617),
- UnitOfVolume.GALLONS,
- ),
+ (500, UnitOfVolume.CUBIC_METERS, 16907011.35, UnitOfVolume.FLUID_OUNCES),
+ (500, UnitOfVolume.CUBIC_METERS, 132086.02617, UnitOfVolume.GALLONS),
(500, UnitOfVolume.CUBIC_METERS, 500000, UnitOfVolume.LITERS),
(500, UnitOfVolume.CUBIC_METERS, 500000000, UnitOfVolume.MILLILITERS),
- (
- 500,
- UnitOfVolume.FLUID_OUNCES,
- pytest.approx(0.52218967),
- UnitOfVolume.CUBIC_FEET,
- ),
- (
- 500,
- UnitOfVolume.FLUID_OUNCES,
- pytest.approx(0.014786764),
- UnitOfVolume.CUBIC_METERS,
- ),
+ (500, UnitOfVolume.FLUID_OUNCES, 0.52218967, UnitOfVolume.CUBIC_FEET),
+ (500, UnitOfVolume.FLUID_OUNCES, 0.014786764, UnitOfVolume.CUBIC_METERS),
(500, UnitOfVolume.FLUID_OUNCES, 3.90625, UnitOfVolume.GALLONS),
- (500, UnitOfVolume.FLUID_OUNCES, pytest.approx(14.786764), UnitOfVolume.LITERS),
- (
- 500,
- UnitOfVolume.FLUID_OUNCES,
- pytest.approx(14786.764),
- UnitOfVolume.MILLILITERS,
- ),
- (500, UnitOfVolume.GALLONS, pytest.approx(66.84027), UnitOfVolume.CUBIC_FEET),
- (500, UnitOfVolume.GALLONS, pytest.approx(1.892706), UnitOfVolume.CUBIC_METERS),
+ (500, UnitOfVolume.FLUID_OUNCES, 14.786764, UnitOfVolume.LITERS),
+ (500, UnitOfVolume.FLUID_OUNCES, 14786.764, UnitOfVolume.MILLILITERS),
+ (500, UnitOfVolume.GALLONS, 66.84027, UnitOfVolume.CUBIC_FEET),
+ (500, UnitOfVolume.GALLONS, 1.892706, UnitOfVolume.CUBIC_METERS),
(500, UnitOfVolume.GALLONS, 64000, UnitOfVolume.FLUID_OUNCES),
- (500, UnitOfVolume.GALLONS, pytest.approx(1892.70589), UnitOfVolume.LITERS),
- (
- 500,
- UnitOfVolume.GALLONS,
- pytest.approx(1892705.89),
- UnitOfVolume.MILLILITERS,
- ),
- (500, UnitOfVolume.LITERS, pytest.approx(17.65733), UnitOfVolume.CUBIC_FEET),
+ (500, UnitOfVolume.GALLONS, 1892.70589, UnitOfVolume.LITERS),
+ (500, UnitOfVolume.GALLONS, 1892705.89, UnitOfVolume.MILLILITERS),
+ (500, UnitOfVolume.LITERS, 17.65733, UnitOfVolume.CUBIC_FEET),
(500, UnitOfVolume.LITERS, 0.5, UnitOfVolume.CUBIC_METERS),
- (500, UnitOfVolume.LITERS, pytest.approx(16907.011), UnitOfVolume.FLUID_OUNCES),
- (500, UnitOfVolume.LITERS, pytest.approx(132.086), UnitOfVolume.GALLONS),
+ (500, UnitOfVolume.LITERS, 16907.011, UnitOfVolume.FLUID_OUNCES),
+ (500, UnitOfVolume.LITERS, 132.086, UnitOfVolume.GALLONS),
(500, UnitOfVolume.LITERS, 500000, UnitOfVolume.MILLILITERS),
- (
- 500,
- UnitOfVolume.MILLILITERS,
- pytest.approx(0.01765733),
- UnitOfVolume.CUBIC_FEET,
- ),
+ (500, UnitOfVolume.MILLILITERS, 0.01765733, UnitOfVolume.CUBIC_FEET),
(500, UnitOfVolume.MILLILITERS, 0.0005, UnitOfVolume.CUBIC_METERS),
- (
- 500,
- UnitOfVolume.MILLILITERS,
- pytest.approx(16.907),
- UnitOfVolume.FLUID_OUNCES,
- ),
- (500, UnitOfVolume.MILLILITERS, pytest.approx(0.132086), UnitOfVolume.GALLONS),
+ (500, UnitOfVolume.MILLILITERS, 16.907, UnitOfVolume.FLUID_OUNCES),
+ (500, UnitOfVolume.MILLILITERS, 0.132086, UnitOfVolume.GALLONS),
(500, UnitOfVolume.MILLILITERS, 0.5, UnitOfVolume.LITERS),
+ (5, UnitOfVolume.CENTUM_CUBIC_FEET, 500, UnitOfVolume.CUBIC_FEET),
+ (5, UnitOfVolume.CENTUM_CUBIC_FEET, 14.15842, UnitOfVolume.CUBIC_METERS),
+ (5, UnitOfVolume.CENTUM_CUBIC_FEET, 478753.24, UnitOfVolume.FLUID_OUNCES),
+ (5, UnitOfVolume.CENTUM_CUBIC_FEET, 3740.26, UnitOfVolume.GALLONS),
+ (5, UnitOfVolume.CENTUM_CUBIC_FEET, 14158.42, UnitOfVolume.LITERS),
],
)
def test_volume_convert(
@@ -672,4 +744,4 @@ def test_volume_convert(
to_unit: str,
) -> None:
"""Test conversion to other units."""
- assert VolumeConverter.convert(value, from_unit, to_unit) == expected
+ assert VolumeConverter.convert(value, from_unit, to_unit) == pytest.approx(expected)
diff --git a/tests/util/test_unit_system.py b/tests/util/test_unit_system.py
index 03385196cab..4813bc34a3a 100644
--- a/tests/util/test_unit_system.py
+++ b/tests/util/test_unit_system.py
@@ -380,6 +380,24 @@ def test_get_unit_system_invalid(key: str) -> None:
@pytest.mark.parametrize(
"device_class, original_unit, state_unit",
(
+ # Test atmospheric pressure
+ (
+ SensorDeviceClass.ATMOSPHERIC_PRESSURE,
+ UnitOfPressure.PSI,
+ UnitOfPressure.HPA,
+ ),
+ (
+ SensorDeviceClass.ATMOSPHERIC_PRESSURE,
+ UnitOfPressure.BAR,
+ UnitOfPressure.HPA,
+ ),
+ (
+ SensorDeviceClass.ATMOSPHERIC_PRESSURE,
+ UnitOfPressure.INHG,
+ UnitOfPressure.HPA,
+ ),
+ (SensorDeviceClass.ATMOSPHERIC_PRESSURE, UnitOfPressure.HPA, None),
+ (SensorDeviceClass.ATMOSPHERIC_PRESSURE, "very_much", None),
# Test distance conversion
(SensorDeviceClass.DISTANCE, UnitOfLength.FEET, UnitOfLength.METERS),
(SensorDeviceClass.DISTANCE, UnitOfLength.INCHES, UnitOfLength.MILLIMETERS),
@@ -391,6 +409,10 @@ def test_get_unit_system_invalid(key: str) -> None:
(SensorDeviceClass.GAS, UnitOfVolume.CUBIC_FEET, UnitOfVolume.CUBIC_METERS),
(SensorDeviceClass.GAS, UnitOfVolume.CUBIC_METERS, None),
(SensorDeviceClass.GAS, "very_much", None),
+ # Test pressure conversion
+ (SensorDeviceClass.PRESSURE, UnitOfPressure.PSI, UnitOfPressure.KPA),
+ (SensorDeviceClass.PRESSURE, UnitOfPressure.BAR, None),
+ (SensorDeviceClass.PRESSURE, "very_much", None),
# Test speed conversion
(
SensorDeviceClass.SPEED,
@@ -435,6 +457,24 @@ def test_get_metric_converted_unit_(
@pytest.mark.parametrize(
"device_class, original_unit, state_unit",
(
+ # Test atmospheric pressure
+ (
+ SensorDeviceClass.ATMOSPHERIC_PRESSURE,
+ UnitOfPressure.PSI,
+ UnitOfPressure.INHG,
+ ),
+ (
+ SensorDeviceClass.ATMOSPHERIC_PRESSURE,
+ UnitOfPressure.BAR,
+ UnitOfPressure.INHG,
+ ),
+ (
+ SensorDeviceClass.ATMOSPHERIC_PRESSURE,
+ UnitOfPressure.HPA,
+ UnitOfPressure.INHG,
+ ),
+ (SensorDeviceClass.ATMOSPHERIC_PRESSURE, UnitOfPressure.INHG, None),
+ (SensorDeviceClass.ATMOSPHERIC_PRESSURE, "very_much", None),
# Test distance conversion
(SensorDeviceClass.DISTANCE, UnitOfLength.CENTIMETERS, UnitOfLength.INCHES),
(SensorDeviceClass.DISTANCE, UnitOfLength.KILOMETERS, UnitOfLength.MILES),
@@ -446,6 +486,10 @@ def test_get_metric_converted_unit_(
(SensorDeviceClass.GAS, UnitOfVolume.CUBIC_METERS, UnitOfVolume.CUBIC_FEET),
(SensorDeviceClass.GAS, UnitOfVolume.CUBIC_FEET, None),
(SensorDeviceClass.GAS, "very_much", None),
+ # Test pressure conversion
+ (SensorDeviceClass.PRESSURE, UnitOfPressure.BAR, UnitOfPressure.PSI),
+ (SensorDeviceClass.PRESSURE, UnitOfPressure.PSI, None),
+ (SensorDeviceClass.PRESSURE, "very_much", None),
# Test speed conversion
(
SensorDeviceClass.SPEED,